Compare commits

...

523 Commits

Author SHA1 Message Date
PJB3005
ffacda1619 Version: 267.4.2 2025-12-02 00:56:57 +01:00
PJB3005
0882d1ce65 Fix NetBitArraySerializer compatibility.
Apparently NetSerializer treats IDynamicTypeSerializer and IStaticTypeSerializer differently for sealed types??

(cherry-picked from 6bbeaeeba6, without the test changes)

(cherry picked from commit d187018834dfa1cdead9ce5a7b72c38b07f8c81f)
2025-12-02 00:56:57 +01:00
PJB3005
a973c0e6ec Version: 267.4.1 2025-12-01 16:05:18 +01:00
PJB3005
3095d90f5c Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
2025-12-01 16:05:17 +01:00
metalgearsloth
c95b4320cf Version: 267.4.0 2025-11-09 18:44:49 +11:00
metalgearsloth
4755cb5747 Better broadphase performance (#6272)
* Better broadphase parallelism

Moves more stuff into the parallel loop and avoids allocating the list per fixtureproxy.

* Fixes

* Better docs

* doc
2025-11-08 02:11:31 +11:00
PJB3005
7bc0ffb711 Make XAML hot reload JIT on UI load
This means we don't have to JIT a bunch of UIs that you might not open, reducing memory usage and startup overhead.

One (1) UI is always JITed in another thread before prototype UIs are loaded, so as to warm up the JIT machinery. Said type is DropDownDebugConsole which always gets used anyways so there's no harm in it.

In total, these changes save more than a second of startup time for me.
2025-10-30 01:41:17 +01:00
beck-thompson
e49515956a Add a basic loading screen! (#6003)
* Added basic loading screen

* Make it look better!

* I forgor xD

* Fix test fails

* Add comment

* Removed unused import

* Only write to file if the number of sections changed

* Servers can now have their own settings

* Minor optionzation and rare colors

* Remove some of the cvars

* debug only loading messages

* Added a few more steps

* Only one section at a time

* nullable section name

* Lock out functions if finished

* Get rid of saving the ccvar

* Cleanup

* Forgot!

* A few tweaks

* Disable vsync

* remove colors

* remove outdated vsync functions

* Silly me xD

* What I get for trying to be clever... ;(

* Better seconds display

* Simplify drawing logic + it looks better

* Type does not need to be partial

* Make interface to expose to content

* Use correct define to gate showing debug info

Should be TOOLS instead of DEBUG

* Use appropriate exception type in BeginLoadingSection

* Fix exception when closing window during loading screen

Would try to stop the main loop before it exists.

* Rename CVars, put debug info behind CVar instead of conditional compilation.

* Add to RELEASE-NOTES.md

* Add UI scaling support

* Make ILoadingScreenManager fully internal

Didn't realize content can't touch it as it'd break the total amount of sections

* Don't re-enable vsync manually, GameController does it at the end of init

* Add command to show top load time usage.

* Improve verbosity of debug time tracking

More steps and some steps named better

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-10-30 00:38:59 +01:00
Pieter-Jan Briers
f3a3f564e1 System font API (#5393)
* System font API

This is a new API that allows operating system fonts to be loaded by the engine and used by content.

Fonts are provided in a flat list exposing all the relevant metadata. They are loaded from disk with a Load call.

Initial implementation is only for Windows DirectWrite.

* Load system fonts as memory mapped files if possible.

This allows sharing the font file memory with other processes which is always good.

* Use ArrayPool to reduce char array allocations

* Disable verbose logging

* Implement system font support on Linux via Fontconfig

* Implement macOS support

* Add "FREEDESKTOP" define constant

This is basically LINUX || FREEBSD. Though FreeBSD currently gets detected as LINUX too. Oh well.

* Compile out Fontconfig and CoreText system font backends when not on those platforms

* Don't add Fontconfig package dep on Mac/Windows

* Allow disabling system font support via CVar

Cuz why not.
2025-10-28 22:07:55 +01:00
Leon Friedrich
8b7fbfa646 Add two new custom yaml serializers (#6253)
* Add two new custom yaml serializers

* make ComponentNameSerializer ignore ignored components

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-10-27 23:05:46 +11:00
Leon Friedrich
bb4c4ed302 Fix PredictedQueueDeleteEntity mispredicts (#6260)
* Fix PredictedQueueDeleteEntity

* typo

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-10-27 23:04:12 +11:00
PJB3005
c9009342b6 Move CVar registration to before config load on server
Fixes error on startup with the rollback system.
2025-10-26 23:14:11 +01:00
PJB3005
3a337e4842 SECURE CVars are no longer a thing
Good riddance
2025-10-26 23:13:42 +01:00
PJB3005
e788325cdb Expose more StringBuilder overloads to sandbox
Just some stuff that got added in the years. Spans, interpolated string handlers.
2025-10-26 23:09:59 +01:00
PJB3005
9a0e3b6b02 dmetamem now sorts results, no longer outputs to log to avoid interleaving 2025-10-26 23:09:37 +01:00
PJB3005
37eabbabc2 Add MetadataUpdateHandlerAttribute to sandbox 2025-10-26 22:53:06 +01:00
PJB3005
ab775af7cd Added FontTagHijackHolder to replace fonts resolved by FontTag.
Existing font prototype system is extremely half-baked and unusable. This allows bypassing it entirely for upcoming content changes.
2025-10-26 20:40:39 +01:00
PJB3005
8ac5fc58d2 Invalidate OutputPanel on style change
Fixes an incorrect height staying cached on font change.
2025-10-26 20:37:42 +01:00
PJB3005
37c7aa544e Properly use arrange in rich text control layout
This code is still broken, but this at least fixes the fact they don't get arranged with a proper size.

Supersedes #6269
2025-10-26 17:42:37 +01:00
PJB3005
7542b1ca16 Don't do work if assigning stylesheet a control already has 2025-10-26 17:40:19 +01:00
PJB3005
8235bd8478 OptionButton can now be filtered 2025-10-26 17:38:41 +01:00
PJB3005
1657a49c1c Fix modifying Label.FontOverride not causing a layout update. 2025-10-25 17:56:58 +02:00
Myra
669b515ce6 Ensure that sdl3 is the fallback if unknown windowingAPI is specified (#6266)
* Ensure what sdl3 is a fallback if unknown windowingAPI is specified

Webedit ops

* I am blind
2025-10-23 23:43:57 +02:00
metalgearsloth
8478e62a3e Add pure to some transform methods (#6262)
Useful IDE stuff
2025-10-22 18:47:02 +02:00
PJB3005
034728258c Add config rollback system
This is intended for content-side settings menus, so we can show users a "does this look correct" prompt after changing sensitive settings like graphics or UI, without risking an untimely config save *storing* broken CVar config.
2025-10-22 14:09:40 +02:00
PJB3005
b0fec0fd76 CVars defined in [CVarDefs] can now be private or internal. 2025-10-22 14:06:33 +02:00
Leon Friedrich
665294bee8 Rethrow more exceptions when EXCEPTION_TOLERANCE is false (#6238)
* Rethrow more exceptions when EXCEPTION_TOLERANCE is false

* A

* update test

* Revert "update test"

This reverts commit 37f4da67fc.

* actually we probably want to know if Deleting an exception throwing entity throws another exception
2025-10-20 20:51:24 +02:00
PJB3005
4b04081749 Fix Menu and NumpadDecimal key codes on SDL3
Fixes #6255
2025-10-16 14:39:08 +02:00
Amy
d3a9199b8e my hatred for yaml is building (#6226) 2025-10-15 23:28:47 +02:00
PJB3005
6fb9ff7554 Improve viewport leak logging
Shows name

Also fixes erroneous leak logging oops
2025-10-15 01:23:44 +02:00
ElectroJr
feb9e1db69 Version: 267.3.0 2025-10-14 22:35:41 +13:00
Leon Friedrich
613705613b Fix bug in OccluderSystem.InRangeUnoccluded (#6247) 2025-10-12 12:09:42 +13:00
Pok
80a053c0a9 command-ftl (#6248) 2025-10-10 19:10:40 +02:00
Leon Friedrich
657455dae0 Add abstract tile debug overlay & command (#6213)
* Add generic debug overlay & command

* fix

* Fix overlays

* a

* comments

* comments

* comment 2
2025-10-09 17:49:17 +13:00
Leon Friedrich
8ae35e12ee Update ComponentTreeSystem (#6211)
* Allow component trees to be disabled

* forgot

* I'm pretty sure this wasn't working as intended

* also outdated

* reduce branches in QueueTreeUpdate

* remove update hashset

* try fix

* Use Entity<T> and add ray overloads

* Move InRangeUnoccluded to engine

* reduce code duplication

* move _initialized check

* release notes
2025-10-09 17:30:00 +13:00
Leon Friedrich
4e2c0e431b Fix MapLoaderSystem.SerializeEntitiesRecursive (#6246)
* Fix MapLoaderSystem.SerializeEntitiesRecursive

* a

* oops
2025-10-08 17:50:50 +13:00
MilenVolf
9c41f19eaf Recursively update joint relays on removing entities from containers (#6244)
* Recursively update joint relays on removing entities from containers

* release notes
2025-10-08 17:50:07 +13:00
Nemanja
f75ce13f00 Always align newly created entities with the grid (#5915)
* align spawns with grids

* :godmode:

* Fix comment

* fix

* release notes
2025-10-06 17:37:48 +13:00
Rouden
ac45a0a64b MapId, MapCoordinates, EntityCoordinates Type serializers (#6165)
* New Type Serializers

* Delete NetCoordinatesSerializer.cs

* Make EntityCoordinates and MapCoordinates use DataRecord

* Turn them into actual record structs

I'm somewhat surprised the DataRecord attribute doesn't check this

* Allocate MapIds before deserializing components

* Deserialize preallocated ids

* fix map merge assert

* remove old

* Use TryGetMap

* release notes
2025-10-06 15:27:08 +13:00
Nikita (Nick)
a8a73e28f4 Fix casing in Sandbox.yml (2 characters changed) (#6243)
* Fix casing in DateOnly property in Sandbox.yml

* another typo
2025-10-04 17:08:59 +02:00
PJB3005
e5983a9ec1 Add DateOnly and TimeOnly to sandbox
Added in .NET 6
2025-10-02 19:02:28 +02:00
PJB3005
b7fa39d8cc Update Robust.Natives 2025-10-02 03:19:49 +02:00
Leon Friedrich
3c30ed749c Fix yaml hotreloading (#6239)
* Fix yaml hotreloading

* ToRelativeSystemPath removes the leading /
2025-09-28 19:00:56 +02:00
haiwwkes
eb1a2ae9b4 init (#6234) 2025-09-27 11:32:02 -04:00
PJB3005
ee0c31a8c3 Make SDL3 default
Fixes #5570
2025-09-27 00:31:52 +02:00
PJB3005
4ab61b840a Remove compat mode forcing
Was broken and I'm moving it to the launcher
2025-09-26 15:02:57 +02:00
PJB3005
df29fa438a Re-allow internal access to Content.Benchmarks, only on development builds 2025-09-26 14:19:58 +02:00
PJB3005
a3756c29bd Merge duplicate AssemblyInfo.cs files in Robust.Shared 2025-09-26 14:15:14 +02:00
PJB3005
4dc17f3aca Version: 267.2.1 2025-09-26 13:40:39 +02:00
PJB3005
d22280f177 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:38 +02:00
ElectroJr
9e8f7092ea Version: 267.2.0 2025-09-26 20:03:34 +12:00
pathetic meowmeow
de188cc773 Defer PredictedQueueDel detachment on the client (#6154)
* Defer PredictedQueueDel detachment on the client

* release notes

* fixes

* I love ambiguous implicit casts

* avoid deferral issues

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-26 19:13:14 +12:00
DrSmugleaf
784a02c0e7 Fix rga and mapfile validators breaking because the PYYaml author published a broken version (#6230)
* Version: 162.2.1

* Fix rga and mapfile validators breaking because the PYYaml author published a broken version

* Revert "Version: 162.2.1"

This reverts commit 9b7f4d48cf.
2025-09-26 14:04:21 +12:00
PJB3005
c8db7f98db Move IResourceManager.GetContentRoots() to be internal, make it return strings instead
Previous API shouldn't have been content-accessible. It also returned OS paths over ResPath which is incorrect.
2025-09-23 15:07:58 +02:00
DrSmugleaf
318c37e686 Fix CollectionExtensions.TryGetValue erroring for indexes under 0 (#6222)
* Version: 162.2.1

* Fix CollectionExtensions.TryGetValue erroring for indexes under 0

* Revert "Version: 162.2.1"

This reverts commit 9b7f4d48cf.

* release notes

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-23 16:04:09 +12:00
Leon Friedrich
524be86449 Add new DetachEntity overload (#6217) 2025-09-23 15:47:29 +12:00
Leon Friedrich
ddeb78accd Make toolshed's CommandImplementationAttribute optional (#6218) 2025-09-23 15:47:21 +12:00
Leon Friedrich
585e847818 Fix warnings (#6224)
* Fix warnings

* remove usings
2025-09-23 15:41:08 +12:00
OnsenCapy
ac3cb4dc2a SpriteComponent: allow animations to optionally stop instead of looping (#6210)
* Loop false

* Removed goobstation comments

* Addressed changes

* Add layer field, set auto animated

* release notes

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-21 14:36:07 +12:00
PJB3005
c06ca39009 Version: 267.1.0 2025-09-19 01:24:02 +02:00
PJB3005
06b11a51f1 Update release notes
That's a lotta stuff.

Decided to try to categorize things this time. See how this works out going forward.
2025-09-19 01:22:36 +02:00
DrSmugleaf
4938a159d4 Change PhysicsSystem.Island.FinalisePositions to TryComp transform component (#6135)
* Change PhysicsSystem.Island.FinalisePositions to TryComp transform component

* Add comment

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-09-18 17:58:54 +02:00
Kara
27f2e270ce Fix compat mode floats in light attenuation + release notes (#6209)
* Fix compat mode float shit

* release notes
2025-09-18 17:44:02 +02:00
Kara
94e60e0b10 More accurate & controllable pointlight attenuation (#6160)
* modify light attenuation function

* support for changing attenuation curve type + lots of docs

* this is what i defaulted to typing in a prototype, so i guess it should just be this instead

* Allow a continuous range of values between inverse and inversequadratic rather than two set curves

* calc is slang for calculator

* fix

* oops committed it at 1 while testing i think, values are balanced for 0
2025-09-18 16:58:35 +02:00
slarticodefast
c6863033a5 fix PlacementManager.CurrentMousePosition during integration tests (#6208)
* fix CurrentMousePosition

* shorter

* rerun test
2025-09-18 16:38:39 +02:00
metalgearsloth
917878d05f Autocomplete more map commands (#5797)
* Autocomplete more map commands

Also added some extra helper features.

* Finish

* Fix bug, avoid IocResolves

* grid is grid

* file filename clash

* turn hint into option

* a

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-18 12:38:17 +10:00
Leon Friedrich
10e4766809 Try fix UI state debug assert (#6191)
* Try fix UI state debug assert

* comment
2025-09-18 00:31:57 +02:00
Leon Friedrich
abd5149245 Improve map serialization error logging & exception tolerance (#6188)
* Improve map serialization error logging

* Prevent remove children of erroring entities

* better logging

* Improve error tolerance

* Even more exception tolerance

* missing !

* Improve handling of category errors

Helps prevents weird bugs that arise due to deleting un-initialized entities

* release notes

* Typo fix

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-09-17 21:44:56 +02:00
Leon Friedrich
912b6da20a Fix serialization of data-records with get-only properties (#6204)
* Fix serialization of data-records with get-only properties

* even less nesting

* asserts
2025-09-17 19:11:12 +02:00
eoineoineoin
94fe0b7721 Fix bug wrapping wide utf16 characters; add documentation for function (#6194) 2025-09-17 13:31:07 +02:00
PJB3005
9fac1e78fb Make ACZ status host explicitly respond with UTF-8 charset
See 0b2b814e4f
2025-09-17 12:55:43 +02:00
PJB3005
6697b76683 Fix download_manifest_file.py to always decode as UTF-8
See 0b2b814e4f
2025-09-17 12:52:13 +02:00
Andi Lilaj
b2ab247b5b Created Debug Version Panel and Version Info Printer similar to DebugSystemPanel (#5624)
* Created Debug Version Panel and Version Info Printer similar to DebugSystemPanel

* dependency injection

* remove VersionInformationPrinter

* Fix sorting
2025-09-17 17:32:25 +10:00
deltanedas
7411ae8138 replace AnchorEntity debug assert with a log (#5508)
* replace AnchorEntity debug assert with a log

* unfuck cefglue

* no!

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-17 17:13:18 +10:00
slarticodefast
d398e3a75b Improve error logging in SharedAudioSystem (#6202) 2025-09-17 16:56:21 +10:00
DrSmugleaf
a5047224bb Make net.interp and net.buffer_size CVars CLIENT, REPLICATED instead of CLIENTONLY (#6196)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-09-17 12:16:12 +10:00
PJB3005
058821c08b Make sending net messages while disconnected on client log an error. 2025-09-16 18:59:24 +02:00
PJB3005
0c691b061d Don't send client replicated CVars when not connected to a server 2025-09-16 18:59:23 +02:00
Skye
51bbc5dc45 Fix resource loading on non-Windows platforms (#6201) 2025-09-16 11:10:30 +02:00
gus
2d3522e752 Allow multiple clients running WebView on a singular machine. (#5947)
* Multiple CEF instances on a single machine

* remove unused

* meh implementation

* cefextension

* me when partials, race conditions and extractiion of methods exists

* Change remote debugging handling

It now defaults to 9222 again. This doesn't seem to cause any issues when launching 3 clients. The debug port can be reconfigured via CVar if desired.

Also disabled debugging by default outside dev builds.

* Lower MaxAttempts to 15

100 was way too much and gave me anxiety idk.

* Fix non-TOOLS default of remote debug port

* Rewrite locking implementation.

It is much smaller, less complicated and probably more robust too. Should be fully atomic (the previous one wasn't).

* Undo unnecessary style changes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-09-16 02:19:59 +02:00
PJB3005
d4f265c314 Fix incorrect path combine in DirLoader and WritableDirProvider
This (and the other couple past commits) reported by Elelzedel.
2025-09-14 14:49:14 +02:00
PJB3005
7654d38612 Move CEF cache out of data directory
Don't want content messing with this...
2025-09-14 14:49:14 +02:00
PJB3005
cdcc255123 Make Robust.Client.WebView.Cef.Program internal. 2025-09-14 14:49:14 +02:00
PJB3005
2f56a6a110 Update SpaceWizards.NFluidSynth to 0.2.2 2025-09-14 14:49:13 +02:00
PJB3005
16fc48cef2 Hide IWritableDirProvider.RootDir on client
This shouldn't be exposed.
2025-09-14 14:49:13 +02:00
āda
5cecbb2cff Fix TransformBounds(Matrix3x2 , Box2Rotated) (#6171)
* jouneys-end

* test

* more tests

* test overkill

* i'm tired

* rip it out

* Keep method but mark it as obsolete

* Release notes

* grammar

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-13 15:11:59 +10:00
PJB3005
6115d6d5cc Give secondary window rendertextures names. 2025-09-13 01:21:22 +02:00
PJB3005
60d26be139 Add devwindow tab listing render targets
Copy TableContainer from content into engine. It's internal though.

Add various stuff to Clyde to allow the UI to access it. Includes a new IClydeInternal.RenderNow() which seems to not completely explode in my face.
2025-09-13 01:21:22 +02:00
ShadowCommander
186392ea80 Refactor grid or map methods (#5124)
* Rename TryGetMapOrGridCoordinates to make it clearer it gets grid first

* Add terminating or deleted checks to TryGetGridOrMapCoordinates

* Add comment to check if TerminatingOrDeleted check is necessary

* Reorganize AttachToGridOrMap to match TryGetGridOrMapCoordinates

* Move validation to method

* Replace internals with TryGetGridOrMapCoordinates

* Explicitly set coordinates type

* Format

* Change name back for now

* Don't duplicate `TerminatingOrDeleted()` check

* Don't call `GetInvWorldMatrix` for the map

* Don't check `TerminatingOrDeleted(uid)` in `TryGetMapOrGridCoordinates()`

* Fix parenting to terminating grid

* Fix matrix error

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-09-11 18:05:04 +10:00
PJB3005
ebe4538d4c Add viewport stuff for caching rendering resources properly.
Content nowadays has a bunch of Overlays that all cache IRenderTextures for various funny operations. These are all broken in the face of multiple viewports, as they need to be cached *per viewport*.

This commit adds an ID field & an event to allow content to properly handle these resources.

Also adds some debug commands
2025-09-07 00:38:55 +02:00
PJB3005
745d0e5532 Add WeakReference<T> to sandbox 2025-09-07 00:17:07 +02:00
PJB3005
d4f7e60432 dmetamem command is now behind #if TOOLS 2025-09-06 18:24:25 +02:00
IProduceWidgets
ced127c164 loadgrid no fail (#6169)
* yoink unneeded map check

* yarr

* command line feedback
2025-09-04 23:40:17 +02:00
Kara
f91bcb62b1 Add ability to specify easings for AnimationTrackProperty keyframes (#6180)
* Add ability to specify easing functions in `AnimationTrackProperty`

* should actually be per keyframe
2025-09-04 18:47:30 +02:00
Pieter-Jan Briers
5268a4a3f0 Move SDL3 binding to NuGet package (#6184)
I'm worried about the IDE performance overhead of the 20k lines of LibraryImport it generates into Robust.Client.

Also, this allows me to trim the binding, which saves a tiny amount of space from publishes. Always nice to have.
2025-09-04 18:44:49 +02:00
PJB3005
1f1e50539b Remove EngineFonts/ and Midi/ from server packaging
Unnecessary on server
2025-09-04 17:33:40 +02:00
PJB3005
ea3132bbba Add asset pass to drop all files from the Audio/ directory
Fixes #6183

This drops MidiCustom and attribution YAML files. Other audio files were already handled by the audio metadata pass.
2025-09-04 17:33:14 +02:00
Kyoth25f
67ccaec418 Add UnicodeCategory to sandbox (#6181) 2025-09-04 11:30:53 +02:00
Kara
40b70e9447 Fix predicted audio having incorrect position & occlusion until the next tick (#6178) 2025-09-03 00:20:42 +02:00
Wachtel
7d37db9ce0 CleanContainter Del to PredictedDel (#6174) 2025-09-02 23:50:21 +02:00
Kara
dd8688df3d Fix ProcessStream using the wrong entity (#6177) 2025-09-02 22:29:11 +02:00
DrSmugleaf
b02c53c6ad Fix OutputPanel.SetMessage causing the panel to bounce if setting a message other than the last one (#6163) 2025-09-02 22:10:09 +02:00
PJB3005
38d3b83818 Add display.max_fps CVar.
Applies when vsync is not enabled.

Had to shuffle stuff around to GameController since it involves the game loop.

The implementation isn't great and undershoots the target FPS value (because the OS overshoots the desired sleep value). I tried using SDL_DelayPrecise too but this causes significantly increased CPU usage probably because it spinwaits and all that nonsense, so I decided against it.

I don't know why I bothered to do this. I just got the idea in my head. Kinda feels like a waste of time, but there's no point not committing it at this point.
2025-09-02 22:05:28 +02:00
Pieter-Jan Briers
f02cd0083a Make SpriteSpecifier.Texture fail validation if it contains ".rsi" (#6155)
No pointing to PNGs inside RSIs.
2025-09-02 22:05:13 +02:00
PJB3005
c2c8af16d0 Fix framerate dependent UI animations 2025-08-30 17:37:36 +02:00
PJB3005
b61003e2a0 Fix DebugConsole completions in devwindow
PopupContainer now supports an AltOriginUpProperty so that the completion popup doesn't overlap the input bar.
2025-08-30 17:21:15 +02:00
John
fb0ec52f8c Fix TimeSpan overflow on Read (#6170)
A lot of areas use TimeSpan.MaxValue but when saved and read the current time is added which results in an overflow.
A check is now performed to prevent this.
2025-08-30 00:59:39 +02:00
PJB3005
fee79d8aa5 Make popups/modals in secondary windows work
We were relying on a global PopupRoot & ModalRoot, which only existed in the main window. This means things like OptionButton would pop out on the *main* window when put on secondary windows.

These two roots are now on the UIRoot instead. WindowRoot needs to have a function called to create these if you're using it manually, OSWindow supports it automatically.
2025-08-30 00:58:52 +02:00
PJB3005
4508105412 Make devwindow & uitest2 use OSWindow 2025-08-29 01:07:09 +02:00
PJB3005
a0ebb290e2 Add WrapContainer control
Lays items out sequentially, wrapping them onto different rows/columns if they stop fitting.

Has multiple options and should be very useful, in both content and engine.

Uses the new Axis system to implement layout on 4 axis, re-uses BoxContainer's code so BoxContainer also got a mild refactor.
2025-08-29 00:41:21 +02:00
PJB3005
d45497e53b Add Axis helper types to make UI layout code generic over multiple axis. 2025-08-29 00:16:35 +02:00
PJB3005
ff8dd021c3 Make uitest (not uitest2) also support tab parameter. 2025-08-29 00:14:35 +02:00
PJB3005
34a371ef1f Make OrderedChildCollection implement IReadOnlyList<T>
Implements indexing operation.
2025-08-27 19:39:54 +02:00
Amy
83109b08e9 Add hashing lib to sandbox (#6167)
* box  your sand

* I only need this one anyway
2025-08-25 22:12:07 +02:00
DrSmugleaf
3d7b83db05 Fix crash on startup with more than 255 CVars (#6157)
* Fix crash on startup with more than 255 CVars

* Oop
2025-08-25 00:40:52 +02:00
leonidussaks
41b2ee19a1 add check if state name is empty in rsi validator (#6145)
* add check if state name exists

* Apply suggestions from code review

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-08-20 18:49:45 +02:00
PJB3005
856cdb8a3d Version: 267.0.0 2025-08-19 00:36:36 +02:00
PJB3005
b783cd79be Release notes for next release 2025-08-19 00:22:27 +02:00
PJB3005
b5ba964f61 Update client publish script to remove more natives
You know I can probably tell the .NET SDK to not copy these, but figuring that out would be effort.
2025-08-19 00:22:18 +02:00
PJB3005
09676a1d9f Re-enable FreeBSD builds 2025-08-19 00:21:30 +02:00
PJB3005
6959f21927 Disable apphost when publishing client builds
Not used anyways.

Fixes FreeBSD builds.
2025-08-19 00:21:01 +02:00
PJB3005
26a1fb35b5 Fix zstd library load on Linux
Probably important
2025-08-18 23:53:28 +02:00
PJB3005
7fb3ce0e70 Guess we aren't having FreeBSD 2025-08-18 23:51:22 +02:00
PJB3005
5497b52100 Re-enable macOS CI
We should have the missing natives now

Fixes #5076
2025-08-18 22:56:42 +02:00
PJB3005
18b5f33080 Enable ARM64 RIDs for publish
Fixes #5830
2025-08-18 22:55:09 +02:00
PJB3005
30d3367c50 Enable SDL3 by default on ARM64
Enough to unblock releasing ARM64 engines
2025-08-18 22:53:27 +02:00
PJB3005
f6aabd1a22 Update NFluidsynth to 0.2.1
MacOS correct library name loading. Yay
2025-08-18 22:42:21 +02:00
PJB3005
6d229a3eb2 Use OpenAL Soft on macOS
Fixes #6148
2025-08-18 22:38:02 +02:00
PJB3005
da28bdbce5 Fix loading of SDL3 on Unix platforms
Didn't pass the assembly info so it wasn't using the proper resolution system.
2025-08-18 22:30:46 +02:00
PJB3005
0181988225 Update native dependencies
Holy shit
2025-08-18 22:20:17 +02:00
Quantum-cross
fb2ba7460a allow toolshed command spawn:in to work if the prototype doesn't have a PhysicsComponent (#6151) 2025-08-18 11:33:35 +02:00
Hannah Giovanna Dawson
b70d20a217 Update OpenTK and OpenTK.Audio.OpenAL to latest (#6107) 2025-08-18 11:32:19 +02:00
DrSmugleaf
ebc33df457 Make Toolshed ProtoId autocomplete use PrototypeIdsLimited instead of caching completions (#6146)
* Make Toolshed ProtoId autoclomplete use PrototypeIdsLimited instead of caching completions

* Add check for entity prototype
2025-08-17 16:46:20 +02:00
PJB3005
697af6771c Put ClientDllMap.cs behind #if fully 2025-08-17 16:39:35 +02:00
PJB3005
20706870da Add SDL3 to DLL map 2025-08-17 16:39:34 +02:00
PJB3005
372fa39228 Merge branch 'dont-skip-leg-day' 2025-08-17 16:27:20 +02:00
PJB3005
d6bfbe4f6f Disable ARM64 targets by default for now 2025-08-17 16:27:14 +02:00
PJB3005
54645b4adf Use fancy mac symbols for key names 2025-08-17 16:24:04 +02:00
PJB3005
7c16573f3e Force enable compat mode on Qualcomm Windows devices
Broken OpenGL driver.
2025-08-17 16:12:59 +02:00
PJB3005
8935b39987 Remove some unnecessary windows natives from client package
Saves like a megabyte. Oops.
2025-08-17 15:54:14 +02:00
PJB3005
388f8369a8 Trim sharpfont on publish
Saves like 100 KB. Wow.
2025-08-17 15:54:14 +02:00
PJB3005
217d889e36 Update to new SharpFont version 2025-08-17 15:54:13 +02:00
Pieter-Jan Briers
f243baccf2 Get CPU model on Linux ARM64
Uses /proc/cpuinfo
2025-08-16 14:22:46 +02:00
PJB3005
790f42ea70 Try to load zstd as libzstd.1.dylib on macOS
This is the correct name for the dynamic library.

We can make this change without breaking old engine versions, as the launcher overrides the import resolver for zstd.
2025-08-16 14:09:53 +02:00
PJB3005
a5fcf122b8 Unhardcode XAML hot reload marker sln
Was previously hardcoded to just "Space Station14.sln"

Co-authored-by: kaylie <moony@hellomouse.net>
2025-08-16 14:09:53 +02:00
PJB3005
df2d6ab8c2 Detect CPU model name on Windows ARM
Uses WMI query
2025-08-16 14:09:53 +02:00
PGray
23c90c0c45 Serialization: Make null literal check culture-invariant (#6136)
Use StringComparison.OrdinalIgnoreCase instead of ToLower() to avoid culture-sensitive casing issues (e.g., Turkish-i) when detecting YAML null literals.
2025-08-12 13:28:30 +02:00
PJB3005
c69756e7f1 Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day 2025-08-07 21:27:41 +02:00
PJB3005
07fbd5263c Run disconnect callbacks after removing channel from lists
Similar to the previous changes to player sessions, but now one layer lower.

Fixed ServerSendToAll from the relevant callbacks sending to a disconnected channel.
2025-08-07 00:44:06 +02:00
PJB3005
a1cdd60602 Version: 266.0.0 2025-08-06 16:14:11 +02:00
PJB3005
6fcaee91b6 Update release notes 2025-08-06 16:11:03 +02:00
slarticodefast
4d4f353680 Move ScaleVisuals to Content (and improve it) (#6096)
* împrove ScaleVisuals

* toolshedify

* fix

* rerun tests

* remove redundant code

* move to content
2025-08-06 14:33:35 +02:00
Hannah Giovanna Dawson
9f0dad80e4 Fix instrument pausing when outside PVS range (#6113) 2025-08-06 01:12:55 +02:00
Hannah Giovanna Dawson
c3f4b9bd67 Update MidiRenderer to use TryNoteOn and TryNoteOff (#6106)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-08-06 01:02:15 +02:00
PJB3005
641411288f Update NFluidsynth to 0.2.0 2025-08-06 00:48:40 +02:00
Hannah Giovanna Dawson
1fb7d3e723 Minimum MIDI note volume (#6127) 2025-08-05 23:35:48 +02:00
PJB3005
8cbc5d4cd8 Raise PlayerStatusChanged after removing disconnected players
This makes it so players aren't in the Sessions list anymore when their status is Disconnected.

Fixes SS14's lobby code sending lobby status updates to the just-disconnected player, which logs an error with the recent net message changes.
2025-08-05 17:31:29 +02:00
PJB3005
b4863dcc38 Properly stop sending messages to disconnected channels.
Log errors, and fix the ChannelClosedException it caused.
2025-08-05 17:12:43 +02:00
Tayrtahn
e771530de2 Mark AutoGenerateComponentStateAttribute fields as readonly (redo) (#6129)
* Mark AutoGenerateComponentStateAttribute fields as readonly

* Remove no-longer-valid test case
2025-08-05 00:48:36 -04:00
Tayrtahn
ce3a5f6bfa Revert "Mark AutoGenerateComponentStateAttribute fields as readonly (#6126)" (#6128)
This reverts commit 1cd802640a.
2025-08-04 18:32:54 -04:00
Tayrtahn
1cd802640a Mark AutoGenerateComponentStateAttribute fields as readonly (#6126) 2025-08-04 18:25:09 -04:00
Perry Fraser
1983734e2d feat: add analyzer for correct AfterAutoHandleStateEvent usage (#6117)
* feat: add analyzer for AfterAutoHandleStateEvent

* fix: correct TestOf attribute

Oopsieeeee.

Also weird newline plus unused import.

* Rerun content tests

* refactor: use ==, not .Contains

* feat: make AttributeHelper.HasAttribute looser

* refactor: use AttributeHelper.HasAttribute

* perf: cache AutoGenStateAttribute's type

* refactor: more pattern matching

ElementAtOrDefault with constant arg is bad; just use positional
matching.
2025-08-04 18:22:06 -04:00
PJB3005
ea380056b4 Make BaseWindow dragging use new cursor shapes
Technology.
2025-08-04 16:34:38 +02:00
PJB3005
9c26fba308 Add uitest tab for mouse cursor shapes 2025-08-04 16:31:51 +02:00
PJB3005
3de48d7595 Add more SDL3-exclusive mouse cursor shapes
They just fall back on GLFW.
2025-08-04 16:31:43 +02:00
pathetic meowmeow
7a510298e1 Add the ability to scale ItemList icons (#6125) 2025-08-04 10:34:46 +02:00
PJB3005
046db645e9 Update ImageSharp to shut up vulnerability warnings
It's just a DoS attack so nothing too major (for us) but still annoying.
2025-08-02 21:52:01 +02:00
Tayrtahn
63e383bb17 Fix NotYamlSerializable analyzer ignoring nullable structs (#5934)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-08-02 19:25:48 +02:00
Tayrtahn
e316649fd1 Add a Select button to ProtoId VV editor (#6097)
* Add a Select button to ProtoId VV editor

* Changelog

* Fix ftl string name

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-08-02 19:08:24 +02:00
Fildrance
121b58ee9a feat: added generic method for getting component from ComponentRegistry (#6082)
* feat: added generic method for getting component from ComponentRegistry

* refactor: corrected xml-doc

* refactor: moved emthod to ComponentRegistry

* Fix release notes entry.

Wording + it was in the template.

* Fix doc comments

* Do not use inappropriate fallible cast.

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-08-02 18:55:49 +02:00
PJB3005
d11f4bcc14 Fix release notes AGAIN 2025-08-02 18:53:29 +02:00
Fildrance
735ef09d42 Better unsubscription for multiple ConfigurationManager subscriptions (#6115)
* feat: new method or aggregating multiple config changed subscriptions into one disposable object or more slim unsubscribing code

* refactor: moved nested private class declaration to bottom of class

* refactor: reusing stateful object in tests is not smart

* fix: invalid code for forming new array during InvokeList.Remove call

* refactor: extracted new sub-multiple builder into configuration manager extensions

* refactor: remove unused code

* refactor: removed UnSubscribeActionsDelegates

* refactor: whitespaces and renaming

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-08-02 18:35:38 +02:00
Connor Huffine
772173cbaf Fix Non-solution Build: second attempt (#6098)
Downselect Robust.Client.Injectors to 'Debug' or 'Release' when built outside of solution context
2025-08-02 18:23:13 +02:00
PJB3005
4bd7aa16c1 Config no longer logs a warning when saved in integration test
Supersedes #6108

See https://github.com/space-wizards/space-station-14/issues/39196
2025-08-02 17:54:09 +02:00
pathetic meowmeow
bc4b4d3e6f Fix color naming crash (#6102) 2025-08-02 17:15:49 +02:00
Łukasz Mędrek
7d9a039252 add: Dictionary<T, TimeSpan> OnUnpaused generator (#6119)
* add: Dictionary<T, TimeSpan> OnUnpaused generator

* fix

* add: test

* Fix compiler warning from duplicate using

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-08-02 16:57:43 +02:00
Zeneganto
857f9a540b Add localization support for TileSpawnWindow (#6121) 2025-08-02 16:53:14 +02:00
Perry Fraser
6ae332d543 fix: use the actual top anchor in SetMarginsPreset (#6118) 2025-08-01 13:10:26 +02:00
slarticodefast
f4786f2d90 add QueueDeleteMap to SharedMapSystem (#6116) 2025-07-31 11:13:28 -04:00
Pieter-Jan Briers
dcbe0505dc Revert "Add WeakEntityReference (#5577)" (#6112)
This reverts commit c3489d4ded.
2025-07-29 18:22:17 +02:00
Leon Friedrich
c3489d4ded Add WeakEntityReference (#5577)
* Add WeakEntityReference

* Use NetEntity

* release notes

* A

* Fix merge conflicts

* comments

* A

* Add network serialization test

* Add ToPrettyString support for WeakEntityReference?

* inheritdoc

* Add GetWeakReference methods

* Not-nullable too

* Make EntitySystem proxy method signatures match EntityManager

* Add TryGetEntity

* interface

* fix test

* De-ref GetWeakReference methods

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
2025-07-29 11:12:49 -04:00
DrSmugleaf
8498634993 Add tests for immutable dicts and sets (#6109) 2025-07-29 01:17:29 +02:00
PJB3005
3d289fbd83 Update NetSerializer
Adds ImmutableDictionary and ImmutableHashSet serializers.
2025-07-29 01:17:19 +02:00
PJB3005
ebce0daa1b Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day 2025-07-28 20:54:36 +02:00
PJB3005
bbbfcca303 Fix RSI preloading with .rsic files 2025-07-28 18:50:20 +02:00
DrSmugleaf
e195ac4ce6 Add ImmutableArrayExtensions All to sandbox.yml (#6110) 2025-07-28 16:05:05 +02:00
PJB3005
dc5cbd085b Enable RSI packing pass in RobustClientAssetGraph 2025-07-26 02:08:34 +02:00
PJB3005
c4dff678a9 Make .rsic packing in asset packaging work
Finishing what I started a couple years ago, the packaging system now packages .rsi files into single .rsic files. This means a single .rsi "file" (1 + N files) becomes a single file when packaged.

This should improve performance on game startup, downloading, etc etc. The total file count for SS14 goes down from 30,000 to 6,000 (with the previous change for merging text files too).

Mostly just involved shuffling a bunch of the RSI loading code around so that it can be re-used for this purpose nicely. The original prototype in the code was copy-pasted, which obviously couldn't be relied upon.

This does mean that if you're loading an RSI's interior PNG directly via a texture path, that PNG will now be unavailable on packaged builds. To avoid this, you can set "rsic": false in the meta.json, so that it gets left alone by the pass.
2025-07-26 01:51:17 +02:00
PJB3005
cd9616c87c Add new text file merge asset pass to RobustClientAssetGraph 2025-07-25 15:57:47 +02:00
PJB3005
d1c6c11755 Add asset pass to merge text files in directories.
This massively reduces the file count of published SS14 builds by a few thousand, by combining YAML prototypes and Fluent files in the same folder into one file.
2025-07-25 15:57:18 +02:00
PJB3005
1ebac7c894 Make prototype load ignore documents with empty values
This happens if you have a YAML file like this:

---
# commented prototype
---
# Real prototype
- type: bla

This case is generated by my (next commit) prototype file merger asset pass, and I don't see any harm in just skipping in this case.

Also improve the logging in general.
2025-07-25 15:54:58 +02:00
PJB3005
6b41be8901 Make AssetPassPackRsis not crap out due to ImageSharp errors.
Still just for testing.
2025-07-25 15:50:21 +02:00
PJB3005
51c929c8ec Version: 265.0.0 2025-07-23 01:47:41 +02:00
PJB3005
4863b09f0a Update release notes 2025-07-23 01:47:16 +02:00
Pieter-Jan Briers
cdd3afaa4c Remove redundant custom math types (#6078)
Vector3, Vector4, Matrix4, and Quaternion are now gone. Use System.Numerics instead.

This commit is just replacing usages, cleaning up using declarations, and moving over the (couple) helpers that are actually important.
2025-07-23 01:15:27 +02:00
slarticodefast
fee67b648c Allow AutoNetworkedField to work with inherited datafields (#6090)
* allow AutoNetworkedField to work for inherited datafields

* fix

* test fix

* typo

* Update Robust.UnitTesting/Shared/GameState/AutoNetworkingTest.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-07-23 01:15:12 +02:00
Perry Fraser
0bf4123b8d feat: Add VV editor for tuples (#6065)
* feat: Add VV editor for tuples

* refactor: make tuple editor work in more cases

* feat: support other arity tuples

* fix: correct release notes entry

* refactor: use a new index selector for tuples

Also yank out silly unused code.

* fix: make all non-ValueTuples readonly

* refactor: spell out ValueTuple arities

,,,,,,,,,,,,,,,,,,,,,
2025-07-22 22:31:19 +02:00
Tayrtahn
9ea51432d1 Un-hardcode EntitySpawnWindow's placement mode dropdown (#5994)
* Find PlacementModes by attribute

* Let modes specify priority

* Make some PlacementManager dependencies public so Content can use them

* Space out the priorities a bit more

* xmldoc for attribute

* Revert "xmldoc for attribute"

This reverts commit f1f0299c55.

* Revert "Space out the priorities a bit more"

This reverts commit 549eac1eb2.

* Revert "Make some PlacementManager dependencies public so Content can use them"

This reverts commit c060f6cb2d.

* Revert "Let modes specify priority"

This reverts commit f113b40c7f.

* Revert "Find PlacementModes by attribute"

This reverts commit 27efb6c5cf.

* Completely redo to use PlacementManager's mode dictionary

* Backwards compat

* Cache the value of AllModeNames
2025-07-22 22:14:53 +02:00
Perry Fraser
c8da6f30a3 fix: resolve remaining ResolvedSoundSpecifier warnings (#6057)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-07-22 22:08:31 +02:00
Tayrtahn
974c1e827d Change a bunch of static Loc.GetString calls to be properly resolved (#6010)
* Add Loc shortcut to LocalizedCommands

* Fix static methods in commands

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-07-22 20:33:43 +02:00
Tayrtahn
3c48b24539 Cleanup prototype instantiation warnings in unit tests (#6058) 2025-07-22 20:32:49 +02:00
Fildrance
d8aefe5118 feat: now view-variable controls can be registered from content, or even dynamically added (#6077)
* feat: now view-variable controls can be registered from content, or even dynamically added

* refactor: whitespaces and xml-doc

* refactor: added changelog entry

* refactor: added methods for adding condition at start and at the end

* refactor: merged start/end methods, for IViewVariableControlFactory, improved changelog message

* refactor: replaced bool insertLast with InsertPosition enum

* refactor: reverse order of checks registration in ViewVariableControlFactory c-tor

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-07-22 19:48:12 +02:00
Tayrtahn
6d9a4719a9 Validate VV EntProtoId values (#6095) 2025-07-22 19:30:01 +02:00
Pieter-Jan Briers
893173ab17 Add workflow to build all configurations. (#6094)
* Add workflow to build all configurations.

Builds Debug, Tools, Release against Linux, Windows and MacOS TargetOS.

See https://github.com/space-wizards/RobustToolbox/pull/6069#issuecomment-3050607114

* Very epic, GitHub

* Whoops

* Actually add Tools configuration to sln
2025-07-22 19:21:21 +02:00
PJB3005
7c0f1b8031 Fix unused dependency warning outside DEBUG
Supersedes #6069
2025-07-22 18:10:49 +02:00
Tayrtahn
bb57f82811 More informative logging for PVS deleted/uninitialized entity errors (#6084) 2025-07-22 12:33:18 +02:00
Perry Fraser
0fc9b0acd0 Lint for prototype IDs with spaces (#6087)
* feat: lint for prototype IDs with spaces

* feat: also disallow periods

* Update RELEASE-NOTES.md

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2025-07-22 12:16:57 +02:00
Hannah Giovanna Dawson
f2b7f0d8d2 NoteOn actually being a NoteOff fix (#6092) 2025-07-22 12:10:58 +02:00
PJB3005
0ec189dece IPrototypeManager TryIndex changes
This effectively gracefully reverts 94f98073b0.

IPrototypeManager.TryIndex now no longer logs an error. This is done by adding a new overload without the logError parameter, so most existing code switches to it. The overload with the logError parameter is now obsolete.

As a replacement for defensive programming situations, the new Resolve() should be used instead.

IPrototypeManager.TryIndex() should not be used for handling IDs that should always be valid, only for handling user input and similar.

I also added a lot of docs.
2025-07-22 00:11:59 +02:00
PJB3005
c876eb1f4c Fix TextInputSetRect not accounting for pixel ratio properly.
Fixes it being positioned wrong on macOS.
2025-07-20 19:51:09 +02:00
PJB3005
1037fc735e Make SDL3 file dialogs have parent window.
Somehow needed to avoid causing it to block on macOS.
2025-07-19 18:45:14 +02:00
PJB3005
d5df765467 Package FreeBSD by default.
We won't officially support FreeBSD launcher builds, but this at least allows third-party launcher builds to have an engine to load properly.
2025-07-19 18:21:51 +02:00
PJB3005
93cf9f4227 Disable threaded window blit on macOS
Can probably do this on Linux too, but I didn't test that.

This feature is, fundamentally, a workaround to avoid WGL MakeCurrent() constantly breaking. The extra threading complexity is not a good thing on other platforms, so get rid of it.
2025-07-19 13:22:27 +02:00
PJB3005
d2977e2a63 Fix secondary window closing breaking rendering on macOS 2025-07-19 02:04:46 +02:00
PJB3005
a3f0ea19c4 Avoid WinBlit threads getting stuck forever when their window closes. 2025-07-19 00:42:31 +02:00
PJB3005
d9032b8757 Move some swapping code behind #ifdef
idk I just did this while debugging something and there's no harm committing it.
2025-07-19 00:42:00 +02:00
PJB3005
cba6e37f9f Fix SDL multiwindow freezing in some cases on macOS
Need SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH set, apparently.
2025-07-19 00:41:27 +02:00
PJB3005
90ec9a80c9 Fix publishing script not passing TargetOS properly 2025-07-16 22:04:15 +02:00
PJB3005
7eaf2f590b Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day 2025-07-15 15:23:23 +02:00
PJB3005
0439ea9893 Update packaging script to support ARM64 properly. 2025-07-15 15:14:40 +02:00
PJB3005
74aa8fa9ed Fix ParallelManager cutting off exception info
0% tested
2025-07-12 23:06:35 +02:00
Nemanja
ceeb002692 Mark ValidatePrototypeIdAttribute as obsolete (#6062) 2025-07-12 15:20:08 +02:00
portfiend
78d807b13c Refactor ColorSelectorSliders.cs, fix color slider event stack overflow (#6072)
* add: IColorSelectorStrategy class

defines some common variables and functions that depend on the slider type
my hope is to kill all the switch statements in here

* add: IColorSelectorStrategy FromColorData method

* add: RGB and HSV color slider strategies

* add: initialize ColorSelectorStrategy

* refactor: rename IColorSelectorStrategy to IColorSliderStrategy

this makes more sense i think

* refactor: nuke switch statements, use strategy in colorselectorsliders

* remove: remove GetSliderLabels in favor of strategy

* refactor: better abstraction for slider InputBox.ValueChanged

* refactor: rename OnColorSet to OnSliderValueChanged
more intuitive

* refactor: turn alpha slider max value into a const
no magic numbers

* tweak: make color sliders update channels individually

* fix: add braces around this callback

* tweak: move some variables around
i realize there's an Order to this so

* add: throw error if UpdateSlider is called with invalid value

* add: documentation comments to ColorSelectorSliders

* refactor: simplify UpdateSlider

* refactor: simplify GetColorValueDivisor

* fix: solved the color slider stack overflow

* fix: ensure _strategy is set before other functions use it

* tweak: rename Update to UpdateAllSliders
clearer

* fix: update slider colors on update
accidentally removed it and forgot to put it back

* remove: redundant comment
false alarm

* fix: prevent inputbox infinite event loop
this was also erroneously changing the color whenever the slider type changed

* fix: reviews part 1
- changed ColorSliderStrategy into abstract class
- fixed "strategy" typo
- changed NotImplementedException into ArgumentOutOfRangeException

* fix: make selector strategy static instances
2025-07-12 14:51:02 +02:00
PJB3005
5cd4c187bf Fix VV member group headers 2025-07-12 02:22:04 +02:00
Perry Fraser
fec477bf41 fix: remove unneeded delta in SharedAudioSystem (#6055) 2025-07-12 01:56:22 +02:00
PJB3005
9d00b1f093 Make VV KeyValuePair prop editor have a minimum width
Avoid buttons for references being squashed to zero width.
2025-07-12 01:47:49 +02:00
PJB3005
de0871d17b Fix VV handling of remote KVPair types
There was a bunch of complex code to analyze the full type string the server sent, except I have no idea what use this was. It's both incorrect (the type string isn't guaranteed to work if the remote .NET version is different) and unnecessary as PropertyFor already handles all the cases.
2025-07-12 01:47:49 +02:00
Perry Fraser
053c469cac fix: loosen random timespan debug assert (#6064)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2025-07-11 21:36:13 +02:00
Leon Friedrich
efa8975bc6 Fix pool manager conflicts (#6075) 2025-07-11 20:55:49 +02:00
Errant
4851e913b0 More TimespanSerializer improvements (#5910)
* improved public TryTimeSpan

* don't want any locale shenanigans or misconceptions with the input

* missed a test line

* also support capitalized time unit indicators

* Doesn't need to be nullable.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-07-11 20:37:39 +02:00
PJB3005
74a318c521 Allow content to skip certain paths in client/server resource copying
Intended so content can ignore the MapImages folder
2025-07-11 18:15:10 +02:00
pathetic meowmeow
e52a6bbbf2 Add OKLCH-based colour descriptions for colorblindness accessibility (#6067)
* Add OKLCH-based colour descriptions for colorblindness accessibility

* my docs so comment

* my feed so back
2025-07-11 15:40:41 +02:00
PJB3005
e169d6a5a2 Enforce integration instance idleness for more helper members
Also allow them to be accessed regardless if from the integration instance thread.
2025-07-10 16:29:16 +02:00
PJB3005
3634ee636b Pooled integration instances now get marked non-idle
Otherwise, pooled integration instances could behave differently from freshly-spawned ones, creating heisentests.
2025-07-10 16:26:56 +02:00
āda
2349728eab out of my element (#6074)
Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-07-10 12:43:51 +02:00
PJB3005
777f02cadd Fix physics closure allocs and some avoidable struct copies 2025-07-10 12:33:17 +02:00
Myra
0fc6f2bce6 Config load fail is now an error instead of a warning (#6070)
* Config load fail is now an error instead of a warning

* Update RELEASE-NOTES.md

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-07-06 20:28:49 +02:00
Tayrtahn
dc3705e520 Add ForbidLiteral to IPrototypeManager methods (#6066)
* Add ForbidLiteral to IPrototypeManager methods

* Cleanup violations
2025-07-06 20:27:32 +02:00
Tayrtahn
01f71ca55a Remove prototype instantiation from AssetPassAudioMetadata (#6059) 2025-06-28 22:18:46 +02:00
PJB3005
c5e812836b Fix loading textures in root folder
Fixes #6052

Also clean up a warning while I'm at it.
2025-06-28 01:37:14 +02:00
PJB3005
56eda3ea92 Version: 264.0.0 2025-06-27 22:03:33 +02:00
Tayrtahn
9dffd36319 Use non-generic TryComp to get MetaDataComponent in DebugAnchoringSystem (#6051) 2025-06-27 20:38:10 +02:00
Tayrtahn
a45b72a1c5 IRobustCloneable and generator support (#5692)
* Add IRobustCloneable and check for it in compnet generator.

* Redo compnetgenerator support; add test

* Disconnect client at end of test

* Actually test for client entities

* Cleanup

* Cleanup 2
2025-06-27 20:37:43 +02:00
Perry Fraser
bd0579ed6d fix: apply scale when calculating sprite bounding box (#6046)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2025-06-26 23:23:57 +02:00
Pieter-Jan Briers
c73b54862e Add analyzers to detect some prototype misuse (#6048)
* Add analyzers to detect some prototype misuse

Detects people marking prototype as NetSerializable.

Detects people creating new prototype instances themselves.

* Update Robust.Analyzers/PrototypeNetSerializableAnalyzer.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
2025-06-26 22:24:23 +02:00
PJB3005
6436ff8040 Fix prototype manager Index exceptions
Index<T> was documented to throw KeyNotFoundException, but actually threw UnknownPrototypeException. Index(Type type, string id) threw KeyNotFoundException.

This has now been made consistent to be UnknownPrototypeException everywhere.
2025-06-26 16:58:35 +02:00
PJB3005
98313ae369 Update NetSerializer submodule
Makes it report where broken serialization types come from.
2025-06-26 16:58:21 +02:00
PJB3005
0e63391203 Add PrototypeManagerExt.Index that takes nullable ProtoId<T> 2025-06-26 16:52:18 +02:00
wixoa
261bfaeeb8 Add AlwaysActive to WebViewControl (#6047) 2025-06-25 21:50:07 +02:00
Tayrtahn
4017e1f57e Make some PlacementManager dependency fields public (#6044)
* Make some PlacementManager dependency fields public

* Revert "Make some PlacementManager dependency fields public"

This reverts commit 99fe37b502.

* Now part of IPlacementManager
2025-06-23 22:48:25 +02:00
lzk
e170bf1ad2 genetive case (#6045)
* dative

* slipped it

* slipped it twice

* 1

* Update _engine_lib.ftl
2025-06-23 22:47:50 +02:00
PJB3005
da0abd2535 Make functions static to avoid delegate allocations in DataDefinitionAnalyzer. 2025-06-22 13:48:50 +02:00
PJB3005
f9d0dd551a Version: 263.0.0 2025-06-22 13:36:07 +02:00
Aiden
b2540a6e08 Static Field Assert (#5926)
* Static Field Assert

* Update Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

---------

Co-authored-by: GoobBot <uristmchands@proton.me>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-06-21 23:51:28 +02:00
DrSmugleaf
66d898ee91 Add GetMessage and SetMessage methods to OutputPanel (#5956)
* Add GetMessage and SetMessage methods to OutputPanel

* Copy paste bad

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-06-21 23:33:03 +02:00
ThereDrD
310dc676ea fix: use maxSizeX instead of Width in rich text entry measure (#5989)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-06-21 15:16:38 +02:00
Perry Fraser
41844d2d30 Adjust how OpenAL extensions are requested (#6000)
* fix: use correct device in OAL extension lookup

* fix: don't try to set non-existent window icons

* Revert "fix: don't try to set non-existent window icons"

This reverts commit 793958fb8c.

Moving to other PR.
2025-06-21 15:16:02 +02:00
slarticodefast
c6f3af20d6 fully obsolete container methods (#6007) 2025-06-21 14:42:57 +02:00
Pieter-Jan Briers
5501209b35 Add API to load maps from byte stream (#6029)
In case you don't want to load from a ResPath.
2025-06-21 14:41:32 +02:00
Tayrtahn
9b2ef75762 Optimize DataDefinitionAnalyzer a bit (#6035)
* Skip fields/properties not in DataDefs

* Only check IsDataField once per field/property

* Remove pointless foreach loop

* Remove an extra IsDataDefinition check

* Revert unneeded changes from testing

* Revert "Remove pointless foreach loop"

This reverts commit f05d566904.

* Restore analysis of multiple declarations
2025-06-21 01:15:59 +02:00
Tayrtahn
196e59b7e4 Clean up all missing EntitySystem proxy method uses (#6027)
* Clean up all missing EntitySystem proxy method uses

* Restore comment

* Fix bad change that caused closure allocation

* tuple

* Revert "tuple"

This reverts commit 14581a40aa.

* Revert "Fix bad change that caused closure allocation"

This reverts commit 215b2559ed.

* Revert "Restore comment"

This reverts commit 4a47a36557.

* Revert "Clean up all missing EntitySystem proxy method uses"

This reverts commit 3b1fe4ce7f.

* Redo with improved code fixer.
Let's see how it fares this time
2025-06-21 00:05:09 +02:00
Perry Fraser
2c936b5973 fix: don't delete people who are teleported to themselves (#6040) 2025-06-20 14:00:27 +02:00
Tayrtahn
7765e71dca Add tests for remaining DataDefinitionAnalyzer diagnostics (#6034)
* Add tests for partial datadefs and partial nested datadefs

* Add test for redundant datafield tag

* Blank lines upset me
2025-06-20 02:26:08 +02:00
TrixxedHeart
d8ae71d8cd adds typeselector (thanks pbj) (#6038) 2025-06-20 02:24:03 +02:00
Tayrtahn
a74812ce5b Make AddComp where clause consistent with AddComponent (#6028) 2025-06-19 10:21:13 +10:00
PJB3005
a7f9b0a6db Fix debug assert when loading MIDI on Windows.
Fixes #6020

The assert was caused by the native OS path (C:\Windows\...) being passed through a ResPath. Bad. While looking at this I realized the sound font loader callback system was a mess and I should probably clean it up, so I did.

The file name is now properly namespaced in the loader callback, which should avoid spaghetti like this in the future. The details of how this works are a pain in the ass because Fluidsynth isn't well-designed.

I split LoadSoundfont() into two functions: one for resource, one for user paths. The other is kept there but compatible.

I can't believe I spent 3 hours on dealing with this nonsense and most of it is just due to Fluidsynth being poorly designed...
2025-06-18 03:25:58 +02:00
Amy
3aac92e4b2 soft only plz (#6030) 2025-06-17 18:56:44 +02:00
PJB3005
c152fb8953 Fix culture-based parsing in TimespanSerializer 2025-06-17 16:03:20 +02:00
DrSmugleaf
10ea5498cf Fix error in some localization functions when an argument is not an EntityUid (#6022) 2025-06-15 14:07:13 +02:00
Walker Fowlkes
324606e5a3 Add a new toolshed command spawn:in (#6021)
* added spawn:in command.

* better annotations

* use EntityManager.System

* ftl

* make it lazy cached.

* Fix typo (it's -> its)

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-06-15 14:05:27 +02:00
Tayrtahn
a8227f7faa Replace static logger call in FileDialogManager (#6018) 2025-06-13 23:20:23 +02:00
PJB3005
9f55400c58 Avoid closure allocation in physics SolveIsland()
There's a parallel call in there that's only used when the island should be processed parallel internally. This isn't done for all islands, so allocating the closure in every case is a massive waste.
2025-06-13 15:00:47 +02:00
PJB3005
8b971f7ae7 Add CompletionHelper.PrototypeIdsLimited API
Somebody ignored the doc comment saying "don't use this with EntityPrototype" so now just *typing* a Tippy command causes the server to lag. Great.

This still isn't too great for performance but at least it's better, and I don't want to commit to making PrototypeManager semi-thread-safe.
2025-06-13 00:15:17 +02:00
PJB3005
e3c7e361ae Avoid server stutters from scsi init
Task.Run go brr
2025-06-12 23:33:48 +02:00
Tayrtahn
5c48dcb211 Fix TabContainer.CurrentTab setter (#6017) 2025-06-12 00:31:03 +02:00
B_Kirill
694de028c2 AudioSystem logging extension (#5959)
* AudioSystem logging extension

* Redo

* Fix

* review
2025-06-11 02:17:49 +02:00
PJB3005
d41c9e7662 Properly catch errors when executing client commands.
Previously these errors propagated all the way into Clyde. Guh.

Probably still need more error handling around the input system, but this is important regardless.
2025-06-11 02:11:32 +02:00
B3CKDOOR
76134e0f8d Adding "Attribution-NonCommercial-NoDerivatives 4.0 International" (#6008)
* Adding "Attribution-NonCommercial-NoDerivatives 4.0 International"

Adding the "Attribution-NonCommercial-NoDerivatives 4.0 International" License type, this is getting marked as an "invalid" license when its actually a valid license.

[License link](https://creativecommons.org/licenses/by-nc-nd/4.0/)

* Darn, forgot a comma
2025-06-09 20:57:54 +02:00
Perry Fraser
2983517e43 fix: don't try to set non-existent window icons (#6016) 2025-06-09 20:56:01 +02:00
metalgearsloth
18849be0b4 Version: 262.0.0 2025-06-09 23:56:58 +10:00
Leon Friedrich
c6a1d82bb1 Validate that Toolshed command arguments have parsers (#6014)
* Add Nullable<T> support to ToolshedManager.TryParse

* Check that command arguments are parseable

* release notes

* a

* A is for Array

* Fix test

* Fix indentation
2025-06-09 23:47:29 +10:00
Leon Friedrich
d89e1a43c6 Add PvsResetTest (#6015) 2025-06-09 23:39:06 +10:00
Leon Friedrich
d894ef70ef Misc SpriteSystem fixes (#6001)
* Move GetPrototypeTextures to SpriteSystem

* Fix tests

* Fix #6002

* release notes

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-06-09 18:59:37 +10:00
DrSmugleaf
c7ea2793ca Fix TransformComponent state handling changing the coordinates of detached entities (#6006)
* Fix TransformComponent state handling changing the coordinates of detached entities

* Make ResetPredictedEntities not handle state for detached entities
2025-06-09 17:44:36 +10:00
Perry Fraser
0c61ff2bee fix: default audio params for PlayStatic (#6011) 2025-06-09 00:28:12 +02:00
Tayrtahn
343a34eac7 Fix warning CS0168 in EntityDeserializer (#6013)
* Fix warning CS0168 in EntityDeserializer

* Actually, why use try-catch just to rethrow anyway?
2025-06-09 00:04:03 +02:00
metalgearsloth
7be41f4890 Add ignoredcomps to IsDefault (#5998) 2025-06-05 23:54:47 +10:00
metalgearsloth
293470a5fe Fix incorrect saved window positions (#5927)
* Fix incorrect saved window positions

As of however many UI PRs ago windows store their last position on the client and it re-opens windows at that position.

The issue is that the code to avoid windows being able to go off-screen was immediately bulldozing this value, at least if the x <= 0. Now we just don't run it until we have a valid measure (probably the frame after) and avoid unnecessarily having an incorrect position applied.

* Explainer
2025-06-05 23:35:28 +10:00
metalgearsloth
2b8057acf0 Version: 261.2.0 2025-06-05 22:55:09 +10:00
Tayrtahn
bec3caa5da Fix error when using tpto on a grid (#5991)
* Fix error when using tpto on a grid

* Calculate map coords outside of loop
2025-06-05 22:45:30 +10:00
metalgearsloth
ea6126563b Add NearestChunkEnumerator (#5972)
Not super fast but want it for biome loading to prio chunks nearby first.
2025-06-05 22:36:22 +10:00
slarticodefast
00494ad9eb fix TryQueueDelete (#5996) 2025-06-05 22:34:50 +10:00
Tayrtahn
6672b7b1bd Correct misleading error message in ShareMapSystem.OnParentChange (#5992) 2025-06-05 22:29:47 +10:00
ruddygreat
8dc55e8748 fix the lifestage checks on predicted entity deletion (#5993)
Co-authored-by: Ruddygreat <ruddygreat1@gmail.com>
2025-06-05 22:29:22 +10:00
Tayrtahn
44ea2cd396 Implement IEquatable for ResolvedPathSpecifier and ResolvedCollectionSpecifier (#5980) 2025-06-01 18:10:55 +10:00
metalgearsloth
2c5604432b Update some GetComponentName usages (#5942)
Rider tells me to use generic and generic one seems better.
2025-06-01 17:54:43 +10:00
Tayrtahn
c696466522 Remove ITileDefinition.ID (#5982) 2025-06-01 17:51:59 +10:00
slarticodefast
01bb98e400 fix static grid center of mass (#5985) 2025-05-29 23:05:54 +10:00
metalgearsloth
af08e747de Defer grid state handling TileChangedEvent (#5981)
Rather than doing the old raise-event-per-tile we just raise it at the end.
2025-05-29 09:21:45 +10:00
metalgearsloth
8c35c2c380 Version: 261.1.0 2025-05-29 00:15:26 +10:00
Tayrtahn
6d46d3f4a5 Cleanup most warnings in unit tests (#5946)
* 2 warnings in JointDeletion_Test

* 1 warning in Collision_Test

* 2 warnings in Color_Test (deleted test of deprecated HCY color space)

* 1 warning in MapVelocity_Test

* 2 warnings in MapManager_Tests

* 2 warnings in MapPauseTests

* 1 warning in NetDisconnectMessageTest

* 1 warning in ContainerTests

* Suppress 1 warning in EntityEventBusTests.ComponentEvent

* 4 warnings in MapGridMap_Tests

* 1 warning in GridDeletion_Test

* Remove TryGetContainingContainer foolishness

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-05-29 00:12:58 +10:00
Tayrtahn
50e06e43fa Automatic Sawmill generation for UIControllers (#5967) 2025-05-29 00:06:55 +10:00
metalgearsloth
986b0f979d Fix physics forces not autoclearing (#5978)
* Fix physics forces not autoclearing

* Changes
2025-05-29 00:04:55 +10:00
metalgearsloth
a51d786dee Version: 261.0.0 2025-05-28 19:34:47 +10:00
metalgearsloth
5f5fed5d6c Update contact xform usage (#5977)
Forgot this one as well.
2025-05-28 19:30:50 +10:00
Tayrtahn
e475cc7898 Cleanup warning in SpriteBoundsOverlay (#5944)
* Cleanup warning in SpriteBoundsOverlay

* Make better use of the primary constructor
2025-05-28 19:28:53 +10:00
metalgearsloth
ee8ea4ec3b Purge PhysicsMapComponent (#5766)
* Replace PhysicsMapComponent

- Dumb idea
- Lots of book-keeping and perf overhead.
- Much saner this way.

* stuff

* More work

* Purge

* Fixes

* Eh?

* Fixes

* Also this

* weh

* Fixes

* ice-cream

* Fix

* Fix stacking / gravity

* Gravity query

* MoveBuffer optimisations

* Fixes for test

* World gravity

* Fix build

* Avoid some transform resolves for contactless ents

* Less getcomps

* Fix contact caching

* Possibly less copies

* reh

* bulldoze

* Test "fix"

* seikrets

* a

* I saw this but now I decideded against it

* true
2025-05-28 19:18:36 +10:00
slarticodefast
7482451ec4 optimize ToMapCoordinates (#5953) 2025-05-28 12:07:23 +10:00
metalgearsloth
dddf5cd2fb Add entities to SpawnEntitiesAttachedTo (#5971)
* Add entities to SpawnEntitiesAttachedTo

Need it for biome stuff.

* factorio
2025-05-27 19:45:48 +10:00
metalgearsloth
01979c451d Make RaiseMoveEvent internal (#5918)
I don't think content should really be calling this tbh.
2025-05-27 19:45:04 +10:00
slarticodefast
181a5ef0b4 fix GetMapLinearVelocity (#5950)
* fix GetMapLinearVelocity

* resolve and adjust other methods
2025-05-27 19:41:43 +10:00
Princess Cheeseballs
e7c7011cc0 Init Commit (#5909) 2025-05-27 19:34:32 +10:00
metalgearsloth
dc97615fd4 Add some Box2i methods (#5969)
Equivalent to Box2.
2025-05-27 19:26:50 +10:00
metalgearsloth
3b4944376b Fix FastNoiseLite fractal bounding (#5970)
This shouldn't be datafielded because it gets set by other datafields.
2025-05-27 19:14:49 +10:00
Tayrtahn
fa6bd8f7ba Cleanup TypeSerializer Logger warnings (#5966) 2025-05-24 20:23:28 +02:00
Whatstone
2398cbcf26 GameController: init LocMgr before init broadcast (#5965) 2025-05-24 19:02:26 +02:00
Tayrtahn
38ce48a83f Cleanup 3 warnings in SharedContainerSystem (#5949)
* Cleanup 3 warnings in SharedContainerSystem

* Don't call Transform twice
2025-05-24 19:00:57 +02:00
PJB3005
4e7de2f272 File dialog fixes and improvements
File dialog requests can now specify the share and access mode they want out of the opened file. This means read-only access is now possible.

While doing this I noticed that the SDL3 backend had a memory leak *and* didn't match the behavior of the other backends. Cleaned up the code to avoid that.

In-engine commands that *can* specify read-only access on file open now do.
2025-05-24 16:38:01 +02:00
Cami
b61075c660 Stopped recursive updates for controls that are not visible, as it vi… (#5960)
* Stopped recursive updates for controls that are not visible, as it violates framerate in large menus.

* Update Robust.Client/UserInterface/Control.cs

---------

Co-authored-by: Cam <Nop>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-05-23 17:42:47 +02:00
Tayrtahn
7b571dc80e Fix 2 instances of warning CS0162 (#5951) 2025-05-23 17:37:08 +02:00
TemporalOroboros
f1c76ca899 Remove unused obsolete TryGetContainingContainer override (#5660)
* Remove unused obsolete TryGetContainingContainer override

* poke tests

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-05-23 17:36:38 +02:00
metalgearsloth
84dcd658aa Version: 260.2.0 2025-05-21 23:30:58 +10:00
metalgearsloth
a634d6bd04 Add WorldNormal to StartCollideEvent (#5954)
We already have the value just a matter of adding it to the event.
2025-05-21 20:41:57 +10:00
DrSmugleaf
36f9df3079 Add System.Text.StringBuilder Insert(int, string) to sandbox.yml (#5955) 2025-05-21 11:20:10 +02:00
keronshb
824c018a69 Version: 260.1.0 2025-05-19 13:11:32 -04:00
Tayrtahn
4b6b688c72 Cleanup warnings in PlacementManager (#5939) 2025-05-18 19:14:16 +10:00
Tayrtahn
71df25b251 Cleanup warning in Clyde.Sprite (#5940) 2025-05-18 18:51:27 +10:00
metalgearsloth
be14a3c249 Expose CompFactory to systems (#5941) 2025-05-18 00:56:09 -04:00
metalgearsloth
3c2a4d5c79 Version: 260.0.0 2025-05-18 03:07:24 +10:00
metalgearsloth
44180b3ee0 Fix / remove startcollidevent worldpoint (#5936)
Now it's worldpoints because it may not necessarily be 1 pointr and internally we fix the actual points themselves.
2025-05-18 03:03:12 +10:00
metalgearsloth
bb0e77e937 Add some EntProtoId overloads (#5938)
Need it for some content stuff didn't feel like doing the rest yet.
2025-05-17 18:28:12 +10:00
ArtisticRoomba
684b9bc852 Add new Vertical property to progress bars (#5932) 2025-05-17 18:27:44 +10:00
Tayrtahn
9f3db6693e Add SpriteSystem dependency to VisualizerSystem (#5935)
* Add protected SpriteSystem reference to VisualizerSystem

* Capital S
2025-05-17 13:26:40 +10:00
metalgearsloth
40d869948d Version: 259.0.0 2025-05-15 20:26:10 +10:00
Tayrtahn
5c97b15849 Mark Entity methods as readonly (#5919)
* Mark Entity methods as readonly

* Add to GenericEntityPrint

* No but really
2025-05-15 20:23:29 +10:00
Tayrtahn
3d8a9a41fa Combine TileChangedEvents in SetTiles (#5912)
* Combine TileChangedEvents in SetTiles

* Raise event after regenerating collision

* continue, not return

* No need for GetComponent

* Swap TileRef for Tile + Vector2i

* Estimate size of tileChanges
2025-05-15 20:22:05 +10:00
metalgearsloth
92fc8722da Version: 258.0.1 2025-05-15 19:28:25 +10:00
metalgearsloth
73f6555624 Fix static ent collision spawn (#5933)
* Fix static ent collision spawn

* Fix test

* cool
2025-05-15 19:11:20 +10:00
metalgearsloth
2ac7bc3ce4 Version: 258.0.0 2025-05-15 00:51:12 +10:00
Leon Friedrich
05cb4bb1c9 Make SpriteSystem.LayerMapReserve not throw (#5930)
* Make SpriteSystem.LayerMapReserve not throw

* fix SpriteComponent.Visible

* remove region
2025-05-14 23:23:51 +10:00
Leon Friedrich
a393efc87a Modify markup tag interfaces and fix some bugs (#5442)
* Modify markup tag interfaces

* Why are nullable structs like this.

* AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

* Avoid breaking changes

* Replace IMarkupTag with IMarkupTagHandler in engine

* Its a breaking change now I guess

* cleanup
2025-05-12 14:09:18 +10:00
Leon Friedrich
4d47cfa1a6 Minor respath improvements (#5876)
* Minor respath improvements

* Add helpers

* tweak helper

* Throw on more than 1 char

* comments

* No emoji separators
2025-05-12 13:04:40 +10:00
DrSmugleaf
2b1d755d9f Fix Container state handling not forcing inserts (#5916) 2025-05-11 22:45:31 +10:00
ElectroJr
db7de0a99f Version: 257.0.2 2025-05-11 23:47:14 +12:00
Leon Friedrich
47f18703af Fix unshaded sprite layers (#5924)
* Fix unshaded sprite layers

* update comment
2025-05-11 21:42:40 +10:00
Leon Friedrich
97c1548301 Add SpriteBoundsTest (#5922) 2025-05-11 15:30:14 +10:00
ElectroJr
cd97f1583f Version: 257.0.1 2025-05-11 13:56:27 +12:00
Leon Friedrich
5fbe25ec9d Fix sprite layer bounds (#5920) 2025-05-11 11:52:20 +10:00
metalgearsloth
516ee47b51 Version: 257.0.0 2025-05-10 22:12:35 +10:00
metalgearsloth
89be682e24 Don't raise wake events for terminating contacts (#5757) 2025-05-10 22:02:22 +10:00
metalgearsloth
6086076559 Avoid checking grid traversal for rotation events (#5778)
* Avoid checking grid traversal for rotation events

* Also this one
2025-05-10 22:01:43 +10:00
beck-thompson
5bd90c908a Optimization RSI preloading / atlas creation (#5817)
* First commit

* Fix multiatlas bug

* Use ValueList instead

* Add FFDH sorting for atlases

* Minor cleanup and value lists
2025-05-10 22:00:03 +10:00
Leon Friedrich
a3d0921cc9 Pause entities that leave PVS range (#5878)
* Pause entities that leave PVS range

* Fix merge

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-05-10 21:57:04 +10:00
Leon Friedrich
15d5b9aa02 Move parts of SpriteComponent to SpriteSystem (#5602)
* Partial sprite component ECS

* release notes

* tests

* Why

* SetSnapCardinals

* NoRotation

* DirectionOverride

* This is why I love distinct overrides that take in object

* LayerSetData

* ISerializationHooks continue to haunt me

* Relocate SetShader

* LayerSetSprite

* LayerSetTexture

* yipeeeee

* LayerSetRsi

* Remove GetFallbackState

* LayerSet Scale,Rotation,Color,Visible

* Fix LayerSetRsi

* LayerSetOffset

* LayerSetDirOffset

* Add overrides that take in a Layer

* LayerSetAnimationTime

* LayerSetRenderingStrategy

* Reduce Resolves, Add Layer.Index

* Access

* Try fix NREs

* Asserts

* LayerGetState

* Cleanup

* Merge helper partial classes

* partial rendering

* GetLayerDirectionCount

* Cache local bounds

* RenderLayer

* RefreshCachedState

* RoundToCardinalAngle

* Fix the pr

* Fix debug assert

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-05-10 21:56:53 +10:00
Leon Friedrich
d24854d94f Improve yaml validation errors for ignored prototypes (#5886)
* Improve yaml validation errors for ignored prototypes

* release notes

* Comments
2025-05-10 21:37:42 +10:00
Tayrtahn
b3cf427013 Catch NotYamlSerializable DataFields with analyzer (#5704)
* Catch NotYamlSerializable DataFields with analyzer

* Extract common defs into shared source
2025-05-10 21:36:33 +10:00
Leon Friedrich
c458abdc69 Move TestPair & PoolManager to engine (#5877)
* Engine pool manager

* Move documentation

* Move namespace

* Move TestMapData to engine

* Option to prevent loading test assembly

* release notes

* Rename to avoid conflicts
2025-05-10 21:35:28 +10:00
metalgearsloth
c76444a33f Version: 256.0.0 2025-05-10 13:40:38 +10:00
B_Kirill
4754661467 Cleanup warnings: CS0649 (#5891)
* Clean up

* Remove "struct UpdateTreesJob"

* Use #pragma

* Use #if DEBUG

* More #if DEBUG
2025-05-10 12:40:15 +10:00
B_Kirill
2a8b776ee9 Cleanup warnings: CS0414 (#5892)
* Clean up

* Use #pragma
2025-05-10 12:39:46 +10:00
SpaceManiac
7d8e5a5841 Fix linear lookup on a dictionary in PlacementManager (#5911) 2025-05-06 16:05:58 +02:00
Centronias
8e416e4519 Makes ItemList not run deselection callback on all list items (#5861)
* Makes ItemList not run deselection callback on all list items

even when they weren't selected

* I cannot be expected to do things intelligently at 2a

* Optimize local usage.

* I'm pretty sure the test failure isn't from ItemList

* switch to avoiding doing anything in the `Selected` setter if the value-to-set-to is the same as the current value.
2025-05-06 14:05:05 +02:00
SlamBamActionman
65f74943d3 Add support for rotated/mirrored tiles (#5652)
* Initial commit

* Add tile rotation/mirror perms

* Nicer UI for the rotation

* Review fixes (also seemed to have missed applying the serialization reading oops)

* One less byte, one less struct size!

* Pretty sure it goes here too

* Fix error
2025-05-05 23:13:31 +10:00
Errant
eb5ed12270 Serialize TimeSpan from text (#5865)
* timespanserializer cleanup

* string reading

* high speed, low drag

* unit tests

* Update Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-05-05 00:57:01 +02:00
PJB3005
c43b7b16c0 Add CancellationTokenRegistration to sandbox 2025-05-05 00:45:05 +02:00
slarticodefast
aee03f0805 fix yaml hotreloading (#5907) 2025-05-04 04:33:37 +02:00
dffdff2423
cfd2b03248 Check audio file signatures instead of extensions (#5894)
* Check audio file signatures instead of extensions

Fixes #5789

Test audio files based on their magic bytes.

* Test for a seekable stream

* Remove Take and Skip linq
2025-05-04 04:33:01 +02:00
dffdff2423
8905a3fe14 Add documentation to the serializer interfaces and remove ITypeReaderWriter (#5897)
* Add documentation to the serializer interfaces

* Remove ITypeReaderWriter and fix the docs

* Fix spelling errors and incorrect docstrings
2025-05-04 04:03:17 +02:00
PJB3005
a878da5b80 Allow texture preload to be skipped for some textures
This is a far cry from a proper resource tracking system, but it's something to avoid a ton of otherwise-unused parallax textures being loaded at game start and consuming VRAM.
2025-05-04 02:24:57 +02:00
Leon Friedrich
806c23e034 Move EntityExt.AsNullable extension methods into the Entity struct (#5899)
* Move `EntityExt.AsNullable` extension methods into the Entity struct

* use constructor

* a
2025-05-04 01:24:23 +02:00
metalgearsloth
e80f5d13a1 Add Vector2i / bitmask conversions (#5901)
Content uses for a couple tile-based flags.
2025-05-04 01:23:04 +02:00
PJB3005
a6905151b6 Move PointLight component states to shared
Necessary so the client can calculate an initial state, which is necessary for prediction and replay seeking to work properly.

Fixes the nuke in SS14 not having its light turn off when going back in a replay.
2025-05-02 01:21:17 +02:00
PJB3005
e742f021fa Dev window tab to show all loaded textures 2025-04-30 15:50:39 +02:00
metalgearsloth
62b4714f1f Version: 255.1.0 2025-04-30 23:38:21 +10:00
Leon Friedrich
1d0404953f Add GridUidChangedEvent and MapUidChangedEvent (#5893)
* Add GridUidChangedEvent and MapUidChangedEvent

* cleanup

* Fix assert

* more fixes

* docs

* record struct

* Use implicit tuple constructor

* stinky review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-04-30 18:36:53 +10:00
Leon Friedrich
d0da13f895 Fix CompileRobustXamlTask for benchmarks (#5902)
* Fix benchmarks

* I love it when path separators are also escape chars
2025-04-30 13:16:28 +10:00
metalgearsloth
ff23f98b26 Document container events (#5904)
It's hard to discern what it's raised directed on and this makes it much easier.
2025-04-30 13:10:03 +10:00
Leon Friedrich
ccfef2a786 Fix PVS NRE (#5900) 2025-04-28 20:11:18 +10:00
B_Kirill
62ce9724fc Clean up (#5890) 2025-04-26 22:59:02 +10:00
Milon
3bbe0e7f44 ftl hot reloading (#5874)
* sloth is so going to kill me

* the voices in my head told me to do this

* Register ILocalizationManagerInternal on client

* Avoid breaking change

* Cleanup

* Release notes
2025-04-26 22:38:56 +10:00
chromiumboy
addd8b5bdd Initial commit (#5880) 2025-04-25 22:19:11 +10:00
ElectroJr
03f8d4d3e0 Version: 255.0.0 2025-04-25 17:32:45 +12:00
Leon Friedrich
728d541ca5 Remove assert in UserInterfaceManager.KeyBindDown (#5889) 2025-04-25 15:30:26 +10:00
Leon Friedrich
4cbce064b8 Fix grid fixtures using locale dependent ids (#5887) 2025-04-24 12:49:04 +02:00
IProduceWidgets
93bb7b1532 Sandbox whitelist for calendar stuff. (#5888) 2025-04-24 12:46:41 +02:00
Pieter-Jan Briers
3ba91d2ed0 Add cycle detection when setting MIDI renderer masters (#5879)
If these things ever get in a cycle (as they did in some SS14 replays),
it'll likely get the client stuck in an infinite loop. Avoid that.
2025-04-24 18:12:59 +10:00
Tayrtahn
8f9e0f6bab Fix warnings in PhysicsMap_Test (#5884) 2025-04-24 18:10:13 +10:00
Tayrtahn
3ed408de5d Fix warnings in Joints_Test (#5885) 2025-04-24 18:09:27 +10:00
DrSmugleaf
2f85408f8f Fix SharedPhysicsSystem.CollideContacts looping contacts that are in nullspace (#5881)
* Fix SharedJointSystem.CreateDistanceJoint taking in an int for minimumDistance instead of a float

* Fix SharedPhysicsSystem.CollideContacts looping contacts that are in nullspace
2025-04-22 22:50:56 +10:00
Leon Friedrich
f49b01b1b7 PredictedDeleteEntity fixes (#5873)
* PredictedDeleteEntity fixes

* release notes
2025-04-20 23:38:43 +10:00
Leon Friedrich
3ce764311d Make RobustIntegrationTest pool by default (#5872) 2025-04-20 17:53:55 +10:00
B_Kirill
adf0b6ae78 Fix CA2264 in Resource Manager (#5870) 2025-04-20 05:02:16 +02:00
metalgearsloth
04406311dc Use EntityQuery for map / grid calls (#5869)
Avoids the type lookup etc etc.
2025-04-20 12:59:06 +10:00
Kyle Tyo
ee45a608b9 Replace IsMap and IsGrid calls with HasComp (#5866) 2025-04-20 12:21:19 +10:00
metalgearsloth
077c91a54b Add autocomplete to scale command (#5864)
* Add autocomplete to scale command

* Also the scale

* this
2025-04-19 22:50:09 +10:00
metalgearsloth
2ac17009ee Deep-copy fixtures on client (#5863)
Fixes a LOTTA bugs
2025-04-19 19:31:36 +10:00
metalgearsloth
a73d1b6666 Update container log warning for pred spawns (#5860)
This was on the full branch I just forgot to pull it out. This is intended that they can go into containers.
2025-04-19 18:08:19 +10:00
metalgearsloth
e5d6f194be Add nullable PredictedDel overloads (#5859)
Forgor when I updated it to Entity<T>
2025-04-19 17:47:25 +10:00
Pieter-Jan Briers
72d893dec5 Add "obsolete inheritance" analyzer (#5858)
This allows us to make it obsolete to *inherit* from a class, and only that.

Intended so people stop inheriting UI controls for no good reason.

Fixes #5856
2025-04-19 17:29:17 +10:00
metalgearsloth
191d7ab81c Version: 254.1.0 2025-04-19 16:53:53 +10:00
metalgearsloth
65d2f2dd2f Entity spawn prediction v1 (#5841)
* Entity spawn prediction v1

Client can't properly interact with this but that requires additional work on top so we can cleanly split this.

* This

* delete fix

* cats
2025-04-19 16:50:41 +10:00
Dae
02b451db2a Add no derivatives licenses to rga validation (#5749) 2025-04-19 12:23:21 +10:00
Ed
d5d4584e11 Update Clyde.GridRendering.cs (#5852) 2025-04-19 00:01:16 +02:00
metalgearsloth
b146b1b82c Version: 254.0.0 2025-04-18 19:06:45 +10:00
Leon Friedrich
2f8f4f2f7a Make MappingDataNode use string keys (#5783)
* string keys

* obsoletions

* Fix ValueTupleSerializer

* Release notes

* fix release note conflict

* cleanup

* Fix yaml validator & tests

* cleanup release notes

* a

* enumerator allocations

* Also sequence enumerator alloc
2025-04-18 19:01:34 +10:00
Tayrtahn
7365a59bd9 Cleanup warnings in CollisionWake_Test (#5847) 2025-04-18 12:05:48 +10:00
Tayrtahn
37560f663b Add GetContainingContainers method to SharedContainerSystem (#5803)
* Add SharedContainerSystem.EnumerateContainingContainers

* More obvious name
2025-04-17 21:51:23 +10:00
TemporalOroboros
bd489e9218 A compilation of simple one-line fixes (#5661)
* Fix warnings in SharedJointSystem

* Fix reference to EC TransformComponent method
Replaces call to TransformComponent.GetMapUid with SharedTransformSystem.GetMap

* Fix obsolete calls in SharedPhysicsSystem.Contacts.cs
Fixes a couple calls to obsolete varients of SetAwake and an obsolete call to RegenerateContacts by converting them to their Entity<T> varients

* Fix obsolete call in SharedPhysicsSystem.Components.cs
Adds one set of parenthesis to convert a 'uid, comp, comp, comp' call to an 'Entity<T, T, T> call.

* Removes unused local var
Removes an unused list of broadphases that was being allocated in TryCollideRect

* One-line fixes in SharedPhysicsSystem.Islands.cs
Fixes all of the easy warnings regarding physics island processing, the rest require more complicated changes than a simple argument rearrangement

* Fix obsolete method call in SharedMapSystem

* Fix a few obsolete ToMap calls in EntityLookup.Queries

* Fix calls to obsolete EntityCoordinate methods in SharedMapSystem.Grids

* Fix calls to obsolete EntityCoordinate methods in SharedLookupSystem.ComponentQueries

* Fix a few obsolete method calls in entity spawning

* Fix obsolete method calls in MapLoaderSystem

* Fix obsolete method call in GridFixtureSystem

* Fix obsolete IsMapInitialized call in SaveMap command

* Fix obsolete MapPosition reference in Client.EyeSystem

* Fix obsolete EntitySystem.Get<TSystem> references in DebugLightTreeSystem

* Fix obsolete EntitySystem.Get<TSystem> reference in DebugEntityLookup command

* Fix obsolete method calls in SpriteBoundsOverlay
Slightly more complicated than the rest, but it's really just changing an unused dependency over to use SharedTransformSystem

* Remove unused IClyde references from controls
LineEdit and TextEdit never use their IClyde dependencies and it generates a warning so yeet

* Remove use of EntitySystem.Get from lightbb command

* Fix DebugDrawingSystem
Removes a bunch of unused private IEntityManager vars
Also removes an obsolete use of TransformComponent.GetWorldPositionRotation

* Removes duplicate position set when splitting grids
There's nothing saying why this is this way and the blame looks like it was an oversight when replacing a bit where they set position and then rotation
Please, oh Chesterton's Fence, spare me your wrath

* Fix obsolete method use in PlacementMode

* Fix obsolete method use in Placement Modes

* Removes unused local var in gamestate management

* Fix unreachable code warnings in gamestate management
Use #else sections to make sure they don't complain about being on the wrong side of a throw

* Fix obsolete ToMap use in EyeManager

* Make InputManager use a sawmill to log

* Fix obsolete ContainerManagerComponent method calls in ContainerSystem

* Make ClientPrototypeManager use a sawmill for logging

* poke tests

* Use LocalizedEntityCommands for SpriteBoundsOverlay toggle

* Use LocalizedEntityCommands for system toggles

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-04-17 11:57:03 +10:00
metalgearsloth
33166e8866 Component lifecycle generics (#5844)
Non-breaking minor.
2025-04-16 21:46:14 +10:00
Leon Friedrich
ed16032280 Disable tile edges (#5842) 2025-04-16 21:25:31 +10:00
metalgearsloth
cf785c886b Mark GetCollidingEntities as obsolete (#5831)
Bad methods, entitylookup is better (it does everything for area queries) and anyone who wants contacts should be querying those directly.
2025-04-14 18:50:01 +10:00
metalgearsloth
6f28c396cf Version: 253.0.0 2025-04-14 14:12:32 +10:00
metalgearsloth
b631f408f2 ItemList optimisation (#5796)
- VV AddComponent window no longer takes 300ms every time you press a key.
- Significantly optimise ItemList internally.
2025-04-14 13:55:48 +10:00
Leon Friedrich
34f4cf9452 More map validator fixes (#5820) 2025-04-14 13:55:02 +10:00
B_Kirill
6a4e4cf3b4 Cleanup warnings: Commands (#5825) 2025-04-14 13:54:42 +10:00
metalgearsloth
8aefa5c53e Make TestPoint use generics (#5828) 2025-04-14 01:49:07 +10:00
DrSmugleaf
2cfc981aa3 Fix popup text overflowing the sides of the screen (#5788)
* Fix popup text overflowing the sides of the screen

* Fix not escaping text
2025-04-13 12:59:23 +02:00
Leon Friedrich
4987c324d9 Fix bad debug assert (#5826) 2025-04-13 20:50:46 +10:00
DrSmugleaf
5450ddd0ba Fix SharedJointSystem.CreateDistanceJoint taking in an int for minimumDistance instead of a float (#5824) 2025-04-13 16:22:22 +10:00
Tayrtahn
378a10678c Improve location reporting for non-writeable DataFields (#5715)
* Better location reporting for readonly DataField errors

* Better location reporting for readonly DataField property errors

* Use SyntaxKind instead of string for TryGetModifierLocation
2025-04-13 16:22:05 +10:00
Leon Friedrich
2e0735b92f Fix NRE in screen-space overlays (#5823)
* Fix pre-init screen-space overlays

* Debug asserts
2025-04-13 16:21:02 +10:00
Leon Friedrich
5756d15333 Make BoundUserInterfaceMessageAttempt broadcast again (#5821) 2025-04-12 18:22:13 +10:00
Leon Friedrich
b6f74b8dea Fix logMissing in EntitySystem.Resolve() (#5822) 2025-04-12 17:40:08 +10:00
Leon Friedrich
3800c5707e Add new SerializationManager.PushComposition overload (#5785) 2025-04-12 12:58:40 +10:00
Leon Friedrich
8f49785b4e Fix RemCompDeferred not always setting lifestage (#5786)
* Fix RemCompDeferred not setting lifestage

* spaelling
2025-04-12 12:57:11 +10:00
metalgearsloth
f274de0f10 Version: 252.0.0 2025-04-12 01:43:29 +10:00
Milon
e128338f9d hi (#5811) 2025-04-12 01:29:41 +10:00
metalgearsloth
588c46273e Version: 251.0.0 2025-04-10 20:53:09 +10:00
Whatstone
919de8ce0e SharedPhysicsSystem.Island: set position after velocity (#5801) 2025-04-09 01:10:17 +10:00
Errant
af27d2d872 equatable FormattedMessage, take 2 (#5780)
* improved Equals and getHashCode

* less jank
2025-04-08 16:50:08 +02:00
Tayrtahn
45bb8740a0 Add ForbidLiteralAttribute and analyzer (#5808)
* Add ForbidLiteral attribute, analyzer, and test

* Removed unused code

* Switch order of methods. It's better this way.
2025-04-08 16:39:38 +02:00
Tobias Berger
4a24539629 Don't implement GetMassData twice (#5816)
The removed body didn't calculate mass for circles
2025-04-09 00:14:45 +10:00
PJB3005
7536c4ec68 Log late MsgEntity again
Yay :)
2025-04-07 15:50:40 +02:00
metalgearsloth
9268c8629d Refactor TileEdgeOverlay (#5295)
* Refactor TileEdgeOverlay

* weh

* Updates

* exweh

* Stupid weh noises

* I am le stupid

* Add logging about tile atlas build time

Seems fine, but wanted to check.

* Don't over-allocate edge tile region array

* Add DirectionExtensions.AllDirections

* Clean up iteration in ClydeTileDefinitionManager

* Don't stackalloc large chunk edge buffers, other code cleanup.

* More release notes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-03 07:52:28 +02:00
PJB3005
0bc0cafe64 Show entity name in "physics shapeinfo" output 2025-04-03 02:59:09 +02:00
PJB3005
8891f3fa0a Make EntitySystem.Subscriptions.SubscribeLocalEvent not require EntityEventArgs
This means it can be used with struct events.
2025-04-03 02:58:20 +02:00
Tornado Tech
4f96c2d233 Added separate localization & clean up (#5227)
* Added separate localization & clean up

* Added new methods docs

* Added GetFoundCultures method

* Clean up code

* Removed some formating shit

* Do better CultureInfo comparison

* Oops

* Review

* Command fixes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-02 16:41:02 +02:00
PJB3005
ab55d5b2f2 Revert "Add Loc property to LocalizedCommands"
This reverts commit 806c5b694b.
2025-04-02 05:23:43 +02:00
PJB3005
806c5b694b Add Loc property to LocalizedCommands
Avoids some ~300 usages of static Loc in SS14 and RT
2025-04-02 05:21:43 +02:00
metalgearsloth
6898053dbd Add autocomplete to tp command (#5795) 2025-04-02 03:48:16 +02:00
metalgearsloth
ae625ebad8 Inline manifold points (#5794)
* Inline manifold points

Max is 2 so no reason to use an array over a fixedarray.

* this
2025-04-02 00:04:51 +11:00
metalgearsloth
3c754a4f49 Don't disable contacting collisionwake ents (#5798)
Good for some content stuff I don't think it caused issues.
2025-04-01 15:04:05 +11:00
Tayrtahn
d84cb6327c Fix SharedTransformSystem methods erroring on failed Resolves (#5787)
* Don't error when GetGrid fails

* Fix other Resolves in SharedTransformSystem
2025-04-01 04:59:47 +11:00
Ciarán Walsh
4bfd92dbc5 Add button to jump to live chat when scrolled up (#5750)
* Add button to jump to live chat when scrolled up

* Expose scroll button class name as a public constant

* Add localisation string to engine

* Make enabling the OutputPanel scroll button opt-in

* Enable scroll button for the debug console

* De-duplicate visibility logic

* Update scroll button visibility when the enabling property is changed
2025-03-30 03:07:54 +02:00
metalgearsloth
c7d228c223 savemap / savegrid autocomplete (#5784)
How mappers coping without this.
2025-03-27 18:54:13 +11:00
Tayrtahn
f244c94905 Switch from checking comp.Owner == ent to GetComp(ent) == comp (#5776) 2025-03-27 15:21:05 +11:00
Tayrtahn
01cac6465b Cleanup warnings in EntityCoordinates_Tests (#5771)
* Fix warnings

* Use transform refs to simplify WithEntityId
2025-03-27 15:20:20 +11:00
metalgearsloth
5a5f238d9a Version: 250.0.0 2025-03-27 15:12:01 +11:00
Tayrtahn
089224cd44 Cleanup warnings in EntityLookup_Test (#5775)
* Replace MapManager.DeleteMap calls with MapSystem.DeleteMap

* Remove unused resolves
2025-03-27 15:07:05 +11:00
Tayrtahn
9f807f1ad2 Cleanup warnings in ClientGameStateManager (#5774)
* Fix unreachable code

* Remove unused variable
2025-03-27 15:06:37 +11:00
metalgearsloth
4be95ea375 Add OtherBody API to contacts (#5779)
* Add OtherBody API to contacts

Thought I had a pr for this.

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-03-27 15:06:04 +11:00
Jerry
03010bf4be Fix DirectoryNotFoundException when saving map or grid on Unix systems (#5773)
* fix(maploader): Fix DirectoryNotFoundException

Someone forgor to create directory before saving map or grid, this caused
an exception on trying to save smth anywhere else than /

* update release notes
2025-03-27 13:56:26 +11:00
Tayrtahn
dacaa974d4 Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in misc tests (#5777)
* Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in various tests

* Poke tests

* I guess this is was technically a breaking change?
2025-03-27 13:50:56 +11:00
DrSmugleaf
9f73e0398a Make MappingDataNode.Equals take 2952 times less time to run when loading maps (#5781) 2025-03-27 12:52:29 +11:00
Tayrtahn
ccc383b1bf Cleanup: Remove redundant Prototype names (#5721)
* Cleanup: Remove redundant Prototype names

* Actually this one probably should stay

* Suppress warning

* Remove warning suppression on AudioMetadataPrototype

* But wait, there's more!
2025-03-26 01:39:10 +01:00
PJB3005
ceb59402a1 Make status and info APIs have CORS allow-origin: *
Allows it to be queried from browser JS. No harm in not allowing this.

Added helper function StatusExt.AddAllowOriginAny to make this easy to add.
2025-03-26 01:16:23 +01:00
Errant
5a6b29fcd2 equatable FormattedMessage (#5772) 2025-03-25 15:32:47 +01:00
PJB3005
6b87cd1e1c Deprecate HCY color space functions
These functions use the wrong color primaries (Rec 601 instead of 709) to calculate the luminance, so I'm just gonna throw them out.

Also, I don't actually trust anybody to know to do sRGB conversion before using them.

Discovered this as a result of reviewing #5360
2025-03-24 04:47:34 +01:00
Leon Friedrich
cf2d6a1dbf Make unshaded sprite layers not require shader/texture changes. (#5248)
* Make unshaded sprite layers not require shader/texture changes.

* Always sample texture

* revert some variable name changes

* Use color SIMD
2025-03-24 02:37:04 +01:00
chromiumboy
f8c838f425 Pass AnimationPlayerComponent in AnimationCompletionEvent (#5755)
* Added a field for the animation player component to the animation completion event

* Addressed review comment

* Updated
2025-03-24 02:22:46 +01:00
Tayrtahn
7405904041 Add entity description as tooltip on entity spawn panel (#5761) 2025-03-23 14:07:22 +01:00
metalgearsloth
2eeebab275 Add pure to some angle methods (#5763)
Saw some of these floating around in mover code.
2025-03-22 13:43:18 +01:00
PJB3005
2856bb3626 Shut up RS1038 warnings
We aren't going to fix these until #5610 is figured out, and these aren't even an indicator of an issue itself.

They indicate that we have code fixes in the same assembly as analyzers, meaning the analyzers COULD fail if we relied on some code fix libs - something we don't do.
2025-03-22 06:20:43 +01:00
PJB3005
be0189748b Fix serialization source gen with partial types
This fixes RMC compilation
2025-03-22 06:09:48 +01:00
Tayrtahn
4529a7569a Replace uses of ProtoId<EntityPrototype> with ProtoId<EntityCategoryPrototype> (#5762) 2025-03-21 04:08:12 +01:00
metalgearsloth
2b16e4db96 Version: 249.0.0 2025-03-21 00:52:45 +11:00
metalgearsloth
64f2245194 Add UpdateVisibilityMask method (#5745)
* Add UpdateVisibilityMask method

We tipped over to the point of systems stepping on each other's toes. Now we do the normal thing and just use the eventbus and it makes content a whole lot cleaner.

* Update resolve

* Update name in line with normal.

* Unserialize this

* weh
2025-03-21 00:47:40 +11:00
Milon
1029047e2f fix (#5752) 2025-03-20 22:30:59 +11:00
metalgearsloth
45dc9ad80e Inline polygon vertices (#5758)
* FastPoly

* Inline polygon vertices

No more pooling, pooling bad. No more arrays in engine, only span.

I made a slim version that's only the 4 verts so no padding on it compared to Polygon. Slightly more clamplicated code but entitylookup + mapmanager are both hotpaths and it's easy to do. Memory usage will likely go up for now but heap allocations should drop significantly due to removing the pooling.

* Unhide these

* Fixes

* More fixes

* More fixes

* Avoid potential bomb
2025-03-20 21:26:05 +11:00
metalgearsloth
54ad808eea Add GetWorldManifold overload (#5756)
* Add GetWorldManifold overload

* revert
2025-03-20 21:10:04 +11:00
metalgearsloth
37c75df6a2 Fix showvelocities (#5759)
* Fix showvelocities

Can't use StateRoot anymore so just pretend it's a window.

* Also file-scoped
2025-03-20 21:05:58 +11:00
slarticodefast
e93c1fae61 Add velocity and angular velocity debug overlays (#5693)
* add velocity and angular velocity debug overlays

* minor improvement

* add descriptions

* fix

* review

* Update RELEASE-NOTES.md

* Update RELEASE-NOTES.md

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-20 13:10:16 +11:00
metalgearsloth
cd0a35f542 Fix light Aabb query (#5744)
Forgot when this got dropped but we need it for grid-tree query as lights spill over grids.
2025-03-13 19:18:23 +11:00
PJB3005
80f2dc6dd3 Fix BoxContainer stretching causing controls to be made too small
In which I fix a bug by just deleting a ton of code and doing nothing else.

(also I added unit tests)
2025-03-13 01:01:18 +01:00
metalgearsloth
139b6f796c Version: 248.0.2 2025-03-12 20:26:50 +11:00
metalgearsloth
2ee7c35fd3 Don't throw on invalid MapUids for overlays (#5740) 2025-03-12 20:16:13 +11:00
metalgearsloth
56eae5ad08 Reduce EntityManager.IsDefault allocations (#5741)
* Reduce EntityManager.IsDefault allocations

We don't actually need the MappingDataNode so we can avoid allocating the entire class per entity.

* Adjust this order
2025-03-12 19:42:08 +11:00
DrSmugleaf
0662cae224 Version: 248.0.1 2025-03-11 19:38:08 -07:00
metalgearsloth
0e2b00edd0 Fix NaN gain audio (#5737) 2025-03-10 20:16:05 +11:00
Richard Van Tassel
d48f7ecb5b bumps ImageSharp version (#5733) 2025-03-08 15:19:23 +01:00
metalgearsloth
e064b7a4f9 Version: 248.0.0 2025-03-08 15:34:53 +11:00
metalgearsloth
654480862e Use Prototype for ITileDef (#5731)
Forgot to push this.
2025-03-08 15:20:25 +11:00
metalgearsloth
353c044b52 Hot reload resources (#5443)
* Fix ResPath CanonPath

Apparently this is supposed to standardise to / but this isn't always the case. Alternatively we could just assert for performance reasons I'm good with either. The comment as written says this should happen.

* Fixes

* change

* assert

* Fix bad respath input

* Buffer

* Merge conflicts

* review

* Fix
2025-03-08 15:16:31 +11:00
Whatstone
3dda8d9e93 Try/catch blocks around BUI open/dispose calls (#5730) 2025-03-08 15:16:11 +11:00
metalgearsloth
41ea10083d Fix ResPath CanonPath (#5452)
* Fix ResPath CanonPath

Apparently this is supposed to standardise to / but this isn't always the case. Alternatively we could just assert for performance reasons I'm good with either. The comment as written says this should happen.

* assert

* Fix bad respath input

* review
2025-03-08 15:02:46 +11:00
eoineoineoin
6290bb7af1 Adds method to Controls.ItemList which updates the item list without erasing contents (#5425)
* Add algorithm from ss14#30292 to ItemList, for use in other UIs

* Add overload for common case of comparing items by their text label
2025-03-08 14:43:43 +11:00
metalgearsloth
348ab70a8d Update B2DynamicTree (#5332)
* Update B2DynamicTree

* API updates

* weh

* forcing it

* Fix all of the bugs

* Rebuild

* A crumb of danger

* Fix merge conflicts
2025-03-08 14:15:47 +11:00
IProduceWidgets
47e11e988c Fix map netId completions (#5495)
* make map netId completions function.

* Update Robust.Shared/Console/CompletionHelper.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-08 13:57:13 +11:00
Leon Friedrich
c459b55052 Add TryLoadGrid override (#5701)
* Add TryLoadGrid override

* fix cmd-addmap-help
2025-03-08 13:50:14 +11:00
Tayrtahn
f10e96a6d1 Clean up warnings in Stack_Test (#5705) 2025-03-08 13:49:52 +11:00
metalgearsloth
e8bac558c6 Use Entity<T> for TileChangedEvent (#5698)
Content is manually resolving this in a few spots so we can avoid that overhead.
2025-03-08 13:48:32 +11:00
metalgearsloth
ffa3bb7202 Fix savedpos caching on UI shutdown (#5729)
Don't need this as it gets handled already.
2025-03-08 13:28:29 +11:00
Dylan Craine
9bcdc95651 Fix deceptive "successfully saved" messages for mappers (#5714)
* Actually check if map save succeeded before displaying success message

It would be great to offer more clarity to the mapper about *why* the
save didn't succeed, but at least they won't be deceived into thinking
their work has been saved when it hasn't.

Portuguese localization text is via DuckDuckGo Translate, so I hope it's
reasonable.

* Actually check save success for saving grids

These messages need localization, too, but that seems out of scope for
my PR.

* Improve map save error message

Now it tells the mapper to go look at the server log.
Still translated via DuckDuckGo Translate.

* Normalize indentation and style
2025-03-08 13:27:59 +11:00
TemporalOroboros
543088ea1f Remove unused private members/local vars (#5722) 2025-03-05 15:50:54 +01:00
deltanedas
56daa63783 make map loading logs better (#5723)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2025-03-05 15:49:20 +01:00
metalgearsloth
9fe9730d4a Add public API for physicshull (#5719) 2025-03-03 22:07:38 +11:00
SlamBamActionman
a1a7ea92d9 Add Regex.Count and StringBuilder.set_Chars to sandbox whitelist (#5717) 2025-03-01 22:42:19 +01:00
poklj
0dec6a425f Modify TryCopyComponents metadata TryGetComponent (#5710) 2025-02-27 11:25:28 +11:00
metalgearsloth
56ced913b7 Audio fixes (#5707) 2025-02-26 22:08:17 +11:00
PJB3005
76b46479b6 Version: 247.2.0 2025-02-23 01:44:44 +01:00
PJB3005
de9a8d286a Release notes 2025-02-23 01:43:58 +01:00
Milon
a1df0fb4af fix some issues with ClientDisconnect (#5625)
* fix

* actually fix for real this time

* just use HappyEyeballsHttp :godo:

* review
2025-02-23 01:33:58 +01:00
pathetic meowmeow
e6bc5a1057 Proxy scrollbar values and value targets in ScrollContainer (#5697) 2025-02-22 22:10:32 +01:00
beck-thompson
11b24579a2 Fix MultiRootInheritanceGraph not detecting circular inheritance (#5672)
* Fix

* This is better!

* Fixes
2025-02-22 22:08:31 +01:00
metalgearsloth
685d002bb7 Move VisibilitySystem to shared (#5694)
* Move VisibilitySystem to shared

* this

* Remove redundant qualifiers.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-02-22 21:36:49 +01:00
Tobias Berger
2e0d18aeaf Fix wrong parameters for Regex.Escape in Sandbox whitelist (#5688) 2025-02-22 18:08:28 +01:00
Kyle Tyo
06dbff0429 believe that should be all of em. (#5691) 2025-02-22 18:01:07 +01:00
Southbridge
15958a9447 Fix issue regarding Tilemaps not saving when modified (#5696)
* One line C# fix

* fixed typo

* after spending way too long trying to figure out the problem, turns out all we needed was a simple one line change
2025-02-22 17:33:33 +01:00
pathetic meowmeow
fd5a4d9b8a Refactor audio system to send collection IDs over the network (#5540)
This is important groundwork for future features such as captioning,
as a caption and other data can be associated with the collection
prototype instead of passing extra data everywhere with the sound.
2025-02-22 17:29:47 +01:00
DrSmugleaf
6d958847cb Fix prototype hot reloading crashing when adding a component that an existing entity already has (#5695) 2025-02-22 16:30:05 +01:00
Milon
8a04a4f3a5 Add a method for copying components (#5654)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-02-21 09:46:13 +11:00
ElectroJr
7104a4f459 Version: 247.1.0 2025-02-20 16:26:04 +13:00
Leon Friedrich
f29949a32c Revert "Add ICloneable support to ComponentNetworkGenerator (#5656)" (#5687)
This reverts commit e14537074e.
2025-02-20 14:22:36 +11:00
Leon Friedrich
3bbbabf238 Update map format validator (#5686)
* Update map format validator

* string -> str
2025-02-20 12:11:12 +11:00
metalgearsloth
d95aca3d9e Fix DirtyFields proxy method (#5684) 2025-02-20 00:15:05 +11:00
Tayrtahn
e14537074e Add ICloneable support to ComponentNetworkGenerator (#5656) 2025-02-18 23:21:40 +11:00
DrSmugleaf
af2d01981f Add optional minimumDistance parameter to SharedJointSystem.CreateDistanceJoint (#5682) 2025-02-18 14:18:15 +11:00
Fildrance
7df23e047c feat: shaders now can accept array of Color as parameter (#5679)
* feat: shaders now can accept array of Color as parameter

* fix: Clyde.SetUniformDirect for Color[] doesn't mutate original array, removed invalid 'in' keyword on SetUniform

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:13:04 +01:00
ElectroJr
5c7ab43049 Fix typo in RELEASE-NOTES.md 2025-02-17 00:15:27 +13:00
ElectroJr
8f75560ec4 Version: 247.0.0 2025-02-17 00:14:18 +13:00
Leon Friedrich
b323c8bd1e Add support for optional and params T[] toolshed command arguments. (#5573)
* Include argument name in completion suggestions

* Support optional args

* It (not so shrimply) works

* Add tests

* Add TestGenericPipeInference

* Fix tests

* Release notes

* Overzealous YAMLLinter

* Improve help signatures, fix map command

* Improve NoImplementationError

* Better type argument help signatures

* better pipe syntax

* fix NRE

* Add test

* a

* Fix silent toolshed failure

* Fix GetConcreteMethodInternal

* Improve vars command

* EntProtoId IAsType

* More GetConcreteMethodInternal fixes

* I hate this so much

* update tp command description

The command arguments call the the "other" entity the "target"

* Support localized argument hints/signatures
2025-02-16 21:55:05 +11:00
Leon Friedrich
faef44daaa Make PVS overrides respect vismasks (#5598)
* Make overrides respect vismasks

* Thread safety

* Release notes

* Use ExpandPvsEvent.Mask for other overrides

* check if already queued
2025-02-16 21:32:23 +11:00
Leon Friedrich
fbc706f37b Refactor map loading & saving (#5572)
* Refactor map loading & saving

* test fixes

* ISerializationManager tweaks

* Fix component composition

* Try fix entity deserialization component composition

* comments

* CL

* error preinit

* a

* cleanup

* error if version is too new

* Add AlwaysPushSerializationTest

* Add auto-inclusion test

* Better categorization

* Combine test components

* Save -> TrySave

Also better handling for saving multiple entities individually

* Create new partial class for map loading

* Add OrphanSerializationTest

* Include MapIds in BeforeSerializationEvent

* Addd LifetimeSerializationTest

* Add TestMixedLifetimeSerialization

* Add CategorizationTest

* explicitly serialize list of nullspace entities

* Add backwards compatibility test

* Version comments

also fixes wrong v4 format

* add MapMergeTest

* Add NetEntity support

* Optimize EntityDeserializer

Avoid unnecessary component deserialization

* fix assert & other bugs

* fucking containers strike again

* Fix deletion of pre-init entities

* fix release note merge conflict

* Update Robust.Shared/Map/MapManager.GridCollection.cs

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

* VV

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-02-16 21:25:07 +11:00
847 changed files with 37249 additions and 23638 deletions

View File

@@ -0,0 +1,34 @@
name: Build All Configurations
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
strategy:
matrix:
targetOS: [Windows, Linux, MacOS]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Debug
run: dotnet build --no-restore --configuration Debug /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}
- name: Build Tools
run: dotnet build --no-restore --configuration Tools /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}
- name: Build Release
run: dotnet build --no-restore --configuration Release /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}

View File

@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}

View File

@@ -26,7 +26,7 @@ jobs:
dotnet-version: 9.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
run: Tools/package_client_build.py
- name: Shuffle files around
run: |

View File

@@ -44,10 +44,11 @@
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.9.4" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.3.0" />
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
<PackageVersion Include="Robust.Natives" Version="0.2.3" />
<PackageVersion Include="Robust.Natives.Zstd" Version="0.1.1-zstd1.5.7" />
<PackageVersion Include="Robust.Natives.Cef" Version="131.3.5" />
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
@@ -55,11 +56,15 @@
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />
<PackageVersion Include="libsodium" Version="1.0.20.1" />
<PackageVersion Include="System.Management" Version="9.0.8" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
<PackageVersion Include="VorbisPizza" Version="1.3.0" />

View File

@@ -13,7 +13,7 @@
</When>
<Otherwise>
<PropertyGroup>
<DefineConstants>$(DefineConstants);LINUX;UNIX</DefineConstants>
<DefineConstants>$(DefineConstants);LINUX;UNIX;FREEDESKTOP</DefineConstants>
</PropertyGroup>
</Otherwise>
</Choose>

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -31,5 +31,6 @@
<Python>python3</Python>
<Python Condition="'$(ActualOS)' == 'Windows'">py -3</Python>
<UseSystemSqlite Condition="'$(TargetOS)' == 'FreeBSD'">True</UseSystemSqlite>
<IsFreedesktop Condition="'$(TargetOS)' == 'FreeBSD' Or '$(TargetOS)' == 'Linux'">True</IsFreedesktop>
</PropertyGroup>
</Project>

View File

@@ -16,7 +16,10 @@
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false">
<SetConfiguration Condition="'$(Configuration)' == 'DebugOpt'">Configuration=Debug</SetConfiguration>
<SetConfiguration Condition="'$(Configuration)' == 'Tools'">Configuration=Release</SetConfiguration>
</ProjectReference>
</ItemGroup>
<!-- XamlIL does not make use of special Robust configurations like DebugOpt. Convert these down. -->
@@ -71,6 +74,6 @@
</PropertyGroup>
<Exec
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath=&quot;$(IntermediateOutputPath.TrimEnd('\'))/&quot;"/>
</Target>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,8 @@ zzzz-object-pronoun = { GENDER($ent) ->
}
# Used internally by the DAT-OBJ() function.
# Not used in en-US. Created for supporting other languages.
# Not used in en-US. Created to support other languages.
# (e.g., "to him," "for her")
zzzz-dat-object = { GENDER($ent) ->
[male] him
[female] her
@@ -29,6 +30,16 @@ zzzz-dat-object = { GENDER($ent) ->
*[neuter] it
}
# Used internally by the GENITIVE() function.
# Not used in en-US. Created to support other languages.
# e.g., "у него" (Russian), "seines Vaters" (German).
zzzz-genitive = { GENDER($ent) ->
[male] his
[female] her
[epicene] their
*[neuter] its
}
# Used internally by the POSS-PRONOUN() function.
zzzz-possessive-pronoun = { GENDER($ent) ->
[male] his

View File

@@ -0,0 +1,3 @@
generic-map = map
generic-grid = grid
generic-mapid = map Id

View File

@@ -1,16 +1,16 @@
# Loc strings for various entity state & client-side PVS related commands
cmd-reset-ent-help = Usage: resetent <Entity UID>
cmd-reset-ent-desc = Reset an entity to the most recently received server state. This will also reset entities that have been detached to null-space.
cmd-reset-ent-help = Usage: {$command} <Entity UID>
cmd-reset-ent-desc = Reset an entity to the most recently received server state. This will also reset entities that have been detached to null-space.
cmd-reset-all-ents-help = Usage: resetallents
cmd-reset-all-ents-desc = Resets all entities to the most recently received server state. This only impacts entities that have not been detached to null-space.
cmd-reset-all-ents-help = Usage: {$command}
cmd-reset-all-ents-desc = Resets all entities to the most recently received server state. This only impacts entities that have not been detached to null-space.
cmd-detach-ent-help = Usage: detachent <Entity UID>
cmd-detach-ent-help = Usage: {$command} <Entity UID>
cmd-detach-ent-desc = Detach an entity to null-space, as if it had left PVS range.
cmd-local-delete-help = Usage: localdelete <Entity UID>
cmd-local-delete-help = Usage: {$command} <Entity UID>
cmd-local-delete-desc = Deletes an entity. Unlike the normal delete command, this is CLIENT-SIDE. Unless the entity is a client-side entity, this will likely cause errors.
cmd-full-state-reset-help = Usage: fullstatereset
cmd-full-state-reset-help = Usage: {$command}
cmd-full-state-reset-desc = Discards any entity state information and requests a full-state from the server.

View File

@@ -0,0 +1,33 @@
color-hue-chroma-lightness = {$lightness} {$chroma} {$hue}
color-hue-chroma = {$chroma} {$hue}
color-hue-lightness = {$lightness} {$hue}
color-very-dark = very dark
color-dark = dark
color-light = light
color-very-light = very light
color-mixed-hue = {$a} {$b}
color-pale = pale
color-gray-adjective = gray
color-strong = strong
color-pink = pink
color-red = red
color-orange = orange
color-yellow = yellow
color-green = green
color-cyan = cyan
color-blue = blue
color-purple = purple
color-brown = brown
color-white = white
color-gray = gray
color-black = black
color-unknown = unknown color, you should not see this
color-pink-color-red = pinkish red
color-red-color-orange = reddish orange
color-orange-color-yellow = orangeish yellow
color-yellow-color-green = yellowish green
color-green-color-cyan = greenish cyan
color-cyan-color-blue = cyanish blue
color-blue-color-purple = blueish purple
color-purple-color-pink = purpleish pink

View File

@@ -1,5 +1,7 @@
### Localization for engine console commands
cmd-hint-float = [float]
## generic command errors
cmd-invalid-arg-number-error = Invalid number of arguments.
@@ -11,6 +13,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-cultureinfo = "{$arg}" is not valid CultureInfo.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
@@ -20,8 +23,8 @@ cmd-error-dir-not-found = Could not find directory: {$dir}.
cmd-failure-no-attached-entity = There is no entity attached to this shell.
## 'help' command
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
cmd-help-desc = Display general help or help text for a specific command.
cmd-help-help = Usage: {$command} [command name]
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
@@ -32,7 +35,7 @@ cmd-help-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.
cmd-cvar-help = Usage: cvar <name | ?> [value]
cmd-cvar-help = Usage: {$command} <name | ?> [value]
If a value is passed, the value is parsed and stored as the new value of the CVar.
If not, the current value of the CVar is displayed.
Use 'cvar ?' to get a list of all registered CVars.
@@ -46,14 +49,14 @@ cmd-cvar-value-hidden = <value hidden>
## 'cvar_subs' command
cmd-cvar_subs-desc = Lists the OnValueChanged subscriptions for a CVar.
cmd-cvar_subs-help = Usage: cvar_subs <name>
cmd-cvar_subs-help = Usage: {$command} <name>
cmd-cvar_subs-invalid-args = Must provide exactly one argument.
cmd-cvar_subs-arg-name = <name>
## 'list' command
cmd-list-desc = Lists available commands, with optional search filter
cmd-list-help = Usage: list [filter]
cmd-list-desc = Lists available commands, with optional search filter.
cmd-list-help = Usage: {$command} [filter]
Lists all available commands. If an argument is provided, it will be used to filter commands by name.
cmd-list-heading = SIDE NAME DESC{"\u000A"}-------------------------{"\u000A"}
@@ -61,13 +64,13 @@ cmd-list-heading = SIDE NAME DESC{"\u000A"}-------------------------{
cmd-list-arg-filter = [filter]
## '>' command, aka remote exec
cmd-remoteexec-desc = Executes server-side commands
cmd-remoteexec-desc = Executes server-side commands.
cmd-remoteexec-help = Usage: > <command> [arg] [arg] [arg...]
Executes a command on the server. This is necessary if a command with the same name exists on the client, as simply running the command would run the client command first.
## 'gc' command
cmd-gc-desc = Run the GC (Garbage Collector)
cmd-gc-help = Usage: gc [generation]
cmd-gc-desc = Run the GC (Garbage Collector).
cmd-gc-help = Usage: {$command} [generation]
Uses GC.Collect() to execute the Garbage Collector.
If an argument is provided, it is parsed as a GC generation number and GC.Collect(int) is used.
Use the 'gfc' command to do an LOH-compacting full GC.
@@ -76,13 +79,13 @@ cmd-gc-arg-generation = [generation]
## 'gcf' command
cmd-gcf-desc = Run the GC, fully, compacting LOH and everything.
cmd-gcf-help = Usage: gcf
cmd-gcf-help = Usage: {$command}
Does a full GC.Collect(2, GCCollectionMode.Forced, true, true) while also compacting LOH.
This will probably lock up for hundreds of milliseconds, be warned.
## 'gc_mode' command
cmd-gc_mode-desc = Change/Read the GC Latency mode
cmd-gc_mode-help = Usage: gc_mode [type]
cmd-gc_mode-desc = Change/Read the GC Latency mode.
cmd-gc_mode-help = Usage: {$command} [type]
If no argument is provided, returns the current GC latency mode.
If an argument is passed, it is parsed as GCLatencyMode and set as the GC latency mode.
@@ -95,8 +98,8 @@ cmd-gc_mode-result = resulting gc latency mode: { $mode }
cmd-gc_mode-arg-type = [type]
## 'mem' command
cmd-mem-desc = Prints managed memory info
cmd-mem-help = Usage: mem
cmd-mem-desc = Prints managed memory info.
cmd-mem-help = Usage: {$command}
cmd-mem-report = Heap Size: { TOSTRING($heapSize, "N0") }
Total Allocated: { TOSTRING($totalAllocated, "N0") }
@@ -105,26 +108,26 @@ cmd-mem-report = Heap Size: { TOSTRING($heapSize, "N0") }
cmd-physics-overlay = {$overlay} is not a recognised overlay
## 'lsasm' command
cmd-lsasm-desc = Lists loaded assemblies by load context
cmd-lsasm-desc = Lists loaded assemblies by load context.
cmd-lsasm-help = Usage: lsasm
## 'exec' command
cmd-exec-desc = Executes a script file from the game's writeable user data
cmd-exec-help = Usage: exec <fileName>
cmd-exec-desc = Executes a script file from the game's writeable user data.
cmd-exec-help = Usage: {$command} <fileName>
Each line in the file is executed as a single command, unless it starts with a #
cmd-exec-arg-filename = <fileName>
## 'dump_net_comps' command
cmd-dump_net_comps-desc = Prints the table of networked components.
cmd-dump_net_comps-help = Usage: dump_net-comps
cmd-dump_net_comps-help = Usage: {$command}
cmd-dump_net_comps-error-writeable = Registration still writeable, network ids have not been generated.
cmd-dump_net_comps-header = Networked Component Registrations:
## 'dump_event_tables' command
cmd-dump_event_tables-desc = Prints directed event tables for an entity.
cmd-dump_event_tables-help = Usage: dump_event_tables <entityUid>
cmd-dump_event_tables-help = Usage: {$command} <entityUid>
cmd-dump_event_tables-missing-arg-entity = Missing entity argument
cmd-dump_event_tables-error-entity = Invalid entity
@@ -132,7 +135,7 @@ cmd-dump_event_tables-arg-entity = <entityUid>
## 'monitor' command
cmd-monitor-desc = Toggles a debug monitor in the F3 menu.
cmd-monitor-help = Usage: monitor <name>
cmd-monitor-help = Usage: {$command} <name>
Possible monitors are: { $monitors }
You can also use the special values "-all" and "+all" to hide or show all monitors, respectively.
@@ -145,23 +148,24 @@ cmd-monitor-plus-all-hint = Shows all monitors
## 'setambientlight' command
cmd-set-ambient-light-desc = Allows you to set the ambient light for the specified map, in SRGB.
cmd-set-ambient-light-help = setambientlight [mapid] [r g b a]
cmd-set-ambient-light-help = Usage: {$command} [mapid] [r g b a]
cmd-set-ambient-light-parse = Unable to parse args as a byte values for a color.
## Mapping commands
cmd-savemap-desc = Serializes a map to disk. Will not save a post-init map unless forced.
cmd-savemap-help = savemap <MapID> <Path> [force]
cmd-savemap-help = Usage: {$command} <MapID> <Path> [force]
cmd-savemap-not-exist = Target map does not exist.
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
cmd-savemap-success = Map successfully saved.
cmd-savemap-error = Could not save map! See server log for details.
cmd-hint-savemap-id = <MapID>
cmd-hint-savemap-path = <Path>
cmd-hint-savemap-force = [bool]
cmd-loadmap-desc = Loads a map from disk into the game.
cmd-loadmap-help = loadmap <MapID> <Path> [x] [y] [rotation] [consistentUids]
cmd-loadmap-help = Usage: {$command} <MapID> <Path> [x] [y] [rotation] [consistentUids]
cmd-loadmap-nullspace = You cannot load into map 0.
cmd-loadmap-exists = Map {$mapId} already exists.
cmd-loadmap-success = Map {$mapId} has been loaded from {$path}.
@@ -176,73 +180,74 @@ cmd-hint-savebp-id = <Grid EntityID>
## 'flushcookies' command
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
cmd-flushcookies-desc = Flush CEF cookie storage to disk
cmd-flushcookies-help = This ensure cookies are properly saved to disk in the event of unclean shutdowns.
cmd-flushcookies-desc = Flush CEF cookie storage to disk.
cmd-flushcookies-help = Usage: {$command}
This ensure cookies are properly saved to disk in the event of unclean shutdowns.
Note that the actual operation is asynchronous.
cmd-ldrsc-desc = Pre-caches a resource.
cmd-ldrsc-help = Usage: ldrsc <path> <type>
cmd-ldrsc-help = Usage: {$command} <path> <type>
cmd-rldrsc-desc = Reloads a resource.
cmd-rldrsc-help = Usage: rldrsc <path> <type>
cmd-rldrsc-help = Usage: {$command} <path> <type>
cmd-gridtc-desc = Gets the tile count of a grid.
cmd-gridtc-help = Usage: gridtc <gridId>
cmd-gridtc-help = Usage: {$command} <gridId>
# Client-side commands
cmd-guidump-desc = Dump GUI tree to /guidump.txt in user data.
cmd-guidump-help = Usage: guidump
cmd-guidump-help = Usage: {$command}
cmd-uitest-desc = Open a dummy UI testing window
cmd-uitest-help = Usage: uitest
cmd-uitest-desc = Open a dummy UI testing window.
cmd-uitest-help = Usage: {$command}
## 'uitest2' command
cmd-uitest2-desc = Opens a UI control testing OS window
cmd-uitest2-help = Usage: uitest2 <tab>
cmd-uitest2-desc = Opens a UI control testing OS window.
cmd-uitest2-help = Usage: {$command} <tab>
cmd-uitest2-arg-tab = <tab>
cmd-uitest2-error-args = Expected at most one argument
cmd-uitest2-error-tab = Invalid tab: '{$value}'
cmd-uitest2-title = UITest2
cmd-setclipboard-desc = Sets the system clipboard
cmd-setclipboard-help = Usage: setclipboard <text>
cmd-setclipboard-desc = Sets the system clipboard.
cmd-setclipboard-help = Usage: {$command} <text>
cmd-getclipboard-desc = Gets the system clipboard
cmd-getclipboard-help = Usage: Getclipboard
cmd-getclipboard-desc = Gets the system clipboard.
cmd-getclipboard-help = Usage: {$command}
cmd-togglelight-desc = Toggles light rendering.
cmd-togglelight-help = Usage: togglelight
cmd-togglelight-help = Usage: {$command}
cmd-togglefov-desc = Toggles fov for client.
cmd-togglefov-help = Usage: togglefov
cmd-togglefov-help = Usage: {$command}
cmd-togglehardfov-desc = Toggles hard fov for client. (for debugging space-station-14#2353)
cmd-togglehardfov-help = Usage: togglehardfov
cmd-togglehardfov-help = Usage: {$command}
cmd-toggleshadows-desc = Toggles shadow rendering.
cmd-toggleshadows-help = Usage: toggleshadows
cmd-toggleshadows-help = Usage: {$command}
cmd-togglelightbuf-desc = Toggles lighting rendering. This includes shadows but not FOV.
cmd-togglelightbuf-help = Usage: togglelightbuf
cmd-togglelightbuf-help = Usage: {$command}
cmd-chunkinfo-desc = Gets info about a chunk under your mouse cursor.
cmd-chunkinfo-help = Usage: chunkinfo
cmd-chunkinfo-help = Usage: {$command}
cmd-rldshader-desc = Reloads all shaders.
cmd-rldshader-help = Usage: rldshader
cmd-rldshader-help = Usage: {$command}
cmd-cldbglyr-desc = Toggle fov and light debug layers.
cmd-cldbglyr-help= Usage: cldbglyr <layer>: Toggle <layer>
cmd-cldbglyr-help= Usage: {$command} <layer>: Toggle <layer>
cldbglyr: Turn all Layers off
cmd-key-info-desc = Keys key info for a key.
cmd-key-info-help = Usage: keyinfo <Key>
cmd-key-info-help = Usage: {$command} <Key>
## 'bind' command
cmd-bind-desc = Binds an input key combination to an input command.
cmd-bind-help = Usage: bind { cmd-bind-arg-key } { cmd-bind-arg-mode } { cmd-bind-arg-command }
cmd-bind-help = Usage: {$command} { cmd-bind-arg-key } { cmd-bind-arg-mode } { cmd-bind-arg-command }
Note that this DOES NOT automatically save bindings.
Use the 'svbind' command to save binding configuration.
@@ -251,310 +256,322 @@ cmd-bind-arg-mode = <BindMode>
cmd-bind-arg-command = <InputCommand>
cmd-net-draw-interp-desc = Toggles the debug drawing of the network interpolation.
cmd-net-draw-interp-help = Usage: net_draw_interp
cmd-net-draw-interp-help = Usage: {$command}
cmd-net-watch-ent-desc = Dumps all network updates for an EntityId to the console.
cmd-net-watch-ent-help = Usage: net_watchent <0|EntityUid>
cmd-net-watch-ent-help = Usage: {$command} <0|EntityUid>
cmd-net-refresh-desc = Requests a full server state.
cmd-net-refresh-help = Usage: net_refresh
cmd-net-refresh-help = Usage: {$command}
cmd-net-entity-report-desc = Toggles the net entity report panel.
cmd-net-entity-report-help = Usage: net_entityreport
cmd-net-entity-report-help = Usage: {$command}
cmd-fill-desc = Fill up the console for debugging.
cmd-fill-help = Fills the console with some nonsense for debugging.
cmd-fill-help = Usage: {$command}
Fills the console with some nonsense for debugging.
cmd-cls-desc = Clears the console.
cmd-cls-help = Clears the debug console of all messages.
cmd-cls-help = Usage: {$command}
Clears the debug console of all messages.
cmd-sendgarbage-desc = Sends garbage to the server.
cmd-sendgarbage-help = The server will reply with 'no u'
cmd-sendgarbage-help = Usage: {$command}
The server will reply with 'no u'
cmd-loadgrid-desc = Loads a grid from a file into an existing map.
cmd-loadgrid-help = loadgrid <MapID> <Path> [x y] [rotation] [storeUids]
cmd-loadgrid-help = Usage: {$command} <MapID> <Path> [x y] [rotation] [storeUids]
cmd-loc-desc = Prints the absolute location of the player's entity to console.
cmd-loc-help = loc
cmd-loc-help = Usage: {$command}
cmd-tpgrid-desc = Teleports a grid to a new location.
cmd-tpgrid-help = tpgrid <gridId> <X> <Y> [<MapId>]
cmd-tpgrid-help = Usage: {$command} <gridId> <X> <Y> [<MapId>]
cmd-rmgrid-desc = Removes a grid from a map. You cannot remove the default grid.
cmd-rmgrid-help = rmgrid <gridId>
cmd-rmgrid-help = Usage: {$command} <gridId>
cmd-mapinit-desc = Runs map init on a map.
cmd-mapinit-help = mapinit <mapID>
cmd-mapinit-help = Usage: {$command} <mapID>
cmd-lsmap-desc = Lists maps.
cmd-lsmap-help = lsmap
cmd-lsmap-help = Usage: {$command}
cmd-lsgrid-desc = Lists grids.
cmd-lsgrid-help = lsgrid
cmd-lsgrid-help = Usage: {$command}
cmd-addmap-desc = Adds a new empty map to the round. If the mapID already exists, this command does nothing.
cmd-addmap-help = addmap <mapID> [initialize]
cmd-addmap-help = Usage: {$command} <mapID> [pre-init]
cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace.
cmd-rmmap-help = rmmap <mapId>
cmd-rmmap-help = Usage: {$command} <mapId>
cmd-savegrid-desc = Serializes a grid to disk.
cmd-savegrid-help = savegrid <gridID> <Path>
cmd-savegrid-help = Usage: {$command} <gridID> <Path>
cmd-testbed-desc = Loads a physics testbed on the specified map.
cmd-testbed-help = testbed <mapid> <test>
cmd-testbed-help = Usage: {$command} <mapid> <test>
## 'flushcookies' command
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
## 'addcomp' command
cmd-addcomp-desc = Adds a component to an entity.
cmd-addcomp-help = addcomp <uid> <componentName>
cmd-addcomp-help = Usage: {$command} <uid> <componentName>
cmd-addcompc-desc = Adds a component to an entity on the client.
cmd-addcompc-help = addcompc <uid> <componentName>
cmd-addcompc-help = Usage: {$command} <uid> <componentName>
## 'rmcomp' command
cmd-rmcomp-desc = Removes a component from an entity.
cmd-rmcomp-help = rmcomp <uid> <componentName>
cmd-rmcomp-help = Usage: {$command} <uid> <componentName>
cmd-rmcompc-desc = Removes a component from an entity on the client.
cmd-rmcompc-help = rmcomp <uid> <componentName>
cmd-rmcompc-help = Usage: {$command} <uid> <componentName>
## 'addview' command
cmd-addview-desc = Allows you to subscribe to an entity's view for debugging purposes.
cmd-addview-help = addview <entityUid>
cmd-addview-help = Usage: {$command} <entityUid>
cmd-addviewc-desc = Allows you to subscribe to an entity's view for debugging purposes.
cmd-addviewc-help = addview <entityUid>
cmd-addviewc-help = Usage: {$command} <entityUid>
## 'removeview' command
cmd-removeview-desc = Allows you to unsubscribe to an entity's view for debugging purposes.
cmd-removeview-help = removeview <entityUid>
cmd-removeview-help = Usage: {$command} <entityUid>
## 'loglevel' command
cmd-loglevel-desc = Changes the log level for a provided sawmill.
cmd-loglevel-help = Usage: loglevel <sawmill> <level>
cmd-loglevel-help = Usage: {$command} <sawmill> <level>
sawmill: A label prefixing log messages. This is the one you're setting the level for.
level: The log level. Must match one of the values of the LogLevel enum.
cmd-testlog-desc = Writes a test log to a sawmill.
cmd-testlog-help = Usage: testlog <sawmill> <level> <message>
cmd-testlog-help = Usage: {$command} <sawmill> <level> <message>
sawmill: A label prefixing the logged message.
level: The log level. Must match one of the values of the LogLevel enum.
message: The message to be logged. Wrap this in double quotes if you want to use spaces.
## 'vv' command
cmd-vv-desc = Opens View Variables.
cmd-vv-help = Usage: vv <entity ID|IoC interface name|SIoC interface name>
cmd-vv-help = Usage: {$command} <entity ID|IoC interface name|SIoC interface name>
## 'showvelocities' command
cmd-showvelocities-desc = Displays your angular and linear velocities.
cmd-showvelocities-help = Usage: showvelocities
cmd-showvelocities-help = Usage: {$command}
## 'setinputcontext' command
cmd-setinputcontext-desc = Sets the active input context.
cmd-setinputcontext-help = Usage: setinputcontext <context>
cmd-setinputcontext-help = Usage: {$command} <context>
## 'forall' command
cmd-forall-desc = Runs a command over all entities with a given component.
cmd-forall-help = Usage: forall <bql query> do <command...>
cmd-forall-help = Usage: {$command} <bql query> do <command...>
## 'delete' command
cmd-delete-desc = Deletes the entity with the specified ID.
cmd-delete-help = delete <entity UID>
cmd-delete-help = Usage: {$command} <entity UID>
# System commands
cmd-showtime-desc = Shows the server time.
cmd-showtime-help = showtime
cmd-showtime-help = Usage: {$command}
cmd-restart-desc = Gracefully restarts the server (not just the round).
cmd-restart-help = restart
cmd-restart-help = Usage: {$command}
cmd-shutdown-desc = Gracefully shuts down the server.
cmd-shutdown-help = shutdown
cmd-shutdown-help = Usage: {$command}
cmd-saveconfig-desc = Saves the server configuration to the config file.
cmd-saveconfig-help = saveconfig
cmd-saveconfig-help = Usage: {$command}
cmd-netaudit-desc = Prints into about NetMsg security.
cmd-netaudit-help = netaudit
cmd-netaudit-help = Usage: {$command}
# Player commands
cmd-tp-desc = Teleports a player to any location in the round.
cmd-tp-help = tp <x> <y> [<mapID>]
cmd-tp-help = Usage: {$command} <x> <y> [<mapID>]
cmd-tpto-desc = Teleports the current player or the specified players/entities to the location of the first player/entity.
cmd-tpto-help = tpto <username|uid> [username|NetEntity]...
cmd-tpto-help = Usage: {$command} <username|uid> [username|NetEntity]...
cmd-tpto-destination-hint = destination (NetEntity or username)
cmd-tpto-victim-hint = entity to teleport (NetEntity or username)
cmd-tpto-parse-error = Cant resolve entity or player: {$str}
cmd-listplayers-desc = Lists all players currently connected.
cmd-listplayers-help = listplayers
cmd-listplayers-help = Usage: {$command}
cmd-kick-desc = Kicks a connected player out of the server, disconnecting them.
cmd-kick-help = kick <PlayerIndex> [<Reason>]
cmd-kick-help = Usage: {$command} <PlayerIndex> [<Reason>]
# Spin command
cmd-spin-desc = Causes an entity to spin. Default entity is the attached player's parent.
cmd-spin-help = spin velocity [drag] [entityUid]
cmd-spin-help = Usage: {$command} velocity [drag] [entityUid]
# Localization command
cmd-rldloc-desc = Reloads localization (client & server).
cmd-rldloc-help = Usage: rldloc
cmd-rldloc-help = Usage: {$command}
# Debug entity controls
cmd-spawn-desc = Spawns an entity with specific type.
cmd-spawn-help = spawn <prototype> OR spawn <prototype> <relative entity ID> OR spawn <prototype> <x> <y>
cmd-spawn-help = Usage: {$command} <prototype> | {$command} <prototype> <relative entity ID> | {$command} <prototype> <x> <y>
cmd-cspawn-desc = Spawns a client-side entity with specific type at your feet.
cmd-cspawn-help = cspawn <entity type>
cmd-scale-desc = Increases or decreases an entity's size naively.
cmd-scale-help = scale <entityUid> <float>
cmd-cspawn-help = Usage: {$command} <entity type>
cmd-dumpentities-desc = Dump entity list.
cmd-dumpentities-help = Dumps entity list of UIDs and prototype.
cmd-dumpentities-help = Usage: {$command}
Dumps entity list of UIDs and prototype.
cmd-getcomponentregistration-desc = Gets component registration information.
cmd-getcomponentregistration-help = Usage: getcomponentregistration <componentName>
cmd-getcomponentregistration-help = Usage: {$command} <componentName>
cmd-showrays-desc = Toggles debug drawing of physics rays. An integer for <raylifetime> must be provided.
cmd-showrays-help = Usage: showrays <raylifetime>
cmd-showrays-help = Usage: {$command} <raylifetime>
cmd-disconnect-desc = Immediately disconnect from the server and go back to the main menu.
cmd-disconnect-help = Usage: disconnect
cmd-disconnect-help = Usage: {$command}
cmd-entfo-desc = Displays verbose diagnostics for an entity.
cmd-entfo-help = Usage: entfo <entityuid>
cmd-entfo-help = Usage: {$command} <entityuid>
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
cmd-fuck-desc = Throws an exception
cmd-fuck-help = Throws an exception
cmd-fuck-desc = Throws an exception.
cmd-fuck-help = Usage: {$command}
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
cmd-showpos-help = Usage: showpos
cmd-showpos-desc = Show the position of all entities on the screen.
cmd-showpos-help = Usage: {$command}
cmd-showrot-desc = Show the rotation of all entities on the screen.
cmd-showrot-help = Usage: {$command}
cmd-showvel-desc = Show the local velocity of all entites on the screen.
cmd-showvel-help = Usage: {$command}
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
cmd-showangvel-help = Usage: {$command}
cmd-sggcell-desc = Lists entities on a snap grid cell.
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
cmd-sggcell-help = Usage: {$command} <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
cmd-overrideplayername-desc = Changes the name used when attempting to connect to the server.
cmd-overrideplayername-help = Usage: overrideplayername <name>
cmd-overrideplayername-help = Usage: {$command} <name>
cmd-showanchored-desc = Shows anchored entities on a particular tile
cmd-showanchored-help = Usage: showanchored
cmd-showanchored-desc = Shows anchored entities on a particular tile.
cmd-showanchored-help = Usage: {$command}
cmd-dmetamem-desc = Dumps a type's members in a format suitable for the sandbox configuration file.
cmd-dmetamem-help = Usage: dmetamem <type>
cmd-dmetamem-help = Usage: {$command} <type>
cmd-launchauth-desc = Load authentication tokens from launcher data to aid in testing of live servers.
cmd-launchauth-help = Usage: launchauth <account name>
cmd-launchauth-help = Usage: {$command} <account name>
cmd-lightbb-desc = Toggles whether to show light bounding boxes.
cmd-lightbb-help = Usage: lightbb
cmd-lightbb-help = Usage: {$command}
cmd-monitorinfo-desc = Monitors info
cmd-monitorinfo-help = Usage: monitorinfo <id>
cmd-monitorinfo-desc = Monitors info.
cmd-monitorinfo-help = Usage: {$command} <id>
cmd-setmonitor-desc = Set monitor
cmd-setmonitor-help = Usage: setmonitor <id>
cmd-setmonitor-desc = Set monitor.
cmd-setmonitor-help = Usage: {$command} <id>
cmd-physics-desc = Shows a debug physics overlay. The arg supplied specifies the overlay.
cmd-physics-help = Usage: physics <aabbs / com / contactnormals / contactpoints / distance / joints / shapeinfo / shapes>
cmd-physics-help = Usage: {$command} <aabbs / com / contactnormals / contactpoints / distance / joints / shapeinfo / shapes>
cmd-hardquit-desc = Kills the game client instantly.
cmd-hardquit-help = Kills the game client instantly, leaving no traces. No telling the server goodbye.
cmd-hardquit-help = Usage: {$command}
Kills the game client instantly, leaving no traces. No telling the server goodbye.
cmd-quit-desc = Shuts down the game client gracefully.
cmd-quit-help = Properly shuts down the game client, notifying the connected server and such.
cmd-quit-help = Usage: {$command}
Properly shuts down the game client, notifying the connected server and such.
cmd-csi-desc = Opens a C# interactive console.
cmd-csi-help = Usage: csi
cmd-csi-help = Usage: {$command}
cmd-scsi-desc = Opens a C# interactive console on the server.
cmd-scsi-help = Usage: scsi
cmd-scsi-help = Usage: {$command}
cmd-watch-desc = Opens a variable watch window.
cmd-watch-help = Usage: watch
cmd-watch-help = Usage: {$command}
cmd-showspritebb-desc = Toggle whether sprite bounds are shown
cmd-showspritebb-help = Usage: showspritebb
cmd-showspritebb-desc = Toggle whether sprite bounds are shown.
cmd-showspritebb-help = Usage: {$command}
cmd-togglelookup-desc = Shows / hides entitylookup bounds via an overlay.
cmd-togglelookup-help = Usage: togglelookup
cmd-togglelookup-help = Usage: {$command}
cmd-net_entityreport-desc = Toggles the net entity report panel.
cmd-net_entityreport-help = Usage: net_entityreport
cmd-net_entityreport-help = Usage: {$command}
cmd-net_refresh-desc = Requests a full server state.
cmd-net_refresh-help = Usage: net_refresh
cmd-net_refresh-help = Usage: {$command}
cmd-net_graph-desc = Toggles the net statistics panel.
cmd-net_graph-help = Usage: net_graph
cmd-net_graph-help = Usage: {$command}
cmd-net_watchent-desc = Dumps all network updates for an EntityId to the console.
cmd-net_watchent-help = Usage: net_watchent <0|EntityUid>
cmd-net_watchent-help = Usage: {$command} <0|EntityUid>
cmd-net_draw_interp-desc = Toggles the debug drawing of the network interpolation.
cmd-net_draw_interp-help = Usage: net_draw_interp <0|EntityUid>
cmd-net_draw_interp-help = Usage: {$command} <0|EntityUid>
cmd-vram-desc = Displays video memory usage statics by the game.
cmd-vram-help = Usage: vram
cmd-vram-help = Usage: {$command}
cmd-showislands-desc = Shows the current physics bodies involved in each physics island.
cmd-showislands-help = Usage: showislands
cmd-showislands-help = Usage: {$command}
cmd-showgridnodes-desc = Shows the nodes for grid split purposes.
cmd-showgridnodes-help = Usage: showgridnodes
cmd-showgridnodes-help = Usage: {$command}
cmd-profsnap-desc = Make a profiling snapshot.
cmd-profsnap-help = Usage: profsnap
cmd-profsnap-help = Usage: {$command}
cmd-devwindow-desc = Dev Window
cmd-devwindow-help = Usage: devwindow
cmd-devwindow-desc = Dev Window.
cmd-devwindow-help = Usage: {$command}
cmd-scene-desc = Immediately changes the UI scene/state.
cmd-scene-help = Usage: scene <className>
cmd-scene-help = Usage: {$command} <className>
cmd-szr_stats-desc = Report serializer statistics.
cmd-szr_stats-help = Usage: szr_stats
cmd-szr_stats-help = Usage: {$command}
cmd-hwid-desc = Returns the current HWID (HardWare ID).
cmd-hwid-help = Usage: hwid
cmd-hwid-help = Usage: {$command}
cmd-vvread-desc = Retrieve a path's value using VV (View Variables).
cmd-vvread-help = Usage: vvread <path>
cmd-vvread-help = Usage: {$command} <path>
cmd-vvwrite-desc = Modify a path's value using VV (View Variables).
cmd-vvwrite-help = Usage: vvwrite <path>
cmd-vvwrite-help = Usage: {$command} <path>
cmd-vvinvoke-desc = Invoke/Call a path with arguments using VV.
cmd-vvinvoke-help = Usage: vvinvoke <path> [arguments...]
cmd-vvinvoke-help = Usage: {$command} <path> [arguments...]
cmd-dump_dependency_injectors-desc = Dump IoCManager's dependency injector cache.
cmd-dump_dependency_injectors-help = Usage: dump_dependency_injectors
cmd-dump_dependency_injectors-help = Usage: {$command}
cmd-dump_dependency_injectors-total-count = Total count: { $total }
cmd-dump_netserializer_type_map-desc = Dump NetSerializer's type map and serializer hash.
cmd-dump_netserializer_type_map-help = Usage: dump_netserializer_type_map
cmd-dump_netserializer_type_map-help = Usage: {$command}
cmd-hub_advertise_now-desc = Immediately advertise to the master hub server
cmd-hub_advertise_now-help = Usage: hub_advertise_now
cmd-hub_advertise_now-desc = Immediately advertise to the master hub server.
cmd-hub_advertise_now-help = Usage: {$command}
cmd-echo-desc = Echo arguments back to the console
cmd-echo-help = Usage: echo "<message>"
cmd-echo-desc = Echo arguments back to the console.
cmd-echo-help = Usage: {$command} "<message>"
## 'vfs_ls' command
cmd-vfs_ls-desc = List directory contents in the VFS.
cmd-vfs_ls-help = Usage: vfs_list <path>
cmd-vfs_ls-help = Usage: {$command} <path>
Example:
vfs_list /Assemblies
cmd-vfs_ls-err-args = Need exactly 1 argument.
cmd-vfs_ls-hint-path = <path>
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites
cmd-reloadtiletextures-help = Usage: reloadtiletextures
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites.
cmd-reloadtiletextures-help = Usage: {$command}
cmd-audio_length-desc = Shows the length of an audio file
cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
cmd-audio_length-help = Usage: {$command} { cmd-audio_length-arg-file-name }
cmd-audio_length-arg-file-name = <file name>
## PVS
@@ -562,3 +579,10 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager.
cmd-localization_set_culture-help = Usage: {$command} <cultureName>
cmd-localization_set_culture-culture-name = <cultureName>
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })
cmd-addmap-hint-2 = runMapInit [true / false]

View File

@@ -8,3 +8,5 @@ color-selector-sliders-alpha = A
color-selector-sliders-rgb = RGB
color-selector-sliders-hsv = HSV
option-button-filter = Filter

View File

@@ -1,19 +1,24 @@
## EntitySpawnWindow
entity-spawn-window-title = Entity Spawn Panel
entity-spawn-window-search-bar-placeholder = search
entity-spawn-window-clear-button = Clear
entity-spawn-window-replace-button-text = Replace
entity-spawn-window-override-menu-tooltip = Override placement
## TileSpawnWindow
tile-spawn-window-title = Place Tiles
tile-spawn-window-mirror-button-text = Mirror Tiles
## Console
console-line-edit-placeholder = Command Here
## OutputPanel
output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode
window-search-bar-placeholder = Search
window-clear-button = Clear

View File

@@ -0,0 +1,25 @@
## "Textures" dev window tab
dev-window-tab-textures-title = Textures
dev-window-tab-textures-reload = Reload
dev-window-tab-textures-filter = Filter
dev-window-tab-textures-summary = Total (est): { $bytes }
dev-window-tab-textures-info = Width: { $width } Height: { $height }
PixelType: { $pixelType } sRGB: { $srgb }
Name: { $name }
Est. memory usage: { $bytes }
## "Render Targets" dev window tab
dev-window-tab-render-targets-title = Render Targets
dev-window-tab-render-targets-reload = Reload
dev-window-tab-render-targets-filter = Filter
dev-window-tab-render-targets-column-id = ID
dev-window-tab-render-targets-column-name = Name
dev-window-tab-render-targets-column-size = Size
dev-window-tab-render-targets-column-type = Type
dev-window-tab-render-targets-column-vram = VRAM
dev-window-tab-render-targets-column-thumbnail = Thumbnail
dev-window-tab-render-targets-value-null = null
dev-window-tab-render-targets-value-not-available = Not available
dev-window-tab-render-targets-summary = Total VRAM: { $vram }

View File

@@ -2,6 +2,7 @@ input-key-Escape = Escape
input-key-Control = Control
input-key-Shift = Shift
input-key-Alt = Alt
input-key-Alt-mac = ⌥
input-key-Menu = Menu
input-key-F1 = F1
input-key-F2 = F2
@@ -70,8 +71,8 @@ input-key-MouseButton9 = Mouse 9
input-key-LSystem-win = Left Win
input-key-RSystem-win = Right Win
input-key-LSystem-mac = Left Cmd
input-key-RSystem-mac = Right Cmd
input-key-LSystem-mac = Left
input-key-RSystem-mac = Right
input-key-LSystem-linux = Left Meta
input-key-RSystem-linux = Right Meta

View File

@@ -42,8 +42,7 @@ command-description-as =
command-description-count =
Counts the amount of entries in it's input, returning an integer.
command-description-map =
Maps the input over the given block, with the provided expected return type.
This command may be modified to not need an explicit return type in the future.
Maps the input over the given block.
command-description-select =
Selects N objects or N% of objects from the input.
One can additionally invert this command with not to make it select everything except N objects instead.
@@ -149,7 +148,7 @@ command-description-max =
Returns the maximum of two values.
command-description-BitAndCommand =
Performs bitwise AND.
command-description-BitOrCommand =
command-description-bitor =
Performs bitwise OR.
command-description-BitXorCommand =
Performs bitwise XOR.
@@ -196,6 +195,8 @@ command-description-spawn-at =
Spawns an entity at the given coordinates.
command-description-spawn-on =
Spawns an entity on the given entity, at it's coordinates.
command-description-spawn-in =
Spawns an entity in the given container on the given entity, dropping it at its coordinates if it doesn't fit
command-description-spawn-attached =
Spawns an entity attached to the given entity, at (0 0) relative to it.
command-description-mappos =
@@ -203,11 +204,11 @@ command-description-mappos =
command-description-pos =
Returns an entity's coordinates.
command-description-tp-coords =
Teleports the target to the given coordinates.
Teleports the given entities to the target coordinates.
command-description-tp-to =
Teleports the target to the given other entity.
Teleports the given entities to the target entity.
command-description-tp-into =
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
command-description-comp-get =
Gets the given component from the given entity.
command-description-comp-add =
@@ -277,7 +278,7 @@ command-description-ModVecCommand =
Performs the modulus operation over the input with the given constant right-hand value.
command-description-BitAndNotCommand =
Performs bitwise AND-NOT over the input.
command-description-BitOrNotCommand =
command-description-bitornot =
Performs bitwise OR-NOT over the input.
command-description-BitXnorCommand =
Performs bitwise XNOR over the input.
@@ -427,3 +428,7 @@ command-description-cmd-info =
On its own, this means it'll print the command's help message.
command-description-comp-rm =
Removes the given component from the entity.
command-description-overlay-toggle = Toggle an overlay on or off
command-description-overlay-add = Add an overlay (if it does not already exist)
command-description-overlay-remove = Remove an overlay

View File

@@ -25,3 +25,9 @@ vv-sound-reference-distance = Reference Distance
vv-sound-loop = Loop
vv-sound-play-offset = Play Offset (s)
vv-sound-variation = Pitch variation
## ProtoId
vv-protoid-id-placeholder = Prototype ID
vv-protoid-select-button-label = Select
vv-protoid-addwindow-title = Set Prototype

View File

@@ -136,6 +136,7 @@ cmd-savemap-not-exist = O mapa de destino não existe.
cmd-savemap-init-warning = Tentativa de salvar um mapa pós-inicialização sem forçar o salvamento.
cmd-savemap-attempt = Tentando salvar o mapa {$mapId} em {$path}.
cmd-savemap-success = Mapa salvo com sucesso.
cmd-savemap-error = Não foi possível salvar o mapa! Consulte o log do servidor para obter detalhes.
cmd-hint-savemap-id = <MapID>
cmd-hint-savemap-path = <Path>
cmd-hint-savemap-force = [bool]

View File

@@ -14,6 +14,8 @@ uniform highp vec2 lightCenter;
uniform highp float lightRange;
uniform highp float lightPower;
uniform highp float lightSoftness;
uniform highp float lightFalloff;
uniform highp float lightCurveFactor;
uniform highp float lightIndex;
uniform sampler2D shadowMap;
@@ -47,8 +49,15 @@ void fragment()
discard;
}
highp float dist = dot(diff, diff) + LIGHTING_HEIGHT;
highp float val = clamp((1.0 - clamp(sqrt(dist) / lightRange, 0.0, 1.0)) * (1.0 / (sqrt(dist + 1.0))), 0.0, 1.0);
// this implementation of light attenuation primarily adapted from
// https://lisyarus.github.io/blog/posts/point-light-attenuation.html
highp float sqr_dist = dot(diff, diff) + LIGHTING_HEIGHT;
highp float s = clamp(sqrt(sqr_dist) / lightRange, 0.0, 1.0);
highp float s2 = s * s;
// controls curve by lerping between two variants (inverse-shape and inversequadratic-shape)
highp float curveFactor = mix(s, s2, clamp(lightCurveFactor, 0.0, 1.0));
highp float val = clamp(((1.0 - s2) * (1.0 - s2)) / (1.0 + lightFalloff * curveFactor), 0.0, 1.0);
val *= lightPower;
val *= mask;

View File

@@ -0,0 +1,110 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.AfterAutoHandleStateAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture, TestOf(typeof(AfterAutoHandleStateAnalyzer))]
public sealed class AfterAutoHandleStateAnalyzerTest
{
private const string SubscribeEventDef = """
using System;
namespace Robust.Shared.GameObjects;
public readonly struct EntityUid;
public abstract class EntitySystem
{
public void SubscribeLocalEvent<T, TEvent>() where TEvent : notnull { }
}
public interface IComponent;
public interface IComponentState;
""";
// A rare case for block-scoped namespace, I thought. Then I realized this
// only needed the one type definition.
private const string OtherTypeDefs = """
using System;
namespace JetBrains.Annotations
{
public sealed class BaseTypeRequiredAttribute(Type baseType) : Attribute;
}
""";
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<AfterAutoHandleStateAnalyzer, DefaultVerifier>
{
TestState = { Sources = { code } }
};
TestHelper.AddEmbeddedSources(test.TestState,
"Robust.Shared.Analyzers.ComponentNetworkGeneratorAuxiliary.cs",
"Robust.Shared.GameObjects.EventBusAttributes.cs");
test.TestState.Sources.Add(("EntitySystem.Subscriptions.cs", SubscribeEventDef));
test.TestState.Sources.Add(("Types.cs", OtherTypeDefs));
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
[AutoGenerateComponentState(true)]
public sealed class AutoGenTrue;
[AutoGenerateComponentState(true, true)]
public sealed class AutoGenTrueTrue;
public sealed class NotAutoGen;
[AutoGenerateComponentState]
public sealed class AutoGenNoArgs;
[AutoGenerateComponentState(false)]
public sealed class AutoGenFalse;
public sealed class Foo : EntitySystem
{
public void Good()
{
// Subscribing to other events works
SubscribeLocalEvent<AutoGenNoArgs, object>();
// First arg true allows subscribing
SubscribeLocalEvent<AutoGenTrue, AfterAutoHandleStateEvent>();
SubscribeLocalEvent<AutoGenTrueTrue, AfterAutoHandleStateEvent>();
}
public void Bad()
{
// Can't subscribe if AutoGenerateComponentState isn't even present
SubscribeLocalEvent<NotAutoGen, AfterAutoHandleStateEvent>();
// Can't subscribe if first arg is not specified/false
SubscribeLocalEvent<AutoGenNoArgs, AfterAutoHandleStateEvent>();
SubscribeLocalEvent<AutoGenFalse, AfterAutoHandleStateEvent>();
}
}
""";
await Verifier(code,
// /0/Test0.cs(29,9): error RA0040: Tried to subscribe to AfterAutoHandleStateEvent for 'NotAutoGen' which doesn't have an AutoGenerateComponentState attribute
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttribute).WithSpan(29, 9, 29, 69).WithArguments("NotAutoGen"),
// /0/Test0.cs(32,9): error RA0041: Tried to subscribe to AfterAutoHandleStateEvent for 'AutoGenNoArgs' which doesn't have raiseAfterAutoHandleState set
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttributeParam).WithSpan(32, 9, 32, 72).WithArguments("AutoGenNoArgs"),
// /0/Test0.cs(33,9): error RA0041: Tried to subscribe to AfterAutoHandleStateEvent for 'AutoGenFalse' which doesn't have raiseAfterAutoHandleState set
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttributeParam).WithSpan(33, 9, 33, 71).WithArguments("AutoGenFalse")
);
}
}

View File

@@ -1,4 +1,4 @@
extern alias SerializationGenerator;
extern alias SerializationGenerator;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
@@ -126,6 +126,48 @@ public sealed class ComponentPauseGeneratorTest
""");
}
[Test]
public void TestDictionary()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public Dictionary<string, TimeSpan> Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
foreach (var key in component.Foo.Keys)
component.Foo[key] += args.PausedTime;
}
}
}
""");
}
[Test]
public void TestAutoState()
{

View File

@@ -21,47 +21,53 @@ public sealed class DataDefinitionAnalyzerTest
},
};
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System;
namespace Robust.Shared.ViewVariables
{
public sealed class ViewVariablesAttribute : Attribute
{
public readonly VVAccess Access = VVAccess.ReadOnly;
public ViewVariablesAttribute() { }
public ViewVariablesAttribute(VVAccess access)
{
Access = access;
}
}
public enum VVAccess : byte
{
ReadOnly = 0,
ReadWrite = 1,
}
}
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute(string? tag = null) : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
public sealed class NotYamlSerializableAttribute : Attribute;
}
""";
[Test]
public async Task Test()
public async Task NoVVReadOnlyTest()
{
const string code = """
using System;
using Robust.Shared.ViewVariables;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.ViewVariables
{
public sealed class ViewVariablesAttribute : Attribute
{
public readonly VVAccess Access = VVAccess.ReadOnly;
public ViewVariablesAttribute() { }
public ViewVariablesAttribute(VVAccess access)
{
Access = access;
}
}
public enum VVAccess : byte
{
ReadOnly = 0,
ReadWrite = 1,
}
}
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
@@ -83,8 +89,168 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(35,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
// /0/Test0.cs(7,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(7, 17, 7, 50).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyFieldTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public readonly int Bad;
[DataField]
public int Good;
}
""";
await Verifier(code,
// /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task PartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed class Foo { }
""";
await Verifier(code,
// /0/Test0.cs(4,15): error RA0017: Type Foo is a DataDefinition but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataDefinitionPartialRule).WithSpan(4, 15, 4, 20).WithArguments("Foo")
);
}
[Test]
public async Task NestedPartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
public sealed class Foo
{
[DataDefinition]
public sealed partial class Nested { }
}
""";
await Verifier(code,
// /0/Test0.cs(3,15): error RA0018: Type Foo contains nested data definition Nested but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.NestedDataDefinitionPartialRule).WithSpan(3, 15, 3, 20).WithArguments("Foo", "Nested")
);
}
[Test]
public async Task RedundantDataFieldTagTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField("someValue")]
public int SomeValue;
}
""";
await Verifier(code,
// /0/Test0.cs(6,6): info RA0027: Data field SomeValue in data definition Foo has an explicitly set tag that matches autogenerated tag
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldRedundantTagRule).WithSpan(6, 6, 6, 28).WithArguments("SomeValue", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public int Bad { get; }
[DataField]
public int Good { get; private set; }
}
""";
await Verifier(code,
// /0/Test0.cs(7,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(7, 20, 7, 28).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task NotYamlSerializableTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[NotYamlSerializable]
public sealed class NotSerializableClass { }
[NotYamlSerializable]
public readonly struct NotSerializableStruct { }
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public NotSerializableClass BadField;
[DataField]
public NotSerializableClass BadProperty { get; set; }
[DataField]
public NotSerializableClass? BadNullableField;
[DataField]
public NotSerializableStruct BadStructField;
[DataField]
public NotSerializableStruct BadStructProperty { get; set; }
[DataField]
public NotSerializableStruct? BadNullableStructField;
[DataField]
public NotSerializableStruct? BadNullableStructProperty { get; set; }
public NotSerializableClass GoodField; // Not a DataField, not a problem
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
}
""";
await Verifier(code,
// /0/Test0.cs(12,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(12, 12, 12, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(15,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(15, 12, 15, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass"),
// /0/Test0.cs(18,12): error RA0036: Data field BadNullableField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(18, 12, 18, 33).WithArguments("BadNullableField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(21,12): error RA0036: Data field BadStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(21, 12, 21, 33).WithArguments("BadStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(24,12): error RA0036: Data field BadStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(24, 12, 24, 33).WithArguments("BadStructProperty", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(27,12): error RA0036: Data field BadNullableStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(27, 12, 27, 34).WithArguments("BadNullableStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(30,12): error RA0036: Data field BadNullableStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(30, 12, 30, 34).WithArguments("BadNullableStructProperty", "Foo", "NotSerializableStruct")
);
}
}

View File

@@ -0,0 +1,189 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ForbidLiteralAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
);
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System.Collections.Generic;
using Robust.Shared.Analyzers;
public sealed class TestClass
{
public static void OneParameterForbidden([ForbidLiteral] string value) { }
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
}
public record struct StringWrapper(string value)
{
private readonly string _value = value;
public static implicit operator string(StringWrapper wrapper)
{
return wrapper._value;
}
}
""";
[Test]
public async Task TestOneParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.OneParameterForbidden(_constValue);
TestClass.OneParameterForbidden(StaticValue);
TestClass.OneParameterForbidden(WrappedValue);
TestClass.OneParameterForbidden("baz");
}
}
""";
await Verifier(code,
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
);
}
[Test]
public async Task TestTwoParametersFirstForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
public void Test()
{
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
TestClass.TwoParametersFirstForbidden("foo", "whatever");
}
}
""";
await Verifier(code,
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
);
}
[Test]
public async Task TestTwoParametersBothForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
public void Test()
{
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
TestClass.TwoParametersBothForbidden("whatever", _constValue);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
);
}
[Test]
public async Task TestListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
);
}
[Test]
public async Task TestParamsListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
);
}
}

View File

@@ -0,0 +1,86 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ObsoleteInheritanceAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
/// <summary>
/// Analyzer that implements <c>[ObsoleteInheritance]</c> checking, to give obsoletion warnings for inheriting types
/// that should never have been virtual.
/// </summary>
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ObsoleteInheritanceAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ObsoleteInheritanceAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task TestBasic()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.Rule).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base")
);
}
[Test]
public async Task TestMessage()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance("Sus")]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself: "Sus"
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.RuleWithMessage).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base", "Sus")
);
}
[Test]
public async Task TestNormal()
{
const string code = """
public class Base;
public class AllowedAllowed : Base;
""";
await Verifier(code);
}
}

View File

@@ -0,0 +1,64 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeInstantiationAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(PrototypeInstantiationAnalyzer))]
public sealed class PrototypeInstantiationAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new RTAnalyzerTest<PrototypeInstantiationAnalyzer>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Prototypes.Attributes.cs",
"Robust.Shared.Prototypes.IPrototype.cs",
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
[Prototype]
public sealed class FooPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
}
public static class Bad
{
public static FooPrototype Real()
{
return new FooPrototype();
}
}
""";
await Verifier(code,
// /0/Test0.cs(15,16): warning RA0039: Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.
VerifyCS.Diagnostic().WithSpan(15, 16, 15, 34));
}
}

View File

@@ -0,0 +1,61 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeNetSerializableAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(PrototypeNetSerializableAnalyzer))]
public sealed class PrototypeNetSerializableAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new RTAnalyzerTest<PrototypeNetSerializableAnalyzer>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Serialization.NetSerializableAttribute.cs",
"Robust.Shared.Prototypes.Attributes.cs",
"Robust.Shared.Prototypes.IPrototype.cs",
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using System;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
[Prototype]
[Serializable, NetSerializable]
public sealed class FooPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
}
""";
await Verifier(code,
// /0/Test0.cs(7,21): warning RA0037: Type FooPrototype is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleNetSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"),
// /0/Test0.cs(7,21): warning RA0038: Type FooPrototype is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"));
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
namespace Robust.Analyzers.Tests;
public sealed class RTAnalyzerTest<TAnalyzer> : CSharpAnalyzerTest<TAnalyzer, DefaultVerifier>
where TAnalyzer : DiagnosticAnalyzer, new()
{
protected override ParseOptions CreateParseOptions()
{
var baseOptions = (CSharpParseOptions) base.CreateParseOptions();
return baseOptions.WithPreprocessorSymbols("ROBUST_ANALYZERS_TEST");
}
}

View File

@@ -10,11 +10,18 @@
<ItemGroup>
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ComponentNetworkGeneratorAuxiliary.cs" LogicalName="Robust.Shared.Analyzers.ComponentNetworkGeneratorAuxiliary.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Serialization\NetSerializableAttribute.cs" LogicalName="Robust.Shared.Serialization.NetSerializableAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Prototypes\Attributes.cs" LogicalName="Robust.Shared.Prototypes.Attributes.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Prototypes\IPrototype.cs" LogicalName="Robust.Shared.Prototypes.IPrototype.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Serialization\Manager\Attributes\DataFieldAttribute.cs" LogicalName="Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs" LinkBase="Implementations" />
</ItemGroup>
<PropertyGroup>

View File

@@ -0,0 +1,85 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AfterAutoHandleStateAnalyzer : DiagnosticAnalyzer
{
private const string AfterAutoHandleStateEventName = "AfterAutoHandleStateEvent";
private const string AutoGenStateAttribute = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute";
private const string SubscribeLocalEventName = "SubscribeLocalEvent";
public static readonly DiagnosticDescriptor MissingAttribute = new(
Diagnostics.IdAutoGenStateAttributeMissing,
"Unreachable AfterAutoHandleState subscription",
"Tried to subscribe to AfterAutoHandleStateEvent for '{0}' which doesn't have an "
+ "AutoGenerateComponentState attribute",
"Usage",
DiagnosticSeverity.Error,
true,
// Does this even show up anywhere in Rider? >:(
"You must mark your component with '[AutoGenerateComponentState(true)]' to subscribe to this event."
);
public static readonly DiagnosticDescriptor MissingAttributeParam = new(
Diagnostics.IdAutoGenStateParamMissing,
"Unreachable AfterAutoHandleState subscription",
"Tried to subscribe to AfterAutoHandleStateEvent for '{0}' which doesn't have "
+ "raiseAfterAutoHandleState set",
"Usage",
DiagnosticSeverity.Error,
true,
"The AutoGenerateComponentState attribute must be passed 'true' in order to subscribe to this event."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
[MissingAttribute, MissingAttributeParam];
public override void Initialize(AnalysisContext context)
{
// This is more to stop user error rather than code generation error
// (Plus this shouldn't affect code gen anyway)
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(compilationContext =>
{
var autoGenStateAttribute = compilationContext.Compilation.GetTypeByMetadataName(AutoGenStateAttribute);
// No attribute, no analyzer.
if (autoGenStateAttribute is null)
return;
compilationContext.RegisterOperationAction(
analysisContext => CheckEventSubscription(analysisContext, autoGenStateAttribute),
OperationKind.Invocation);
});
}
private static void CheckEventSubscription(OperationAnalysisContext context, ITypeSymbol autoGenStateAttribute)
{
if (context.Operation is not IInvocationOperation operation)
return;
// Check the method has the right name and has the right type args
if (operation.TargetMethod is not
{ Name: SubscribeLocalEventName, TypeArguments: [var component, { Name: AfterAutoHandleStateEventName }] })
return;
// Search the component's attributes for something matching autoGenStateAttribute
AttributeHelper.HasAttribute(component, autoGenStateAttribute, out var autoGenAttribute);
// First argument is raiseAfterAutoHandleState—note it shouldn't ever
// be null, since it has a default, but eh.
if (autoGenAttribute?.ConstructorArguments[0].Value is true)
return;
context.ReportDiagnostic(Diagnostic.Create(autoGenAttribute is null ? MissingAttribute : MissingAttributeParam,
operation.Syntax.GetLocation(),
component.Name));
}
}

View File

@@ -18,10 +18,11 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
private const string DataFieldAttributeName = "DataField";
private const string ViewVariablesAttributeName = "ViewVariables";
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
Diagnostics.IdDataDefinitionPartial,
"Type must be partial",
"Type {0} is a DataDefinition but is not partial",
@@ -31,7 +32,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type that is a data definition as partial."
);
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
Diagnostics.IdNestedDataDefinitionPartial,
"Type must be partial",
"Type {0} contains nested data definition {1} but is not partial",
@@ -41,7 +42,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type containing a nested data definition as partial."
);
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
"Data field {0} in data definition {1} is readonly",
@@ -51,7 +52,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the readonly modifier."
);
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
"Data field property {0} in data definition {1} does not have a setter",
@@ -61,7 +62,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to add a setter."
);
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
Diagnostics.IdDataFieldRedundantTag,
"Data field has redundant tag specified",
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
@@ -81,9 +82,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the ViewVariables attribute."
);
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
Diagnostics.IdDataFieldYamlSerializable,
"Data field type is not YAML serializable",
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to use a type that is YAML serializable."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
);
public override void Initialize(AnalysisContext context)
@@ -91,23 +102,31 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
context.RegisterSymbolStartAction(symbolContext =>
{
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
return;
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
if (!IsDataDefinition(typeSymbol))
return;
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}, SymbolKind.NamedType);
}
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
{
if (context.Node is not TypeDeclarationSyntax declaration)
return;
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
if (!IsDataDefinition(type))
if (context.ContainingSymbol is not INamedTypeSymbol type)
return;
if (!IsPartial(declaration))
@@ -118,7 +137,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
var containingType = type.ContainingType;
while (containingType != null)
{
var containingTypeDeclaration = (TypeDeclarationSyntax) containingType.DeclaringSyntaxReferences[0].GetSyntax();
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
if (!IsPartial(containingTypeDeclaration))
{
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
@@ -128,31 +147,31 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}
}
private void AnalyzeDataField(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataField(SyntaxNodeAnalysisContext context)
{
if (context.Node is not FieldDeclarationSyntax field)
return;
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type))
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
return;
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
if (fieldSymbol == null)
continue;
if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute))
continue;
if (IsReadOnlyDataField(type, fieldSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol))
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
{
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
@@ -163,32 +182,51 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
continue;
fieldTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(fieldTypeSymbol);
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
fieldSymbol.Name,
type.Name,
fieldTypeSymbol.MetadataName
));
}
}
}
private void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
{
if (context.Node is not PropertyDeclarationSyntax property)
return;
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
return;
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type) || type.IsRecord || type.IsValueType)
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
return;
if (type.IsRecord || type.IsValueType)
return;
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(property);
if (propertySymbol == null)
return;
if (!IsDataField(propertySymbol, out _, out var datafieldAttribute))
return;
if (IsReadOnlyDataField(type, propertySymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol))
if (HasRedundantTag(propertySymbol, datafieldAttribute))
{
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
@@ -199,13 +237,25 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
return;
propertyTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(propertyTypeSymbol);
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
propertySymbol.Name,
type.Name,
propertyTypeSymbol.Name
));
}
}
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
{
if (!IsDataField(field, out _, out _))
return false;
return IsReadOnlyMember(type, field);
}
@@ -285,6 +335,20 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
{
foreach (var modifier in syntax.Modifiers)
{
if (modifier.IsKind(modifierKind))
{
location = modifier.GetLocation();
return true;
}
}
location = syntax.GetLocation();
return false;
}
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
{
if (member is IFieldSymbol field)
@@ -316,17 +380,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool HasRedundantTag(ISymbol symbol)
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
{
if (!IsDataField(symbol, out var _, out var attribute))
return false;
// No args, no problem
if (attribute.ConstructorArguments.Length == 0)
if (datafieldAttribute.ConstructorArguments.Length == 0)
return false;
// If a tag is explicitly specified, it will be the first argument...
var tagArgument = attribute.ConstructorArguments[0];
var tagArgument = datafieldAttribute.ConstructorArguments[0];
// ...but the first arg could also something else, since tag is optional
// so we make sure that it's a string
if (tagArgument.Value is not string explicitName)
@@ -341,9 +402,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool HasVVReadWrite(ISymbol symbol)
{
if (!IsDataField(symbol, out _, out _))
return false;
// Make sure it has ViewVariablesAttribute
AttributeData? viewVariablesAttribute = null;
foreach (var attr in symbol.GetAttributes())
@@ -367,6 +425,11 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return (VVAccess)accessByte == VVAccess.ReadWrite;
}
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
{
return HasAttribute(type, NotYamlSerializableName);
}
private static bool IsImplicitDataDefinition(ITypeSymbol type)
{
if (HasAttribute(type, ImplicitDataDefinitionNamespace))

View File

@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
{
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
public static DiagnosticDescriptor ForbidLiteralRule = new(
Diagnostics.IdForbidLiteral,
"Parameter forbids literal values",
"The {0} parameter of {1} forbids literal values",
"Usage",
DiagnosticSeverity.Warning,
true,
"Pass in a validated wrapper type like ProtoId, or a const or static value."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}
private void AnalyzeOperation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation invocationOperation)
return;
// Check each parameter of the method invocation
foreach (var argumentOperation in invocationOperation.Arguments)
{
// Check for our attribute on the parameter
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
continue;
// Handle parameters using the params keyword
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
{
// Check each param value
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
{
CheckArgumentSyntax(context, argumentOperation, subArgument);
}
continue;
}
// Not params, so just check the single parameter
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
continue;
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
}
}
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
{
// Handle collection types
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
{
// Check each value of the collection
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
{
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
continue;
// Check if a literal was passed in
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
continue;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
expressionSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
return;
}
// Not a collection, just a single value to check
// Check if it's a literal
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
return;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
argumentSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
}

View File

@@ -0,0 +1,75 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObsoleteInheritanceAnalyzer : DiagnosticAnalyzer
{
private const string Attribute = "Robust.Shared.Analyzers.ObsoleteInheritanceAttribute";
public static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdObsoleteInheritance,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself",
"Usage",
DiagnosticSeverity.Warning,
true);
public static readonly DiagnosticDescriptor RuleWithMessage = new(
Diagnostics.IdObsoleteInheritanceWithMessage,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself: \"{2}\"",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, RuleWithMessage];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(CheckClass, SymbolKind.NamedType);
}
private static void CheckClass(SymbolAnalysisContext context)
{
if (context.Symbol is not INamedTypeSymbol typeSymbol)
return;
if (typeSymbol.IsValueType || typeSymbol.BaseType is not { } baseType)
return;
if (!AttributeHelper.HasAttribute(baseType, Attribute, out var data))
return;
var location = context.Symbol.Locations[0];
if (GetMessageFromAttributeData(data) is { } message)
{
context.ReportDiagnostic(Diagnostic.Create(
RuleWithMessage,
location,
[typeSymbol.Name, baseType.Name, message]));
}
else
{
context.ReportDiagnostic(Diagnostic.Create(
Rule,
location,
[typeSymbol.Name, baseType.Name]));
}
}
private static string? GetMessageFromAttributeData(AttributeData data)
{
if (data.ConstructorArguments is not [var message, ..])
return null;
return message.Value as string;
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PrototypeInstantiationAnalyzer : DiagnosticAnalyzer
{
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
public static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdPrototypeInstantiation,
"Do not instantiate prototypes directly",
"Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(static ctx =>
{
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
if (prototypeInterface == null)
return;
ctx.RegisterOperationAction(symContext => Check(prototypeInterface, symContext), OperationKind.ObjectCreation);
});
}
private static void Check(INamedTypeSymbol prototypeInterface, OperationAnalysisContext ctx)
{
if (ctx.Operation is not IObjectCreationOperation { Type: { } resultType } creationOp)
return;
if (!TypeSymbolHelper.ImplementsInterface(resultType, prototypeInterface))
return;
ctx.ReportDiagnostic(Diagnostic.Create(Rule, creationOp.Syntax.GetLocation()));
}
}

View File

@@ -0,0 +1,76 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PrototypeNetSerializableAnalyzer : DiagnosticAnalyzer
{
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
private const string NetSerializableAttributeType = "Robust.Shared.Serialization.NetSerializableAttribute";
public static readonly DiagnosticDescriptor RuleNetSerializable = new(
Diagnostics.IdPrototypeNetSerializable,
"Prototypes should not be [NetSerializable]",
"Type {0} is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
"Usage",
DiagnosticSeverity.Warning,
true);
public static readonly DiagnosticDescriptor RuleSerializable = new(
Diagnostics.IdPrototypeSerializable,
"Prototypes should not be [Serializable]",
"Type {0} is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [
RuleNetSerializable,
RuleSerializable
];
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(static ctx =>
{
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
var netSerializableAttribute = ctx.Compilation.GetTypeByMetadataName(NetSerializableAttributeType);
if (prototypeInterface == null || netSerializableAttribute == null)
return;
ctx.RegisterSymbolAction(symbolContext => CheckClass(prototypeInterface, netSerializableAttribute, symbolContext), SymbolKind.NamedType);
});
}
private static void CheckClass(
INamedTypeSymbol prototypeInterface,
INamedTypeSymbol netSerializableAttribute,
SymbolAnalysisContext symbolContext)
{
if (symbolContext.Symbol is not INamedTypeSymbol symbol)
return;
if (!TypeSymbolHelper.ImplementsInterface(symbol, prototypeInterface))
return;
if (AttributeHelper.HasAttribute(symbol, netSerializableAttribute, out _))
{
symbolContext.ReportDiagnostic(
Diagnostic.Create(RuleNetSerializable, symbol.Locations[0], symbol.ToDisplayString()));
}
if (symbol.IsSerializable)
{
symbolContext.ReportDiagnostic(
Diagnostic.Create(RuleSerializable, symbol.Locations[0], symbol.ToDisplayString()));
}
}
}

View File

@@ -26,10 +26,10 @@ public class PhysicsTumblerBenchmark
var entManager = _sim.Resolve<IEntityManager>();
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var mapUid = entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 800; i++)
for (var i = 0; i < 300; i++)
{
entManager.TickUpdate(0.016f, false);
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
@@ -42,6 +42,9 @@ public class PhysicsTumblerBenchmark
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase))
entManager.System<SharedBroadphaseSystem>().RebuildBottomUp(mapBroadphase);
}
[Benchmark]
@@ -49,7 +52,7 @@ public class PhysicsTumblerBenchmark
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 5000; i++)
for (var i = 0; i < 1000; i++)
{
entManager.TickUpdate(0.016f, false);
}

View File

@@ -42,8 +42,8 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
public void GlobalSetup()
{
ProgramShared.PathOffset = "../../../../";
var server = StartServer();
var client = StartClient();
var server = StartServer(new() {Pool = false});
var client = StartClient(new() {Pool = false});
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -162,9 +162,10 @@ namespace Robust.Client.WebView.Cef
}
}
public bool IsOpen => _data != null;
public bool IsLoading => _data?.Browser.IsLoading ?? false;
public void EnteredTree()
public void StartBrowser()
{
DebugTools.AssertNull(_data);
@@ -195,7 +196,7 @@ namespace Robust.Client.WebView.Cef
_data = new LiveData(texture, client, browser, renderer);
}
public void ExitedTree()
public void CloseBrowser()
{
DebugTools.AssertNotNull(_data);

View File

@@ -0,0 +1,51 @@
using System;
using System.IO;
using Robust.Client.Utility;
namespace Robust.Client.WebView.Cef;
internal sealed partial class WebViewManagerCef
{
private const string BaseCacheName = "cef_cache";
private const string LockFileName = "robust.lock";
private FileStream? _lockFileStream;
private const int MaxAttempts = 15; // This probably shouldn't be a cvar because the only reason you'd need it change for legit just botting the game.
private string FindAndLockCacheDirectory()
{
var rootDir = Path.Combine(UserDataDir.GetRootUserDataDir(_gameController), BaseCacheName);
for (var i = 0; i < MaxAttempts; i++)
{
var cacheDirPath = Path.Combine(rootDir, i.ToString());
if (TryLockCacheDir(i, cacheDirPath))
return cacheDirPath;
}
throw new Exception("Unable to locate available CEF cache directory!");
}
private bool TryLockCacheDir(int attempt, string path)
{
_sawmill.Verbose($"Trying to lock cache directory {attempt}");
// Does not fail if directory already exists.
Directory.CreateDirectory(path);
var lockFilePath = Path.Combine(path, LockFileName);
try
{
var file = File.Open(lockFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
_lockFileStream = file;
_sawmill.Debug($"Successfully locked CEF cache directory {attempt}");
return true;
}
catch (IOException ex)
{
_sawmill.Error($"Failed to lock cache directory {attempt}: {ex}");
return false;
}
}
}

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -59,9 +61,9 @@ namespace Robust.Client.WebView.Cef
if (cefResourcesPath == null)
throw new InvalidOperationException("Unable to locate cef_resources directory!");
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
var cachePath = FindAndLockCacheDirectory();
var settings = new CefSettings()
{
@@ -71,7 +73,7 @@ namespace Robust.Client.WebView.Cef
BrowserSubprocessPath = subProcessPath,
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
ResourcesDirPath = cefResourcesPath,
RemoteDebuggingPort = 9222,
RemoteDebuggingPort = remoteDebugPort,
CookieableSchemesList = "usr,res",
CachePath = cachePath,
};

View File

@@ -81,11 +81,13 @@ namespace Robust.Client.WebView.Headless
private sealed class WebViewControlImplDummy : DummyBase, IWebViewControlImpl
{
public void EnteredTree()
public bool IsOpen => false;
public void StartBrowser()
{
}
public void ExitedTree()
public void CloseBrowser()
{
}

View File

@@ -9,8 +9,10 @@ namespace Robust.Client.WebView
/// </summary>
internal interface IWebViewControlImpl : IWebViewControl
{
void EnteredTree();
void ExitedTree();
public bool IsOpen { get; }
void StartBrowser();
void CloseBrowser();
void MouseMove(GUIMouseMoveEventArgs args);
void MouseExited();
void MouseWheel(GUIMouseWheelEventArgs args);

View File

@@ -26,4 +26,16 @@ public static class WCVars
/// </summary>
public static readonly CVarDef<bool> WebHeadless =
CVarDef.Create("web.headless", false, CVar.CLIENTONLY);
#if TOOLS
private const int DefaultRemoteDebugPort = 9222;
#else
private const int DefaultRemoteDebugPort = 0;
#endif
/// <summary>
/// If not 0, the port number used for Chromium's remote debugging.
/// </summary>
public static readonly CVarDef<int> WebRemoteDebugPort =
CVarDef.Create("web.remote_debug_port", DefaultRemoteDebugPort, CVar.CLIENTONLY);
}

View File

@@ -14,6 +14,7 @@ namespace Robust.Client.WebView
[Dependency] private readonly IWebViewManagerInternal _webViewManager = default!;
private readonly IWebViewControlImpl _controlImpl;
private bool _alwaysActive;
[ViewVariables(VVAccess.ReadWrite)]
public string Url
@@ -22,6 +23,21 @@ namespace Robust.Client.WebView
set => _controlImpl.Url = value;
}
[ViewVariables(VVAccess.ReadWrite)]
public bool AlwaysActive
{
get => _alwaysActive;
set
{
_alwaysActive = value;
if (_alwaysActive && !_controlImpl.IsOpen)
_controlImpl.StartBrowser();
else if (!_alwaysActive && _controlImpl.IsOpen && !IsInsideTree)
_controlImpl.CloseBrowser();
}
}
[ViewVariables] public bool IsLoading => _controlImpl.IsLoading;
public WebViewControl()
@@ -39,14 +55,16 @@ namespace Robust.Client.WebView
{
base.EnteredTree();
_controlImpl.EnteredTree();
if (!_controlImpl.IsOpen)
_controlImpl.StartBrowser();
}
protected override void ExitedTree()
{
base.ExitedTree();
_controlImpl.ExitedTree();
if (!_alwaysActive)
_controlImpl.CloseBrowser();
}
protected internal override void MouseMove(GUIMouseMoveEventArgs args)

View File

@@ -40,11 +40,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
}
return (keyFrameIndex, playingTime);
@@ -55,7 +51,7 @@ namespace Robust.Client.Animations
/// <summary>
/// The RSI state to play when this keyframe gets triggered.
/// </summary>
public readonly string Resource;
public readonly ResolvedSoundSpecifier Specifier;
/// <summary>
/// A function that returns the audio parameter to be used.
@@ -69,9 +65,9 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
{
Resource = resource;
Specifier = specifier;
KeyTime = keyTime;
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
}

View File

@@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Animations;
using Robust.Shared.Maths;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Animations
{
@@ -13,7 +11,7 @@ namespace Robust.Client.Animations
/// </summary>
public abstract class AnimationTrackProperty : AnimationTrack
{
public List<KeyFrame> KeyFrames { get; protected set; } = new();
public List<KeyFrame> KeyFrames { get; set; } = new();
/// <summary>
/// How to interpolate values when between two keyframes.
@@ -56,8 +54,14 @@ namespace Robust.Client.Animations
}
else
{
var next = KeyFrames[nextKeyFrame];
// Get us a scale 0 -> 1 here.
var t = playingTime / KeyFrames[nextKeyFrame].KeyTime;
var t = playingTime / next.KeyTime;
// Apply easing to time parameter, if one was specified
if (next.Easing != null)
t = next.Easing(t);
switch (InterpolationMode)
{
@@ -122,9 +126,9 @@ namespace Robust.Client.Animations
case Vector2 vector2:
return Vector2Helpers.InterpolateCubic((Vector2) preA, vector2, (Vector2) b, (Vector2) postB, t);
case Vector3 vector3:
return Vector3.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
return VectorHelpers.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
case Vector4 vector4:
return Vector4.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
return VectorHelpers.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
case float f:
return MathHelper.InterpolateCubic((float) preA, f, (float) b, (float) postB, t);
case double d:
@@ -149,10 +153,20 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(object value, float keyTime)
/// <summary>
/// An easing function to apply when interpolating to this keyframe's value.
/// Modifies the time parameter (0..1) of the interpolation between the previous keyframe and this one.
/// </summary>
/// <remarks>
/// See <see cref="Easings"/> for examples of easing functions, or provide your own.
/// </remarks>
public readonly Func<float, float>? Easing;
public KeyFrame(object value, float keyTime, Func<float, float>? easing = null)
{
Value = value;
KeyTime = keyTime;
Easing = easing;
}
}
}

View File

@@ -84,6 +84,19 @@ internal partial class AudioManager
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
}
void IAudioInternal.Remove(AudioStream stream)
{
if (stream.ClydeHandle == null)
return;
if (!_audioSampleBuffers.Remove(stream.BufferId))
{
return;
}
AL.DeleteBuffer(stream.BufferId);
}
/// <inheritdoc/>
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
@@ -120,9 +133,9 @@ internal partial class AudioManager
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
return new AudioStream(this, buffer, handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
}
/// <inheritdoc/>
@@ -179,9 +192,9 @@ internal partial class AudioManager
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
return new AudioStream(this, buffer, handle, length, wav.NumChannels, name);
}
/// <inheritdoc/>
@@ -210,8 +223,8 @@ internal partial class AudioManager
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
return new AudioStream(this, buffer, handle, length, channels, name);
}
public void SetMasterGain(float newGain)
@@ -293,7 +306,7 @@ internal partial class AudioManager
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[stream.BufferId].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
@@ -370,5 +383,12 @@ internal partial class AudioManager
}
_bufferedAudioSources.Clear();
foreach (var buffer in _audioSampleBuffers.Values)
{
DeleteAudioBufferOnMainThread(buffer.BufferHandle);
}
_audioSampleBuffers.Clear();
}
}

View File

@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
@@ -17,13 +17,15 @@ internal sealed partial class AudioManager : IAudioInternal
{
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
[Shared.IoC.Dependency] private readonly IReloadManager _reload = default!;
[Shared.IoC.Dependency] private readonly IResourceCache _cache = default!;
private Thread? _gameThread;
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
new();
@@ -54,8 +56,8 @@ internal sealed partial class AudioManager : IAudioInternal
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
_alContextExtensions.Add(extension);
}
@@ -116,6 +118,22 @@ internal sealed partial class AudioManager : IAudioInternal
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
_reload.Register("/Audio", "*.ogg");
_reload.Register("/Audio", "*.wav");
_reload.OnChanged += OnReload;
}
private void OnReload(ResPath args)
{
if (args.Extension != "ogg" &&
args.Extension != "wav")
{
return;
}
_cache.ReloadResource<AudioResource>(args);
}
internal bool IsMainThread()
@@ -126,7 +144,7 @@ internal sealed partial class AudioManager : IAudioInternal
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0)
EFX.DeleteFilter(handles.filterHandle);
ALC.EFX.DeleteFilter(handles.filterHandle);
}
private void _checkAlcError(ALDevice device,
@@ -140,6 +158,11 @@ internal sealed partial class AudioManager : IAudioInternal
}
}
internal void LogError(string message)
{
OpenALSawmill.Error(message);
}
/// <summary>
/// Like _checkAlError but allows custom data to be passed in as relevant.
/// </summary>

View File

@@ -6,8 +6,15 @@ namespace Robust.Client.Audio;
/// <summary>
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
/// </summary>
public sealed class AudioStream
public sealed class AudioStream : IDisposable
{
private IAudioInternal _audio;
/// <summary>
/// Buffer ID for this audio in AL.
/// </summary>
internal int BufferId { get; }
public TimeSpan Length { get; }
internal IClydeHandle? ClydeHandle { get; }
public string? Name { get; }
@@ -15,8 +22,10 @@ public sealed class AudioStream
public string? Artist { get; }
public int ChannelCount { get; }
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
internal AudioStream(IAudioInternal internalAudio, int bufferId, IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
{
_audio = internalAudio;
BufferId = bufferId;
ClydeHandle = handle;
Length = length;
ChannelCount = channelCount;
@@ -24,4 +33,9 @@ public sealed class AudioStream
Title = title;
Artist = artist;
}
public void Dispose()
{
_audio.Remove(this);
}
}

View File

@@ -1,4 +1,3 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Components;
using Robust.Shared.GameObjects;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -56,6 +57,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
private EntityQuery<PhysicsComponent> _physicsQuery;
private float _maxRayLength;
private float _zOffset;
private float _audioEndBuffer;
public override float ZOffset
{
@@ -79,8 +82,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
}
private float _zOffset;
/// <inheritdoc />
public override void Initialize()
{
@@ -108,20 +109,31 @@ public sealed partial class AudioSystem : SharedAudioSystem
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true);
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
Subs.CVar(CfgManager, CVars.AudioTickRate, OnAudioTickRate, true);
InitializeLimit();
}
private void OnAudioBuffer(float value)
{
_audioEndBuffer = value;
}
private void OnAudioTickRate(int obj)
{
_audioFrameTime = 1f / obj;
_audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
private void OnAudioState(Entity<AudioComponent> entity, ref AfterAutoHandleStateEvent args)
{
var component = entity.Comp;
if (component.LifeStage < ComponentLifeStage.Initialized)
return;
ApplyAudioParams(component.Params, component);
component.Source.Global = component.Global;
@@ -145,21 +157,29 @@ public sealed partial class AudioSystem : SharedAudioSystem
case AudioState.Stopped:
component.StopPlaying();
component.PlaybackPosition = 0f;
break;
return;
}
// If playback position changed then update it.
if (!string.IsNullOrEmpty(component.FileName))
{
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
var currentPosition = component.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
var currentPosition = entity.Comp.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
if (diff > 0.1f)
// Don't try to set the audio too far ahead.
if (!string.IsNullOrEmpty(entity.Comp.FileName))
{
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
{
component.PlaybackPosition = position;
entity.Comp.StopPlaying();
return;
}
}
// If the difference is minor then we'll just keep playing it.
if (diff > 0.1f)
{
entity.Comp.PlaybackPosition = position;
}
}
/// <summary>
@@ -207,6 +227,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
{
var component = entity.Comp;
length ??= GetAudioLength(component.FileName);
// If audio came into range then start playback at the correct position.
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
if (TryAudioLimit(component.FileName))
{
@@ -230,10 +254,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
length ??= GetAudioLength(component.FileName);
// If audio came into range then start playback at the correct position.
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
// If the offset < buffer than just play it from the start.
if (offset < AudioDespawnBuffer)
{
offset = 0;
}
// Not enough audio to play
else if (offset > length.Value.TotalSeconds - _audioEndBuffer)
{
component.StopPlaying();
return;
}
if (offset > 0)
{
@@ -341,13 +372,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
var parentUid = xform.ParentUid;
Vector2 worldPos;
component.Volume = component.Params.Volume;
// Handle grid audio differently by using grid position.
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
var parentUid = xform.ParentUid;
worldPos = _maps.GetGridPosition(parentUid);
}
else
@@ -381,7 +412,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
else
{
var occlusion = GetOcclusion(listener, delta, distance, entity);
var occlusion = GetOcclusion(listener, delta, distance, parentUid);
component.Occlusion = occlusion;
}
@@ -389,11 +420,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
component.Position = worldPos;
// Make race cars go NYYEEOOOOOMMMMM
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
if (_physicsQuery.TryGetComponent(parentUid, out var physicsComp))
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
var velocity = _physics.GetMapLinearVelocity(parentUid, physicsComp);
component.Velocity = velocity;
}
}
@@ -415,6 +446,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
return occlusion;
}
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
{
var filename = GetAudioPath(specifier);
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
return true;
Log.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
@@ -433,15 +474,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
return false;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
return PlayStatic(specifier, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
@@ -477,21 +518,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
Specifier = specifier,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
}
/// <summary>
@@ -499,9 +540,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
component.Global = true;
component.Source.Global = true;
DirtyField(entity, component, nameof(AudioComponent.Global));
@@ -513,22 +554,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
Specifier = specifier,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
}
/// <summary>
@@ -537,17 +578,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
LogAudioPlaybackOnInvalidEntity(specifier, entity);
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
// Since we're playing the sound immediately in the middle of a tick, we need to force ProcessStream -now-
// to set occlusion/position/velocity etc
// otherwise predicted positional sounds will sound very incorrect in several possible ways (e#5802, e#6175) until the next tick
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
return playing;
}
@@ -557,22 +603,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Specifier = specifier,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
}
/// <summary>
@@ -581,41 +627,45 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
// see PlayEntity for why this is necessary
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
return playing;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
return PlayEntity(specifier, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
@@ -629,39 +679,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
LoadStream(entity, stream);
EntityManager.InitializeAndStartEntity(entity);
var comp = entity.Comp;
@@ -673,8 +723,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
offset = Math.Clamp(offset, 0f, maxOffset);
source.PlaybackPosition = offset;
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
ApplyAudioParams(comp.Params, comp);
source.StartPlaying();
return (entity, comp);
}
@@ -694,17 +742,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
PlayGlobal(ev.Specifier, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)
@@ -712,6 +760,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
{
var soundInfo = specifier?.ToString() ?? "unknown sound";
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
}
#region Jobs
private record struct UpdateAudioJob : IParallelRobustJob

View File

@@ -1,5 +1,6 @@
using System;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Maths;
@@ -15,16 +16,16 @@ internal sealed class AudioEffect : IAudioEffect
public AudioEffect(IAudioInternal manager)
{
Handle = EFX.GenEffect();
Handle = ALC.EFX.GenEffect();
_master = manager;
EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
ALC.EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
}
public void Dispose()
{
if (Handle != 0)
{
EFX.DeleteEffect(Handle);
ALC.EFX.DeleteEffect(Handle);
Handle = 0;
}
}
@@ -43,14 +44,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
_master._checkAlError();
}
}
@@ -61,14 +62,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
_master._checkAlError();
}
}
@@ -79,14 +80,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
_master._checkAlError();
}
}
@@ -97,14 +98,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
_master._checkAlError();
}
}
@@ -115,14 +116,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
_master._checkAlError();
}
}
@@ -133,14 +134,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
_master._checkAlError();
}
}
@@ -151,14 +152,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
_master._checkAlError();
}
}
@@ -169,14 +170,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
_master._checkAlError();
}
}
@@ -187,14 +188,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
_master._checkAlError();
}
}
@@ -205,14 +206,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
_master._checkAlError();
}
}
@@ -223,7 +224,7 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
var value = ALC.EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
@@ -231,7 +232,7 @@ internal sealed class AudioEffect : IAudioEffect
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
ALC.EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
_master._checkAlError();
}
}
@@ -242,14 +243,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
_master._checkAlError();
}
}
@@ -260,14 +261,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
_master._checkAlError();
}
}
@@ -278,7 +279,7 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
var value = ALC.EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
@@ -286,7 +287,7 @@ internal sealed class AudioEffect : IAudioEffect
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
ALC.EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
_master._checkAlError();
}
}
@@ -297,14 +298,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
_master._checkAlError();
}
}
@@ -315,14 +316,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
_master._checkAlError();
}
}
@@ -333,14 +334,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
_master._checkAlError();
}
}
@@ -351,14 +352,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
_master._checkAlError();
}
}
@@ -369,14 +370,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
_master._checkAlError();
}
}
@@ -387,14 +388,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
_master._checkAlError();
}
}
@@ -405,14 +406,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
_master._checkAlError();
}
}
@@ -423,14 +424,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
ALC.EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
ALC.EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
_master._checkAlError();
}
}
@@ -441,14 +442,14 @@ internal sealed class AudioEffect : IAudioEffect
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
ALC.EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
ALC.EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
_master._checkAlError();
}
}

View File

@@ -1,4 +1,4 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Audio.OpenAL;
using Robust.Shared.Audio.Effects;
namespace Robust.Client.Audio.Effects;
@@ -6,13 +6,13 @@ namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AuxiliaryAudio : IAuxiliaryAudio
{
internal int Handle = EFX.GenAuxiliaryEffectSlot();
internal int Handle = ALC.EFX.GenAuxiliaryEffectSlot();
public void Dispose()
{
if (Handle != -1)
{
EFX.DeleteAuxiliaryEffectSlot(Handle);
ALC.EFX.DeleteAuxiliaryEffectSlot(Handle);
Handle = -1;
}
}
@@ -22,11 +22,11 @@ internal sealed class AuxiliaryAudio : IAuxiliaryAudio
{
if (effect is AudioEffect audEffect)
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, audEffect.Handle);
ALC.EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, audEffect.Handle);
}
else
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, 0);
ALC.EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, 0);
}
}
}

View File

@@ -13,6 +13,8 @@ namespace Robust.Client.Audio;
/// </summary>
internal sealed class HeadlessAudioManager : IAudioInternal
{
private int _audioBuffer;
/// <inheritdoc />
public void InitializePostWindowing()
{
@@ -65,6 +67,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
{
}
/// <inheritdoc />
public void Remove(AudioStream stream)
{
}
/// <inheritdoc />
public void StopAllAudio()
{
@@ -101,11 +108,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
return new AudioStream(null, length, channels, name);
return new AudioStream(this, _audioBuffer++, null, length, channels, name);
}
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
private AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
{
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
return new AudioStream(this, _audioBuffer++, null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
}
}

View File

@@ -44,6 +44,8 @@ internal interface IAudioInternal : IAudioManager
void SetAttenuation(Attenuation attenuation);
void Remove(AudioStream stream);
/// <summary>
/// Stops all audio from playing.
/// </summary>

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using Robust.Client.Audio.Sources;
using Robust.Shared.Audio.Sources;
namespace Robust.Client.Audio;
@@ -11,7 +10,7 @@ namespace Robust.Client.Audio;
public interface IAudioManager
{
IAudioSource? CreateAudioSource(AudioStream stream);
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
@@ -156,8 +157,13 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// Loads a new soundfont into the renderer.
/// </summary>
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
void LoadSoundfont(string filename, bool resetPresets = false);
void LoadSoundfontResource(ResPath path, bool resetPresets = false);
void LoadSoundfontUser(ResPath path, bool resetPresets = false);
/// <summary>
/// Invoked whenever a new midi event is registered.
/// </summary>
@@ -207,4 +213,6 @@ public interface IMidiRenderer : IDisposable
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
/// </summary>
internal void InternalDispose();
byte MinVolume { get; set; }
}

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NFluidsynth;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager
{
// For loading sound fonts, we have to use a callback model where we can only parse a string.
// This API, frankly, fucking sucks.
//
// These prefixes are used to separate the various places a file *can* be loaded from.
//
// We cannot prevent Fluidsynth from trying to load prefixed paths itself if they are invalid
// So if content specifies "/foobar.sf2" to be loaded and it doesn't exist,
// Fluidsynth *will* try to fopen("RES:/foobar.sf2"). For this reason I'm putting in some nonsense characters
// that will pass through Fluidsynth fine, but make sure the filename is *never* a practically valid OS path.
//
// NOTE: Raw disk paths *cannot* be prefixed as Fluidsynth needs to load those itself.
// Specifically, their .dls loader doesn't respect file callbacks.
// If you're curious why this is: it's two-fold:
// * The Fluidsynth C code for the .dls loader just doesn't use the file callbacks, period.
// * Even if it did, we're not specifying those file callbacks, as they're per loader,
// and we're only adding a *new* sound font loader with file callbacks, not modifying the existing ones.
// The loader for .sfX format and .dls format are different loader objects in Fluidsynth.
internal const string PrefixCommon = "!/ -?\x0001";
internal const string PrefixLegacy = PrefixCommon + "LEGACY";
internal const string PrefixUser = PrefixCommon + "USER";
internal const string PrefixResources = PrefixCommon + "RES";
private void LoadSoundFontSetup(MidiRenderer renderer)
{
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfontResource(FallbackSoundfont);
// Load system-specific soundfonts.
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
continue;
try
{
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
renderer.LoadSoundfontDisk(filepath);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
renderer.LoadSoundfontDisk(OsxSoundfont);
}
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
renderer.LoadSoundfontDisk(WindowsSoundfont);
}
}
// Maybe load soundfont specified in environment variable.
// Load it here so it can override system soundfonts but not content or user data soundfonts.
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is { } soundfontOverride)
{
// Just to avoid funny shit: avoid people smuggling a prefix in here.
// I wish I could separate this properly...
var (prefix, _) = SplitPrefix(soundfontOverride);
if (IsValidPrefix(prefix))
{
_midiSawmill.Error($"Not respecting {SoundfontEnvironmentVariable} env variable: invalid file path");
}
else if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
{
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
renderer.LoadSoundfontDisk(soundfontOverride);
}
}
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading content soundfont {file}");
renderer.LoadSoundfontResource(file);
}
// Load every soundfont from the user data directory last, since those may override any other soundfont.
_midiSawmill.Debug($"Loading soundfonts from user data directory {CustomSoundfontDirectory}");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
foreach (var file in enumerator)
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading user soundfont {file}");
renderer.LoadSoundfontUser(file);
}
}
internal static string PrefixPath(string prefix, string value)
{
return $"{prefix}:{value}";
}
internal static (string prefix, string? value) SplitPrefix(string filename)
{
var filenameSplit = filename.Split(':', 2);
if (filenameSplit.Length == 1)
return (filenameSplit[0], null);
return (filenameSplit[0], filenameSplit[1]);
}
internal static bool IsValidPrefix(string prefix)
{
return prefix is PrefixLegacy or PrefixUser or PrefixResources;
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly MidiManager _parent;
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public ResourceLoaderCallbacks(MidiManager parent)
{
_parent = parent;
}
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream stream;
try
{
stream = OpenCore(filename);
}
catch (Exception e)
{
_parent._midiSawmill.Error($"Error while opening sound font: {e}");
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
private Stream OpenCore(string filename)
{
var (prefix, value) = SplitPrefix(filename);
if (!IsValidPrefix(prefix) || value == null)
return File.OpenRead(filename);
var resourceCache = _parent._resourceManager;
var resourcePath = new ResPath(value);
switch (prefix)
{
case PrefixUser:
return resourceCache.UserData.OpenRead(resourcePath);
case PrefixResources:
return resourceCache.ContentFileRead(resourcePath);
case PrefixLegacy:
// Try resources first, then try user data.
if (resourceCache.TryContentFileRead(resourcePath, out var stream))
return stream;
return resourceCache.UserData.OpenRead(resourcePath);
default:
throw new UnreachableException("Invalid prefix specified!");
}
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
}
catch (EndOfStreamException)
{
return -1;
}
return 0;
}
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
}
public override long Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (long) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
}

View File

@@ -42,7 +42,7 @@ internal sealed partial class MidiManager : IMidiManager
[Dependency] private readonly IRuntimeLog _runtime = default!;
private AudioSystem _audioSys = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
private SharedPhysicsSystem _physics = default!;
private SharedTransformSystem _xformSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
@@ -81,7 +81,7 @@ internal sealed partial class MidiManager : IMidiManager
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _gain = 0f;
private bool _volumeDirty = true;
private bool _gainDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
@@ -96,7 +96,7 @@ internal sealed partial class MidiManager : IMidiManager
return;
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
_volumeDirty = true;
_gainDirty = true;
}
}
@@ -114,12 +114,13 @@ internal sealed partial class MidiManager : IMidiManager
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private static readonly string WindowsSoundfont = $@"{Environment.GetEnvironmentVariable("SystemRoot")}\system32\drivers\gm.dls";
private static readonly string WindowsSoundfont =
$@"{Environment.GetEnvironmentVariable("SystemRoot")}\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private static readonly ResPath FallbackSoundfont = new ResPath("/Midi/fallback.sf2");
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
@@ -145,11 +146,13 @@ internal sealed partial class MidiManager : IMidiManager
{
if (FluidsynthInitialized || _failedInitialize) return;
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_gain = value;
_volumeDirty = true;
}, true);
_cfgMan.OnValueChanged(CVars.MidiVolume,
value =>
{
_gain = value;
_gainDirty = true;
},
true);
_midiSawmill = _logger.GetSawmill("midi");
#if DEBUG
@@ -167,13 +170,15 @@ internal sealed partial class MidiManager : IMidiManager
// not a directory, preserve the old file and create an actual directory
else if (!_resourceManager.UserData.IsDir(CustomSoundfontDirectory))
{
_resourceManager.UserData.Rename(CustomSoundfontDirectory, CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.Rename(CustomSoundfontDirectory,
CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
try
{
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
NFluidsynth.Logger
.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
@@ -193,7 +198,7 @@ internal sealed partial class MidiManager : IMidiManager
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int) (Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(midiParallel, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
@@ -219,7 +224,7 @@ internal sealed partial class MidiManager : IMidiManager
};
_audioSys = _entityManager.EntitySysManager.GetEntitySystem<AudioSystem>();
_broadPhaseSystem = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_physics = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_xformSystem = _entityManager.System<SharedTransformSystem>();
_entityManager.GetEntityQuery<PhysicsComponent>();
_entityManager.GetEntityQuery<TransformComponent>();
@@ -263,83 +268,10 @@ internal sealed partial class MidiManager : IMidiManager
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
var renderer =
new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
// Load system-specific soundfonts.
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
continue;
try
{
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
renderer.LoadSoundfont(filepath);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
renderer.LoadSoundfont(OsxSoundfont);
}
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
renderer.LoadSoundfont(WindowsSoundfont);
}
}
// Maybe load soundfont specified in environment variable.
// Load it here so it can override system soundfonts but not content or user data soundfonts.
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is {} soundfontOverride)
{
if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
{
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
renderer.LoadSoundfont(soundfontOverride);
}
}
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading content soundfont {file}");
renderer.LoadSoundfont(file.ToString());
}
var userDataPath = _resourceManager.UserData.RootDir == null
? CustomSoundfontDirectory
: new ResPath(_resourceManager.UserData.RootDir) / CustomSoundfontDirectory.ToRelativePath();
// Load every soundfont from the user data directory last, since those may override any other soundfont.
_midiSawmill.Debug($"Loading soundfonts from user data directory {userDataPath}");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
foreach (var file in enumerator)
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading user soundfont {file}");
renderer.LoadSoundfont(file.ToString());
}
LoadSoundFontSetup(renderer);
renderer.Source.Gain = _gain;
@@ -347,6 +279,7 @@ internal sealed partial class MidiManager : IMidiManager
{
_renderers.Add(renderer);
}
return renderer;
}
finally
@@ -383,99 +316,23 @@ internal sealed partial class MidiManager : IMidiManager
_updateSemaphore.Release();
_volumeDirty = false;
_gainDirty = false;
}
private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
{
// TODO: This should be sharing more code with AudioSystem.
try
{
if (renderer.Disposed)
return;
if (_volumeDirty)
{
renderer.Source.Gain = Gain;
}
if (!renderer.Mono)
{
renderer.Source.Global = true;
return;
}
MapCoordinates mapPos;
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
renderer.TrackingCoordinates = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
// Pause it if the attached entity is paused.
if (_entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Pause();
return;
}
}
else if (renderer.TrackingCoordinates == null)
{
renderer.Source.Pause();
return;
}
mapPos = renderer.TrackingCoordinates.Value;
// If it's on a different map then just mute it, not pause.
if (mapPos.MapId == MapId.Nullspace || mapPos.MapId != listener.MapId)
{
renderer.Source.Gain = 0f;
return;
}
// Was previously muted maybe so try unmuting it?
if (renderer.Source.Gain == 0f)
{
renderer.Source.Gain = Gain;
}
var worldPos = mapPos.Position;
var delta = worldPos - listener.Position;
var distance = delta.Length();
// Update position
// Out of range so just clip it for us.
if (distance > renderer.Source.MaxDistance)
{
// Still keeps the source playing, just with no volume.
renderer.Source.Gain = 0f;
return;
}
// Same imprecision suppression as audiosystem.
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
renderer.Source.Position = worldPos;
// Update velocity (doppler).
if (!_entityManager.Deleted(renderer.TrackingEntity))
{
var velocity = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity.Value);
renderer.Source.Velocity = velocity;
}
if (!renderer.Source.Global)
UpdateLocalRenderer(renderer, listener);
else
{
renderer.Source.Velocity = Vector2.Zero;
}
// Update occlusion
var occlusion = _audioSys.GetOcclusion(listener, delta, distance, renderer.TrackingEntity);
renderer.Source.Occlusion = occlusion;
UpdateGlobalRenderer(renderer);
}
catch (Exception ex)
{
@@ -483,6 +340,58 @@ internal sealed partial class MidiManager : IMidiManager
}
}
private void UpdateLocalRenderer(IMidiRenderer renderer, MapCoordinates listener)
{
if (_entityManager.Deleted(renderer.TrackingEntity) || _entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Gain = 0f;
return;
}
MapCoordinates mapCoords = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
renderer.TrackingCoordinates = mapCoords;
if (mapCoords.MapId == MapId.Nullspace || mapCoords.MapId != listener.MapId)
{
renderer.Source.Gain = 0f;
return;
}
Vector2 mapPosition = mapCoords.Position;
Vector2 listenerDelta = mapPosition - listener.Position;
var listenerDeltaLength = listenerDelta.Length();
if (listenerDeltaLength > renderer.Source.MaxDistance)
{
renderer.Source.Gain = 0f;
return;
}
if (listenerDeltaLength is > 0f and < 0.01f)
{
mapPosition = listener.Position;
listenerDelta = Vector2.Zero;
listenerDeltaLength = 0f;
}
if (_gainDirty || renderer.Source.Gain == 0f)
renderer.Source.Gain = Gain;
renderer.Source.Position = mapPosition;
renderer.Source.Velocity = _physics.GetMapLinearVelocity(renderer.TrackingEntity.Value);
renderer.Source.Occlusion =
_audioSys.GetOcclusion(listener, listenerDelta, listenerDeltaLength, renderer.TrackingEntity);
}
private void UpdateGlobalRenderer(IMidiRenderer renderer)
{
if (_gainDirty)
renderer.Source.Gain = Gain;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
@@ -502,7 +411,7 @@ internal sealed partial class MidiManager : IMidiManager
{
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
if (renderer.Master is {Disposed: true})
renderer.Master = null;
renderer.Render();
@@ -572,130 +481,6 @@ internal sealed partial class MidiManager : IMidiManager
midiEvent.Velocity);
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly MidiManager _parent;
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public ResourceLoaderCallbacks(MidiManager parent)
{
_parent = parent;
}
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = _parent._resourceManager;
var resourcePath = new ResPath(filename);
if (resourcePath.IsRooted)
{
// is it in content?
if (resourceCache.ContentFileExists(filename))
{
if (!resourceCache.TryContentFileRead(filename, out stream))
return IntPtr.Zero;
}
// is it in userdata?
else if (resourceCache.UserData.Exists(resourcePath))
{
stream = resourceCache.UserData.OpenRead(resourcePath);
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
}
catch (EndOfStreamException)
{
return -1;
}
return 0;
}
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
}
public override long Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (long) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
#region Jobs
private record struct MidiUpdateJob : IParallelRobustJob

View File

@@ -0,0 +1,45 @@
using System;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiRenderer
{
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
public void LoadSoundfont(string filename, bool resetPresets = true)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixLegacy, filename),
resetPresets);
}
public void LoadSoundfontResource(ResPath path, bool resetPresets = false)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixResources, path.ToString()),
resetPresets);
}
public void LoadSoundfontUser(ResPath path, bool resetPresets = false)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixUser, path.ToString()),
resetPresets);
}
internal void LoadSoundfontDisk(string path, bool resetPresets = false)
{
LoadSoundfontCore(
path,
resetPresets);
}
private void LoadSoundfontCore(string filenameString, bool resetPresets)
{
lock (_playerStateLock)
{
_synth.LoadSoundFont(filenameString, resetPresets);
MidiSoundfont = 1;
}
}
}

View File

@@ -16,7 +16,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.Audio.Midi;
internal sealed class MidiRenderer : IMidiRenderer
internal sealed partial class MidiRenderer : IMidiRenderer
{
private readonly IMidiManager _midiManager;
private readonly ITaskManager _taskManager;
@@ -214,6 +214,11 @@ internal sealed class MidiRenderer : IMidiRenderer
[ViewVariables]
public BitArray FilteredChannels { get; } = new(RobustMidiEvent.MaxChannels);
[ViewVariables]
public byte MinVolume { get => _minVolume; set => _minVolume = value; }
private byte _minVolume;
[ViewVariables(VVAccess.ReadWrite)]
public byte? VelocityOverride { get; set; } = null;
@@ -226,6 +231,9 @@ internal sealed class MidiRenderer : IMidiRenderer
if (value == _master)
return;
if (CheckMasterCycle(value))
throw new InvalidOperationException("Tried to set master to a child of this renderer!");
if (_master is { Disposed: false })
{
try
@@ -432,15 +440,6 @@ internal sealed class MidiRenderer : IMidiRenderer
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
}
public void LoadSoundfont(string filename, bool resetPresets = true)
{
lock (_playerStateLock)
{
_synth.LoadSoundFont(filename, resetPresets);
MidiSoundfont = 1;
}
}
void IMidiRenderer.Render()
{
Render();
@@ -545,14 +544,7 @@ internal sealed class MidiRenderer : IMidiRenderer
if (velocity <= 0)
continue;
try
{
_synth.NoteOn(channel, key, velocity);
}
catch (FluidSynthInteropException e)
{
_midiSawmill.Error($"CH:{channel} KEY:{key} VEL:{velocity} {e.ToStringBetter()}");
}
_synth.TryNoteOn(channel, key, velocity);
}
}
@@ -580,19 +572,32 @@ internal sealed class MidiRenderer : IMidiRenderer
{
case RobustMidiCommand.NoteOff:
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.NoteOff(midiEvent.Channel, midiEvent.Key);
break;
_synth.TryNoteOff(midiEvent.Channel, midiEvent.Key);
break;
case RobustMidiCommand.NoteOn:
// Velocity 0 *can* represent a NoteOff event.
var velocity = midiEvent.Velocity;
if (velocity == 0)
{
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
}
if (FilteredChannels[midiEvent.Channel])
break;
var velocity = VelocityOverride ?? midiEvent.Velocity;
if (MinVolume > 0)
velocity = (byte)Math.Floor(MathHelper.Lerp(MinVolume, 127, (float)velocity / 127));
velocity = VelocityOverride ?? velocity;
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
case RobustMidiCommand.AfterTouch:
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = midiEvent.Value;
_synth.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
@@ -729,4 +734,22 @@ internal sealed class MidiRenderer : IMidiRenderer
_synth?.Dispose();
_player?.Dispose();
}
/// <summary>
/// Check that a given renderer is not already a child of this renderer, i.e. it would introduce a cycle if set as master of this renderer.
/// </summary>
private bool CheckMasterCycle(IMidiRenderer? otherRenderer)
{
// Doesn't inside drift, cringe.
while (otherRenderer != null)
{
if (otherRenderer == this)
return true;
otherRenderer = otherRenderer.Master;
}
return false;
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -13,7 +12,7 @@ internal sealed class AudioSource : BaseAudioSource
/// <summary>
/// Underlying stream to the audio.
/// </summary>
private readonly AudioStream _sourceStream;
internal readonly AudioStream SourceStream;
#if DEBUG
private bool _didPositionWarning;
@@ -21,7 +20,7 @@ internal sealed class AudioSource : BaseAudioSource
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
{
_sourceStream = sourceStream;
SourceStream = sourceStream;
}
/// <inheritdoc />
@@ -47,13 +46,13 @@ internal sealed class AudioSource : BaseAudioSource
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
if (SourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
SourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{SourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif
@@ -77,7 +76,7 @@ internal sealed class AudioSource : BaseAudioSource
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
ALC.EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
Master.RemoveAudioSource(SourceHandle);

View File

@@ -1,7 +1,6 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Sources;
@@ -82,9 +81,9 @@ public abstract class BaseAudioSource : IAudioSource
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
var state = AL.GetSource(SourceHandle, ALGetSourcei.SourceState);
Master._checkAlError();
return state == ALSourceState.Playing;
return state == (int)ALSourceState.Playing;
}
set
{
@@ -208,6 +207,12 @@ public abstract class BaseAudioSource : IAudioSource
}
set
{
if (float.IsNaN(value))
{
Master.LogError($"Tried to set NaN gain, setting audio source to 0f: {Environment.StackTrace}");
value = 0f;
}
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
@@ -356,11 +361,11 @@ public abstract class BaseAudioSource : IAudioSource
if (audio is AuxiliaryAudio impAudio)
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
}
else
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
}
Master._checkAlError();
@@ -370,12 +375,12 @@ public abstract class BaseAudioSource : IAudioSource
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
FilterHandle = ALC.EFX.GenFilter();
ALC.EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio.Sources;
namespace Robust.Client.Audio.Sources;
@@ -37,9 +36,9 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
var state = AL.GetSource(SourceHandle, ALGetSourcei.SourceState);
_master._checkAlError();
return state == ALSourceState.Playing;
return state == (int)ALSourceState.Playing;
}
set
{
@@ -84,7 +83,7 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
ALC.EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
AL.DeleteBuffers(BufferHandles);

View File

@@ -115,10 +115,6 @@ namespace Robust.Client
/// <inheritdoc />
public void DisconnectFromServer(string reason)
{
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
DebugTools.Assert(_net.IsConnected);
// run level changed in OnNetDisconnect()
// are both of these *really* needed?
_net.ClientDisconnect(reason);
}

View File

@@ -8,8 +8,10 @@ using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Graphics.FontManagement;
using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Localization;
using Robust.Client.Map;
using Robust.Client.Placement;
using Robust.Client.Player;
@@ -36,6 +38,7 @@ using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
@@ -46,6 +49,7 @@ using Robust.Shared.Replays;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client
@@ -64,6 +68,7 @@ namespace Robust.Client
deps.Register<IMapManagerInternal, NetworkedMapManager>();
deps.Register<INetworkedMapManager, NetworkedMapManager>();
deps.Register<IEntityManager, ClientEntityManager>();
deps.Register<FontTagHijackHolder>();
deps.Register<IReflectionManager, ClientReflectionManager>();
deps.Register<IConsoleHost, ClientConsoleHost>();
deps.Register<IClientConsoleHost, ClientConsoleHost>();
@@ -102,6 +107,11 @@ namespace Robust.Client
deps.Register<ProfViewManager>();
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
deps.Register<NetworkResourceManager>();
deps.Register<IReloadManager, ReloadManager>();
deps.Register<ILocalizationManager, ClientLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
deps.Register<LoadingScreenManager>();
deps.Register<ILoadingScreenManager, LoadingScreenManager>();
switch (mode)
{
@@ -114,6 +124,8 @@ namespace Robust.Client
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
deps.Register<ISystemFontManager, SystemFontManagerFallback>();
deps.Register<ISystemFontManagerInternal, SystemFontManagerFallback>();
break;
case GameController.DisplayMode.Clyde:
deps.Register<IClyde, Clyde>();
@@ -124,6 +136,8 @@ namespace Robust.Client
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();
deps.Register<ISystemFontManager, SystemFontManager>();
deps.Register<ISystemFontManagerInternal, SystemFontManager>();
break;
default:
throw new ArgumentOutOfRangeException();
@@ -138,6 +152,7 @@ namespace Robust.Client
deps.Register<IViewVariablesManager, ClientViewVariablesManager>();
deps.Register<IClientViewVariablesManager, ClientViewVariablesManager>();
deps.Register<IClientViewVariablesManagerInternal, ClientViewVariablesManager>();
deps.Register<IViewVariableControlFactory, ViewVariableControlFactory>();
deps.Register<IClientConGroupController, ClientConGroupController>();
deps.Register<IScriptClient, ScriptClient>();
deps.Register<IRobustSerializer, ClientRobustSerializer>();

View File

@@ -31,8 +31,7 @@ public sealed class LightTreeSystem : ComponentTreeSystem<LightTreeComponent, Po
var pos = XformSystem.GetRelativePosition(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
entry.Component.TreeUid.Value);
return ExtractAabb(in entry, pos, default);
}

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
@@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees;
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
}
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
=> QueueTreeUpdate(uid, component, args.Xform);
// TODO remove this when finally ECSing sprite components
[ByRefEvent]
internal readonly struct QueueSpriteTreeUpdateEvent
{
public readonly TransformComponent Xform;
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
{
Xform = xform;
}
}
[Dependency] private readonly SpriteSystem _sprite = default!;
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
@@ -36,6 +19,11 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override int InitialCapacity => 1024;
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}
#endregion
}

View File

@@ -71,7 +71,7 @@ internal sealed class ClientNetConfigurationManager : NetConfigurationManager, I
// Actually set the CVar
base.SetCVar(name, value, force);
if ((flags & CVar.REPLICATED) == 0)
if ((flags & CVar.REPLICATED) == 0 || !NetManager.IsConnected)
return;
var msg = new MsgConVars();

View File

@@ -191,8 +191,16 @@ namespace Robust.Client.Console
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
cmd.Execute(shell, command, cmdArgs);
try
{
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
cmd.Execute(shell, command, cmdArgs);
}
catch (Exception e)
{
_conLogger.Error($"ExecuteError - {command}:\n{e}");
shell.WriteError($"There was an error while executing the command: {e}");
}
}
private bool CanExecute(string cmdName)

View File

@@ -173,29 +173,51 @@ namespace Robust.Client.Console.Commands
}
}
internal sealed class ShowPositionsCommand : LocalizedCommands
internal sealed class ShowPositionsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showpos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugPositions = !mgr.DebugPositions;
_debugDrawing.DebugPositions = !_debugDrawing.DebugPositions;
}
}
internal sealed class ShowRotationsCommand : LocalizedCommands
internal sealed class ShowRotationsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showrot";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugRotations = !mgr.DebugRotations;
_debugDrawing.DebugRotations = !_debugDrawing.DebugRotations;
}
}
internal sealed class ShowVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugVelocities = !_debugDrawing.DebugVelocities;
}
}
internal sealed class ShowAngularVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showangvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugAngularVelocities = !_debugDrawing.DebugAngularVelocities;
}
}

View File

@@ -1,17 +1,19 @@
#if DEBUG
using Robust.Client.Debugging;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class DebugAnchoredCommand : LocalizedCommands
public sealed class DebugAnchoredCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugAnchoringSystem _system = default!;
public override string Command => "showanchored";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugAnchoringSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
}

View File

@@ -1,10 +1,11 @@
using System;
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
namespace Robust.Client.Console.Commands
{
#if DEBUG
#if TOOLS
internal sealed class DumpMetadataMembersCommand : LocalizedCommands
{
public override string Command => "dmetamem";
@@ -19,10 +20,28 @@ namespace Robust.Client.Console.Commands
return;
}
foreach (var sig in AssemblyTypeChecker.DumpMetaMembers(type))
var members = AssemblyTypeChecker.DumpMetaMembers(type)
.GroupBy(x => x.IsField)
.ToDictionary(x => x.Key, x => x.Select(t => t.Value).ToList());
if (members.TryGetValue(true, out var fields))
{
System.Console.WriteLine(@$"- ""{sig}""");
shell.WriteLine(sig);
fields.Sort(StringComparer.Ordinal);
foreach (var member in fields)
{
System.Console.WriteLine(@$"- ""{member}""");
}
}
if (members.TryGetValue(false, out var methods))
{
methods.Sort(StringComparer.Ordinal);
foreach (var member in methods)
{
System.Console.WriteLine(@$"- ""{member}""");
}
}
}

View File

@@ -1,17 +1,19 @@
#if DEBUG
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
internal sealed class LightDebugCommand : LocalizedCommands
internal sealed class LightDebugCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugLightTreeSystem _system = default!;
public override string Command => "lightbb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugLightTreeSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands;
[UsedImplicitly]
internal sealed class LocalizationSetCulture : LocalizedCommands
{
private const string Name = "localization_set_culture";
private const int ArgumentCount = 1;
public override string Command => Name;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != ArgumentCount)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(args[0], predefinedOnly: false);
}
catch (CultureNotFoundException)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-cultureinfo", ("arg", args[0])));
return;
}
LocalizationManager.SetCulture(culture);
shell.WriteLine(LocalizationManager.GetString("cmd-localization_set_culture-changed",
("code", culture.Name),
("nativeName", culture.NativeName),
("englishName", culture.EnglishName)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(GetCultureNames(),
LocalizationManager.GetString("cmd-localization_set_culture-culture-name")),
_ => CompletionResult.Empty
};
}
private static HashSet<string> GetCultureNames()
{
var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !string.IsNullOrEmpty(x.Name))
.ToArray();
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
allNames.UnionWith(cultureInfos.Select(x => x.TwoLetterISOLanguageName));
allNames.UnionWith(cultureInfos.Select(x => x.Name));
return allNames;
}
}

View File

@@ -0,0 +1,18 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class ShowPlayerVelocityCommand : LocalizedEntityCommands
{
[Dependency] private readonly ShowPlayerVelocityDebugSystem _system = default!;
public override string Command => "showplayervelocity";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using ItemJustification = Robust.Client.UserInterface.Controls.WrapContainer.ItemJustification;
namespace Robust.Client.Console.Commands;
internal sealed partial class UITestControl
{
private sealed class TabWrapContainer : Control
{
private readonly CheckBox _equalSizeBox;
private readonly CheckBox _reverseBox;
private readonly OptionButton _axisButton;
private readonly OptionButton _justifyButton;
private readonly LineEdit _separationEdit;
private readonly LineEdit _crossSeparationEdit;
public TabWrapContainer()
{
var container = new WrapContainer
{
MouseFilter = MouseFilterMode.Stop,
VerticalExpand = true,
};
var random = new Random(3005);
for (var i = 0; i < 35; i++)
{
var val = random.Next(1, 16);
var text = string.Create(val, 0, (span, _) => span.Fill('O'));
container.AddChild(new Button { Text = text });
}
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 4,
Children =
{
(_equalSizeBox = new CheckBox
{
Text = nameof(WrapContainer.EqualSize)
}),
(_reverseBox = new CheckBox
{
Text = nameof(WrapContainer.Reverse)
}),
(_axisButton = new OptionButton()),
(_justifyButton = new OptionButton()),
(_separationEdit = new LineEdit
{
PlaceHolder = "Separation",
SetWidth = 100,
}),
(_crossSeparationEdit = new LineEdit
{
PlaceHolder = "Cross Separation",
SetWidth = 100,
})
}
},
new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.Black },
Children =
{
container
}
}
},
});
_axisButton.AddItem(nameof(Axis.Horizontal), (int)Axis.Horizontal);
_axisButton.AddItem(nameof(Axis.HorizontalReverse), (int)Axis.HorizontalReverse);
_axisButton.AddItem(nameof(Axis.Vertical), (int)Axis.Vertical);
_axisButton.AddItem(nameof(Axis.VerticalReverse), (int)Axis.VerticalReverse);
_axisButton.OnItemSelected += args =>
{
_axisButton.SelectId(args.Id);
container.LayoutAxis = (Axis)args.Id;
};
_justifyButton.AddItem(nameof(ItemJustification.Begin), (int)ItemJustification.Begin);
_justifyButton.AddItem(nameof(ItemJustification.Center), (int)ItemJustification.Center);
_justifyButton.AddItem(nameof(ItemJustification.End), (int)ItemJustification.End);
_justifyButton.OnItemSelected += args =>
{
_justifyButton.SelectId(args.Id);
container.Justification = (ItemJustification)args.Id;
};
_equalSizeBox.OnPressed += _ => container.EqualSize = _equalSizeBox.Pressed;
_reverseBox.OnPressed += _ => container.Reverse = _reverseBox.Pressed;
_separationEdit.OnTextChanged += args =>
{
if (!int.TryParse(args.Text, out var sep))
sep = 0;
container.SeparationOverride = sep;
};
_crossSeparationEdit.OnTextChanged += args =>
{
if (!int.TryParse(args.Text, out var sep))
sep = 0;
container.CrossSeparationOverride = sep;
};
}
}
}

View File

@@ -3,9 +3,8 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -44,7 +43,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
var progressBar = new ProgressBar { MaxValue = 10, Value = 5 };
vBox.AddChild(progressBar);
var optionButton = new OptionButton();
var optionButton = new OptionButton
{
ToolTip = "This button has a tooltip. Spooky!"
};
optionButton.AddItem("Honk");
optionButton.AddItem("Foo");
optionButton.AddItem("Bar");
@@ -154,6 +156,8 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
_sprite = new TabSpriteView();
_tabContainer.AddChild(_sprite);
_tabContainer.AddChild(TabCursorShapes());
_tabContainer.AddChild(new TabWrapContainer { Name = nameof(Tab.WrapContainer) });
}
public void OnClosed()
@@ -204,12 +208,62 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
private Control TabRichText()
{
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkupOrThrow(Lipsum));
var msg = FormattedMessage.FromMarkupOrThrow(Lipsum);
msg.AddMarkupOrThrow("\n\nWAWWAAWAWWAWA [cmdlink=\"DOES IT WORK\" command=\"help\" /] DOES IT WORK");
label.SetMessage(msg, [typeof(CommandLinkTag)]);
TabContainer.SetTabTitle(label, "RichText");
return label;
}
private Control TabCursorShapes()
{
var box = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
};
var styleBox = new StyleBoxFlat
{
BackgroundColor = Color.Black
};
foreach (var cursorName in Enum.GetNames<CursorShape>())
{
// Go over names due to duplicate definitions in the enum.
var cursor = Enum.Parse<CursorShape>(cursorName);
// Wow was I bad at API design.
if (cursor == CursorShape.Custom)
continue;
var panel = new PanelContainer
{
PanelOverride = styleBox,
DefaultCursorShape = cursor,
MouseFilter = MouseFilterMode.Stop,
MinHeight = 30,
Children =
{
new Label
{
Text = cursorName,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(4)
}
}
};
box.AddChild(panel);
}
return new ScrollContainer
{
Children = { box },
VScrollEnabled = true,
HScrollEnabled = false,
Name = nameof(Tab.TabCursorShapes),
};
}
public void SelectTab(Tab tab)
{
_tabContainer.CurrentTab = (int)tab;
@@ -226,32 +280,14 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
TextEdit = 6,
RichText = 7,
SpriteView = 8,
TabCursorShapes = 9,
WrapContainer = 10,
}
}
internal sealed class UITestCommand : LocalizedCommands
internal abstract class BaseUITestCommand : LocalizedCommands
{
public override string Command => "uitest";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var window = new DefaultWindow { MinSize = new(800, 600) };
var control = new UITestControl();
window.OnClose += control.OnClosed;
window.Contents.AddChild(control);
window.OpenCentered();
}
}
internal sealed class UITest2Command : LocalizedCommands
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiMgr = default!;
public override string Command => "uitest2";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
public sealed override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
@@ -272,18 +308,10 @@ internal sealed class UITest2Command : LocalizedCommands
control.SelectTab(tab);
}
var window = _clyde.CreateWindow(new WindowCreateParameters
{
Title = Loc.GetString("cmd-uitest2-title"),
});
var root = _uiMgr.CreateWindowRoot(window);
window.DisposeOnClose = true;
window.RequestClosed += _ => control.OnClosed();
root.AddChild(control);
CreateWindow(control);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public sealed override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
@@ -294,4 +322,35 @@ internal sealed class UITest2Command : LocalizedCommands
return CompletionResult.Empty;
}
protected abstract void CreateWindow(UITestControl control);
}
internal sealed class UITestCommand : BaseUITestCommand
{
public override string Command => "uitest";
protected override void CreateWindow(UITestControl control)
{
var window = new DefaultWindow { MinSize = new(800, 600) };
window.OnClose += control.OnClosed;
window.Contents.AddChild(control);
window.OpenCentered();
}
}
internal sealed class UITest2Command : BaseUITestCommand
{
public override string Command => "uitest2";
protected override void CreateWindow(UITestControl control)
{
var window = new OSWindow
{
Title = Loc.GetString("cmd-uitest2-title"),
};
window.AddChild(control);
window.Closed += control.OnClosed;
window.Show();
}
}

View File

@@ -1,19 +0,0 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class VelocitiesCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
public override string Command => "showvelocities";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystems.GetEntitySystem<VelocityDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -0,0 +1,44 @@
#if TOOLS
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Client.Console.Commands;
internal sealed class ViewportClearAllCachedCommand : IConsoleCommand
{
[Dependency] private readonly IClydeInternal _clyde = default!;
public string Command => "vp_clear_all_cached";
public string Description => "Fires IClydeViewport.ClearCachedResources on all viewports";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
_clyde.ViewportsClearAllCached();
}
}
internal sealed class ViewportTestFinalizeCommand : IConsoleCommand
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
public string Command => "vp_test_finalize";
public string Description => "Creates a viewport, renders it once, then leaks it (finalizes it).";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var vp = _clyde.CreateViewport(new Vector2i(1920, 1080), nameof(ViewportTestFinalizeCommand));
vp.Eye = _eyeManager.CurrentEye;
vp.Render();
// Leak it.
}
}
#endif // TOOLS

View File

@@ -85,7 +85,7 @@ namespace Robust.Client.Console
MouseFilter = MouseFilterMode.Stop;
Result = result;
var compl = new FormattedMessage();
var dim = Color.FromHsl((0f, 0f, 0.8f, 1f));
var dim = Color.FromHsl(new Vector4(0f, 0f, 0.8f, 1f));
// warning: ew ahead
string basen = "default";

View File

@@ -82,7 +82,7 @@ namespace Robust.Client.Debugging
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
{
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
if (TryComp(ent, out MetaDataComponent? meta))
{
text.AppendLine($"uid: {ent}, {meta.EntityName}");
}

View File

@@ -1,140 +1,221 @@
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using System.Numerics;
namespace Robust.Client.Debugging
namespace Robust.Client.Debugging;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
private bool _debugVelocities;
private bool _debugAngularVelocities;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
public bool DebugPositions
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public bool DebugPositions
get => _debugPositions;
set
{
get => _debugPositions;
set
if (value == DebugPositions)
{
if (value == DebugPositions)
{
return;
}
return;
}
_debugPositions = value;
_debugPositions = value;
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, EntityManager, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
/// <summary>
/// Toggles the visual overlay of the local rotation.
/// </summary>
public bool DebugRotations
/// <summary>
/// Toggles the visual overlay of the rotation for each entity on screen.
/// </summary>
public bool DebugRotations
{
get => _debugRotations;
set
{
get => _debugRotations;
set
if (value == DebugRotations)
{
if (value == DebugRotations)
{
return;
}
return;
}
_debugRotations = value;
_debugRotations = value;
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, EntityManager));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
}
}
private sealed class EntityPositionOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the local velocity for each entity on screen.
/// </summary>
public bool DebugVelocities
{
get => _debugVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(EntityLookupSystem lookup, IEntityManager entityManager, SharedTransformSystem transform)
if (value == DebugVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
_transform = transform;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(entity);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
_overlayManager.AddOverlay(new EntityVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityVelocityOverlay>();
}
}
}
private sealed class EntityRotationOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the angular velocity for each entity on screen.
/// </summary>
public bool DebugAngularVelocities
{
get => _debugAngularVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityRotationOverlay(EntityLookupSystem lookup, IEntityManager entityManager)
if (value == DebugAngularVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugAngularVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityAngularVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
_overlayManager.AddOverlay(new EntityAngularVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityAngularVelocityOverlay>();
}
}
}
private sealed class EntityPositionOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
}
}
private sealed class EntityRotationOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
}
}
private sealed class EntityVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = 0.2f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var localVelocity = physicsComp.LinearVelocity;
if (localVelocity != Vector2.Zero)
worldHandle.DrawLine(center, center + localVelocity * multiplier, Color.Yellow);
}
}
}
private sealed class EntityAngularVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = (float)(0.2 / (2 * System.Math.PI));
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var angularVelocity = physicsComp.AngularVelocity;
if (angularVelocity != 0.0f)
worldHandle.DrawCircle(center, angularVelocity * multiplier, angularVelocity > 0 ? Color.Magenta : Color.Blue, false);
}
}
}
}

View File

@@ -413,8 +413,9 @@ namespace Robust.Client.Debugging
}
var body = bodyEnt.Comp;
var meta = _entityManager.GetComponent<MetaDataComponent>(bodyEnt);
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner} ({meta.EntityName})");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;

View File

@@ -0,0 +1,53 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.Shared.Utility;
namespace Robust.Client.Debugging;
[ToolshedCommand]
internal sealed class OverlayCommand : ToolshedCommand
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _factory = default!;
[CommandImplementation("toggle")]
internal void Toggle([CommandArgument(customParser:typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
if (!overlay.IsSubclassOf(typeof(Overlay)))
throw new ArgumentException("Type must be a subclass of overlay");
if (_overlay.HasOverlay(overlay))
Remove(overlay);
else
Add(overlay);
}
[CommandImplementation("add")]
internal void Add([CommandArgument(customParser: typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
if (!overlay.IsSubclassOf(typeof(Overlay)))
throw new ArgumentException("Type must be a subclass of overlay");
if (!overlay.HasParameterlessConstructor())
throw new ArgumentException("Type must have parameterless constructor");
if (_overlay.HasOverlay(overlay))
return;
// TODO OVERLAYS Give overlays the ContentAccessAllowedAttribute?
var instance = (Overlay) _factory.CreateInstanceUnchecked(overlay, oneOff: true);
if (instance is IPostInjectInit init)
init.PostInject();
_overlay.AddOverlay(instance);
}
[CommandImplementation("remove")]
public void Remove([CommandArgument(customParser: typeof(ReflectionTypeParser<Overlay>))] Type overlay)
{
_overlay.RemoveOverlay(overlay);
}
}

View File

@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Debugging.Overlays;
/// <summary>
/// This is an abstract helper class that can be used to create simple debug overlays that need to render tile based data.
/// </summary>
[UsedImplicitly]
public abstract class TileDebugOverlay : Overlay, IPostInjectInit
{
[Dependency] protected readonly IEntityManager Entity = default!;
[Dependency] protected readonly IEyeManager Eye = default!;
[Dependency] protected readonly IMapManager MapMan = default!;
[Dependency] protected readonly IInputManager Input = default!;
[Dependency] protected readonly IUserInterfaceManager Ui = default!;
[Dependency] protected readonly IResourceCache Cache = default!;
protected SharedTransformSystem Transform = default!;
protected MapSystem Map = default!;
protected EntityLookupSystem Lookup = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
protected Font Font = default!;
protected List<Entity<MapGridComponent>> Grids = new();
public void PostInject()
{
Transform = Entity.System<SharedTransformSystem>();
Map = Entity.System<MapSystem>();
Lookup = Entity.System<EntityLookupSystem>();
var font = Cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf");
Font = new VectorFont(font, 8);
Init();
}
protected virtual void Init()
{
}
protected internal override void Draw(in OverlayDrawArgs args)
{
Grids.Clear();
if (args.Viewport.Eye?.Position.MapId is not {} map || map == MapId.Nullspace)
return;
MapMan.FindGridsIntersecting(map, args.WorldBounds, ref Grids);
foreach (var grid in Grids)
{
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen(args, grid);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args, grid);
break;
}
}
Grids.Clear();
}
protected virtual void DrawScreen(in OverlayDrawArgs args, Entity<MapGridComponent> grid)
{
var handle = args.ScreenHandle;
var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner);
var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2);
var tilesEnumerator = Map.GetLocalTilesEnumerator(grid, grid, gridBounds);
while (tilesEnumerator.MoveNext(out var tile))
{
var tileBounds = Lookup.GetLocalBounds(tile, grid.Comp.TileSize);
if (!gridBounds.Intersects(tileBounds))
continue;
var screenTileCentre = Eye.WorldToScreen(Vector2.Transform(tileBounds.Center, matrix));
DrawTileText(handle, screenTileCentre, tile.GridIndices, grid);
}
// Draw mouse tooltip
DrawTooltip(handle);
}
protected virtual void DrawTooltip(DrawingHandleScreen handle)
{
var mousePos = Input.MouseScreenPosition;
if (!mousePos.IsValid)
return;
if (Ui.MouseGetControl(mousePos) is not IViewportControl viewport)
return;
var coords = viewport.PixelToMap(mousePos.Position);
if (!MapMan.TryFindGridAt(coords, out var grid, out var comp))
return;
var local = Map.WorldToLocal(grid, comp, coords.Position);
var x = (int) Math.Floor(local.X / comp.TileSize);
var y = (int) Math.Floor(local.Y / comp.TileSize);
var indices = new Vector2i(x, y);
DrawTooltip(handle, mousePos.Position, local, indices, (grid, comp));
}
/// <summary>
/// Draw a tooltip around the mouse
/// </summary>
/// <param name="mouseScreen">The mouse's screen coordinates</param>
/// <param name="mouseLocal">The mouse's local grid coordinates</param>
/// <param name="indices">The mouse's tile indices</param>
/// <param name="grid">The grid that the mouse is hovering over</param>
protected virtual void DrawTooltip(DrawingHandleScreen handle, Vector2 mouseScreen, Vector2 mouseLocal, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetTooltip(mouseLocal, indices, grid) is not { } text)
return;
var lineHeight = Font.GetLineHeight(1f);
var offset = new Vector2(0, lineHeight);
handle.DrawString(Font, mouseScreen - offset, text);
}
protected virtual void DrawTileText(DrawingHandleScreen handle, Vector2 tileCentre, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetText(indices, grid) is {} text)
handle.DrawString(Font, tileCentre, text);
}
protected virtual void DrawWorld(in OverlayDrawArgs args, Entity<MapGridComponent> grid)
{
var handle = args.WorldHandle;
var (_, _, matrix, invMatrix) = Transform.GetWorldPositionRotationMatrixWithInv(grid.Owner);
var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(grid.Comp.TileSize * 2);
var tilesEnumerator = Map.GetLocalTilesEnumerator(grid, grid, gridBounds);
while (tilesEnumerator.MoveNext(out var tile))
{
handle.SetTransform(matrix);
var tileBounds = Lookup.GetLocalBounds(tile, grid.Comp.TileSize);
if (gridBounds.Intersects(tileBounds))
DrawTile(handle, tileBounds, tile.GridIndices, grid);
}
handle.SetTransform(Matrix3x2.Identity);
}
protected virtual void DrawTile(DrawingHandleWorld handle, Box2 tile, Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetColor(indices, grid) is not { } color)
return;
handle.DrawRect(tile, color.Border, filled: false);
handle.DrawRect(tile, color.Fill, filled: true);
}
/// <summary>
/// Get text that will be rendered in a grid tile.
/// </summary>
protected abstract string? GetText(Vector2i indices, Entity<MapGridComponent> grid);
/// <summary>
/// Get tooltip text that will be shown next to the mouse.
/// </summary>
/// <param name="mousePos">The mouse's position relative to the grid.</param>
/// <param name="gridIndices">The grid indices corresponding to the mouse's position</param>
/// <param name="grid">The grid that the mouse is over.</param>
protected abstract string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity<MapGridComponent> grid);
/// <summary>
/// Get a border & fill color that will be used to draw a grid tile.
/// </summary>
protected abstract (Color Fill, Color Border)? GetColor(Vector2i indices, Entity<MapGridComponent> grid);
}
/// <summary>
/// Variant of <see cref="TileDebugOverlay"/> that exists to draw simple float information for each tile.
/// </summary>
public abstract class TileFloatDebugOverlay : TileDebugOverlay
{
protected virtual float MinValue => 0;
protected virtual float MaxValue => 1;
protected abstract float? GetData(Vector2i indices, Entity<MapGridComponent> grid);
protected override string? GetText(Vector2i indices, Entity<MapGridComponent> grid)
{
return GetData(indices, grid)?.ToString("F2");
}
protected override string? GetTooltip(Vector2 mousePos, Vector2i indices, Entity<MapGridComponent> grid)
{
return GetData(indices, grid)?.ToString("F2");
}
protected override (Color Fill, Color Border)? GetColor(Vector2i indices, Entity<MapGridComponent> grid)
{
if (GetData(indices, grid) is not { } value)
return null;
var color = Gradient(value, MinValue, MaxValue);
return (color.WithAlpha(0.2f), color);
}
/// <summary>
/// Simple yellow -> orange -> red gradient.
/// </summary>
public Color Gradient(float value, float min, float max)
{
// map min to 1, max to 0
value = (value - min) / (max - min);
return value < 0.5f
? Color.InterpolateBetween(Color.Yellow, Color.Orange, value * 2)
: Color.InterpolateBetween(Color.Orange, Color.Red, (value - 0.5f) * 2);
}
}

View File

@@ -8,12 +8,14 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SDL3;
namespace Robust.Client
{
internal partial class GameController : IPostInjectInit
{
private IGameLoop? _mainLoop;
private bool _dontStart;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
@@ -93,6 +95,8 @@ namespace Robust.Client
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
_displayMode = mode;
if (!StartupSystemSplash(options, logHandlerFactory))
{
_logger.Fatal("Failed to start game controller!");
@@ -159,8 +163,11 @@ namespace Robust.Client
return;
}
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
if (!_dontStart)
{
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
}
CleanupGameThread();
}

View File

@@ -31,6 +31,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -93,6 +94,10 @@ namespace Robust.Client
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
[Dependency] private readonly LoadingScreenManager _loadscr = default!;
private IWebViewManagerHook? _webViewHook;
@@ -107,6 +112,8 @@ namespace Robust.Client
private ResourceManifestData? _resourceManifest;
private DisplayMode _displayMode;
public void SetCommandLineArgs(CommandLineArgs args)
{
_commandLineArgs = args;
@@ -127,27 +134,39 @@ namespace Robust.Client
return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? "";
}
public bool ShowLoadingBar()
{
return _resourceManifest!.ShowLoadingBar ?? _configurationManager.GetCVar(CVars.LoadingShowBar);
}
internal bool StartupContinue(DisplayMode displayMode)
{
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(GameTitle());
_loadscr.Initialize(42);
_taskManager.Initialize();
_parallelMgr.Initialize();
_loadscr.BeginLoadingSection("Init graphics", dontRender: true);
_clyde.InitializePostWindowing();
_clyde.SetWindowTitle(GameTitle());
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_audio.InitializePostWindowing, "Init audio");
_loadscr.LoadingStep(_taskManager.Initialize, _taskManager);
_loadscr.LoadingStep(_parallelMgr.Initialize, _parallelMgr);
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
// Load optional Robust modules.
LoadOptionalRobustModules(displayMode, _resourceManifest!);
_loadscr.LoadingStep(_systemFontManager.Initialize, "System fonts");
// Load optional Robust modules.
_loadscr.LoadingStep(() => LoadOptionalRobustModules(displayMode, _resourceManifest!), "Robust Modules");
_loadscr.BeginLoadingSection(_modLoader);
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!ContentStart);
var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1";
_modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing);
if (!LoadModules())
return false;
@@ -156,15 +175,23 @@ namespace Robust.Client
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
_serializationManager.Initialize();
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_serializationManager.Initialize, _serializationManager);
_loadscr.LoadingStep(_loc.Initialize, _loc);
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PreInit), "Content PreInit");
// Finish initialization of WebView if loaded.
_webViewHook?.Initialize();
_loadscr.LoadingStep(() =>
{
// Finish initialization of WebView if loaded.
if (_webViewHook != null)
_loadscr.LoadingStep(_webViewHook.Initialize, _webViewHook);
},
"WebView init");
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.Init), "Content Init");
// Start bad file extensions check after content init,
// in case content screws with the VFS.
@@ -173,41 +200,51 @@ namespace Robust.Client
_configurationManager,
_logManager.GetSawmill("res"));
_resourceCache.PreloadTextures();
_networkManager.Initialize(false);
_configurationManager.SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_loadscr.LoadingStep(_resourceCache.PreloadTextures, "Texture preload");
_loadscr.LoadingStep(() => { _networkManager.Initialize(false); }, _networkManager);
_loadscr.LoadingStep(_configurationManager.SetupNetworking, _configurationManager);
_loadscr.LoadingStep(_serializer.Initialize, _serializer);
_loadscr.LoadingStep(_inputManager.Initialize, _inputManager);
_loadscr.LoadingStep(_console.Initialize, _console);
// Make sure this is done before we try to load prototypes,
// avoid any possibility of race conditions causing the check to not finish
// before prototype load.
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
_loadscr.LoadingStep(
() => ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions),
"Check bad file extensions");
_reflectionManager.Initialize();
_loadscr.LoadingStep(_reload.Initialize, _reload);
_loadscr.LoadingStep(_reflectionManager.Initialize, _reflectionManager);
_loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager);
_loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager);
_loadscr.BeginLoadingSection(_prototypeManager);
_prototypeManager.Initialize();
_prototypeManager.LoadDefaultPrototypes();
_xamlProxyManager.Initialize();
_xamlHotReloadManager.Initialize();
_userInterfaceManager.Initialize();
_eyeManager.Initialize();
_entityManager.Initialize();
_mapManager.Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_tagManager.Initialize();
_protoLoadMan.Initialize();
_netResMan.Initialize();
_replayLoader.Initialize();
_replayPlayback.Initialize();
_replayRecording.Initialize();
_userInterfaceManager.PostInitialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init");
_loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager);
_loadscr.LoadingStep(_entityManager.Initialize, _entityManager);
_loadscr.LoadingStep(_mapManager.Initialize, _mapManager);
_loadscr.LoadingStep(_gameStateManager.Initialize, _gameStateManager);
_loadscr.LoadingStep(_placementManager.Initialize, _placementManager);
_loadscr.LoadingStep(_viewVariablesManager.Initialize, _viewVariablesManager);
_loadscr.LoadingStep(_scriptClient.Initialize, _scriptClient);
_loadscr.LoadingStep(_client.Initialize, _client);
_loadscr.LoadingStep(_discord.Initialize, _discord);
_loadscr.LoadingStep(_tagManager.Initialize, _tagManager);
_loadscr.LoadingStep(_protoLoadMan.Initialize, _protoLoadMan);
_loadscr.LoadingStep(_netResMan.Initialize, _netResMan);
_loadscr.LoadingStep(_replayLoader.Initialize, _replayLoader);
_loadscr.LoadingStep(_replayPlayback.Initialize, _replayPlayback);
_loadscr.LoadingStep(_replayRecording.Initialize, _replayRecording);
_loadscr.LoadingStep(_userInterfaceManager.PostInitialize, "UI postinit");
// Init stuff before this if at all possible.
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PostInit), "Content PostInit");
_loadscr.Finish();
if (_commandLineArgs?.Username != null)
{
@@ -268,6 +305,9 @@ namespace Robust.Client
}
};
_configurationManager.OnValueChanged(CVars.DisplayMaxFPS, _ => UpdateVsyncConfig());
_configurationManager.OnValueChanged(CVars.DisplayVSync, _ => UpdateVsyncConfig(), invokeImmediately: true);
_clyde.Ready();
if (_resourceManifest!.AutoConnect &&
@@ -348,9 +388,6 @@ namespace Robust.Client
var userDataDir = GetUserDataDir();
_configurationManager.Initialize(false);
// MUST load cvars before loading from config file so the cfg manager is aware of secure cvars.
// So SECURE CVars are blacklisted from config.
_configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client
_configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared
@@ -382,7 +419,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
@@ -414,7 +451,8 @@ namespace Robust.Client
_configurationManager.OverrideConVars(new[]
{
(CVars.DisplayWindowIconSet.Name, WindowIconSet()),
(CVars.DisplaySplashLogo.Name, SplashLogo())
(CVars.DisplaySplashLogo.Name, SplashLogo()),
(CVars.LoadingShowBar.Name, ShowLoadingBar().ToString()),
});
}
@@ -479,10 +517,18 @@ namespace Robust.Client
public void Shutdown(string? reason = null)
{
DebugTools.AssertNotNull(_mainLoop);
if (_mainLoop == null)
{
if (!_dontStart)
{
_logger.Info($"Shutdown called before client init completed: {reason ?? "No reason provided"}");
_dontStart = true;
}
return;
}
// Already got shut down I assume,
if (!_mainLoop!.Running)
if (!_mainLoop.Running)
{
return;
}
@@ -704,6 +750,30 @@ namespace Robust.Client
}
private void UpdateVsyncConfig()
{
if (_displayMode == DisplayMode.Headless)
return;
var vsync = _configurationManager.GetCVar(CVars.DisplayVSync);
var maxFps = Math.Clamp(_configurationManager.GetCVar(CVars.DisplayMaxFPS), 0, 10_000);
_clyde.VsyncEnabled = vsync;
if (_mainLoop == null)
return;
if (vsync || maxFps == 0)
{
_mainLoop.SleepMode = SleepMode.None;
}
else
{
_mainLoop.SleepMode = SleepMode.Limit;
_mainLoop.LimitMinFrameTime = TimeSpan.FromSeconds(1.0 / maxFps);
}
}
internal enum DisplayMode : byte
{
Headless,

View File

@@ -0,0 +1,123 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class ClientEntityManager
{
public override EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
var ent = SpawnAttachedTo(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
{
var ent = Spawn(protoName, overrides, doMapInit);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var ent = Spawn(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
{
var ent = SpawnAtPosition(protoName, coordinates, overrides);
FlagPredicted(ent);
return ent;
}
public override bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnNextTo(protoName, target, out uid, xform, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
{
var ent = SpawnNextToOrDrop(protoName, target, xform, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName,
containerUid,
containerId,
out inserted,
xform,
containerComp,
overrides);
FlagPredicted(ent);
return ent;
}
public override void FlagPredicted(Entity<MetaDataComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp))
return;
DebugTools.Assert(IsClientSide(ent.Owner, ent.Comp));
EnsureComponent<PredictedSpawnComponent>(ent.Owner);
// TODO: Need to map call site or something, needs to be consistent between client and server.
}
}

View File

@@ -29,6 +29,9 @@ namespace Robust.Client.GameObjects
internal event Action? AfterStartup;
internal event Action? AfterShutdown;
private readonly Queue<EntityUid> _queuedPredictedDeletions = new();
private readonly HashSet<EntityUid> _queuedPredictedDeletionsSet = new();
public override void Initialize()
{
SetupNetworking();
@@ -117,7 +120,7 @@ namespace Robust.Client.GameObjects
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
{
// TODO Prediction
// does the client actually need to dirty the field?
@@ -216,6 +219,35 @@ namespace Robust.Client.GameObjects
base.TickUpdate(frameTime, noPredictions, histogram);
}
internal override void ProcessQueueudDeletions()
{
base.ProcessQueueudDeletions();
while (_queuedPredictedDeletions.TryDequeue(out var uid))
{
if (!MetaQuery.TryGetComponentInternal(uid, out var meta))
continue;
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
continue;
var xform = TransformQuery.GetComponentInternal(uid);
if (meta.NetEntity.IsClientSide())
{
DeleteEntity(uid, meta, xform);
}
else
{
_xforms.DetachEntity(uid, xform, meta, null);
// base call bypasses IGameTiming.InPrediction check
// This is pretty janky and there should be a way for the client to dirty an entity outside of prediction
// TODO PREDICTION Is actually needed after the current predicted deletion fix?
base.Dirty(uid, xform, meta);
}
}
_queuedPredictedDeletionsSet.Clear();
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, bool recordReplay = true)
{
@@ -291,5 +323,62 @@ namespace Robust.Client.GameObjects
}
}
#endregion
/// <inheritdoc />
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
// So there's 3 scenarios:
// 1. Networked entity we just move to nullspace and rely on state handling.
// 2. Clientside predicted entity we delete and rely on state handling.
// 3. Clientside only entity that actually needs deleting here.
if (ent.Comp1.NetEntity.IsClientSide())
{
DeleteEntity(ent, ent.Comp1, ent.Comp2);
}
else
{
_xforms.DetachEntity(ent, ent.Comp2);
}
}
public override bool IsQueuedForDeletion(EntityUid uid)
=> QueuedDeletionsSet.Contains(uid) || _queuedPredictedDeletions.Contains(uid);
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?> ent)
{
// Some UIs get disposed after entity-manager has shut down and already deleted all entities.
if (!Started)
return;
if (IsQueuedForDeletion(ent.Owner))
return;
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
if (ent.Comp.NetEntity.IsClientSide())
{
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
// base call to skip that.
// TODO create override that takes in metadata comp
base.QueueDeleteEntity(ent);
}
else
{
if (!_queuedPredictedDeletionsSet.Add(ent.Owner))
return;
_queuedPredictedDeletions.Enqueue(ent.Owner);
}
}
}
}

View File

@@ -1,9 +1,11 @@
using Robust.Shared.GameObjects;
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
[Obsolete]
public partial interface IRenderableComponent : IComponent
{
int DrawDepth { get; set; }

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;

View File

@@ -10,21 +10,21 @@ using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class ShowSpriteBBCommand : LocalizedCommands
public sealed class ShowSpriteBBCommand : LocalizedEntityCommands
{
[Dependency] private readonly SpriteBoundsSystem _system = default!;
public override string Command => "showspritebb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<SpriteBoundsSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -40,7 +40,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
_overlay = new SpriteBoundsOverlay(EntityManager);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -55,18 +55,13 @@ namespace Robust.Client.GameObjects
private bool _enabled;
}
public sealed class SpriteBoundsOverlay : Overlay
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly IEntityManager _entityManager;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
{
_renderTree = renderTree;
_entityManager = entityManager;
}
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
protected internal override void Draw(in OverlayDrawArgs args)
{
@@ -74,10 +69,11 @@ namespace Robust.Client.GameObjects
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
{
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
var (sprite, xform) = entry;
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;

View File

@@ -14,7 +14,9 @@ namespace Robust.Client.GameObjects
private EntityQuery<AnimationPlayerComponent> _playerQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
#pragma warning disable CS0414
[Dependency] private readonly IComponentFactory _compFact = default!;
#pragma warning restore CS0414
public override void Initialize()
{
@@ -76,7 +78,7 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
var completedEvent = new AnimationCompletedEvent(uid, component, key, true);
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
}
@@ -95,7 +97,7 @@ namespace Robust.Client.GameObjects
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
component ??= EnsureComp<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
@@ -156,7 +158,7 @@ namespace Robust.Client.GameObjects
public bool HasRunningAnimation(EntityUid uid, string key)
{
return EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? component) &&
return TryComp(uid, out AnimationPlayerComponent? component) &&
component.PlayingAnimations.ContainsKey(key);
}
@@ -187,7 +189,7 @@ namespace Robust.Client.GameObjects
return;
}
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
var completedEvent = new AnimationCompletedEvent(entity.Owner, entity.Comp, key, false);
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
}
@@ -202,13 +204,33 @@ namespace Robust.Client.GameObjects
/// </summary>
public sealed class AnimationCompletedEvent : EntityEventArgs
{
/// <summary>
/// The entity associated with the event.
/// </summary>
public EntityUid Uid { get; init; }
/// <summary>
/// The animation player component associated with the entity this event was raised on.
/// </summary>
public AnimationPlayerComponent AnimationPlayer { get; init; }
/// <summary>
/// The key associated with the animation that was completed.
/// </summary>
public string Key { get; init; } = string.Empty;
/// <summary>
/// If true, the animation finished by getting to its natural end.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(Robust.Client.GameObjects.AnimationPlayerComponent,string)"/> or similar overloads.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(EntityUid,AnimationPlayerComponent,string)"/> or similar overloads.
/// </summary>
public bool Finished { get; init; }
public AnimationCompletedEvent(EntityUid uid, AnimationPlayerComponent animationPlayer, string key, bool finished = true)
{
Uid = uid;
AnimationPlayer = animationPlayer;
Key = key;
Finished = finished;
}
}
}

View File

@@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -58,7 +57,7 @@ namespace Robust.Client.GameObjects
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
return;
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
}
public override void ShutdownContainer(BaseContainer container)
@@ -232,7 +231,7 @@ namespace Robust.Client.GameObjects
return;
}
Insert(message.Entity, container);
Insert(message.Entity, container, force: true);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
@@ -303,7 +302,7 @@ namespace Robust.Client.GameObjects
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
{
var parentXform = TransformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
if (TryComp<ContainerManagerComponent>(parent, out var manager) && TryGetContainingContainer(parent, child, out var container, manager))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
lightOccluded = lightOccluded || container.OccludesLight;
@@ -344,7 +343,7 @@ namespace Robust.Client.GameObjects
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child, out var container))
if (TryGetContainingContainer(entity, child, out var container, manager))
{
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;

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