Compare commits

...

374 Commits

Author SHA1 Message Date
Pieter-Jan Briers
420f690a77 Version: 210.1.2 2024-03-10 21:09:05 +01:00
Pieter-Jan Briers
612320b947 YIPPEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE IMAGESHARP VULNERABILITY
(cherry picked from commit 859f150404)
2024-03-10 21:09:05 +01:00
Pieter-Jan Briers
ef0bc1a2e4 Version: 210.1.1 2024-02-17 22:17:52 +01:00
Pieter-Jan Briers
72ba484f5b Changelog for key binding fix PRs 2024-02-17 22:12:33 +01:00
Pieter-Jan Briers
a70e511fcb Change default of ButtonGroup.IsNoneSetAllowed to true.
This brings default ButtonGroup behavior back to before #4841.

The original comments in the code *did* clearly intend for the other behavior to be the default, but the code was blatantly bugged (whoops) so this didn't happen. Content relied on this A LOT and it's quite sane behavior regardless so just change the default back call it a day.
2024-02-17 22:09:23 +01:00
Errant
e7f9e95525 fix default keybinds not knowing their place (#4903)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-17 22:03:36 +01:00
nikthechampiongr
bd908f9db6 Invalid keybinds will no longer mess up your game. (#4902)
* Fix issues when saving invalid keybinds.

* Fix horrible thing I forgot to fix.

* Change error log to debug

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-02-17 21:54:35 +01:00
Leon Friedrich
f8cb1729a3 Version: 210.1.0 2024-02-16 11:21:40 +13:00
Leon Friedrich
fd9d5c8aa8 Make ButtonGroup setter behaviour consistent with comment 2024-02-16 11:19:14 +13:00
Pieter-Jan Briers
4677296934 Add IsNoneSetAllowed mode to ButtonGroup.
This allows a button group to have no button pressed by default, which is the behavior of most radio buttons.
2024-02-15 01:16:33 +01:00
Pieter-Jan Briers
708f5dd376 Un-hardcode C:\Windows in MidiManager
Keeping an eye out for our bros who put Windows on a drive that isn't C:
2024-02-14 14:32:05 +01:00
Pieter-Jan Briers
4a06acda32 NetUserId implement ISelfSerialize
For @VasilisThePikachu
2024-02-14 01:08:38 +01:00
Pieter-Jan Briers
e7beb2032b Version: 210.0.3
This version changes nothing but I need it because I pushed the previous
version after amending it which means the tag is the wrong commit SORRY.
2024-02-13 16:11:47 +01:00
Pieter-Jan Briers
c7bd75f800 Version: 210.0.2 2024-02-13 16:06:28 +01:00
Pieter-Jan Briers
b4165e8661 ALSO revert changes to TextureRect from #4841
Stretch modes are broken or something, SS14 lobby art looks wrong. Can't be arsed to debug it myself.
2024-02-13 16:05:46 +01:00
Pieter-Jan Briers
ad339b5bfd Version: 210.0.1 2024-02-13 15:30:20 +01:00
Pieter-Jan Briers
e1197af8ce Revert changes to TextureButton from #4841
Breaks SS14 stylesheets due to not responding to style properties anymore.

At least one of those seems to be unfixable (ModulateSelf usage) which makes me think we should just deprecate ModulateSelf instead. However I'm not fixing that here.
2024-02-13 15:29:46 +01:00
metalgearsloth
102cadf3a6 Version: 210.0.0 2024-02-13 18:26:01 +11:00
Hannah Giovanna Dawson
e7723b61bc Add UnicodeRange to sandbox (#4894)
* Add GetEncoding to sandbox (#4892)
Need this struct allowlisted to for nice unicode sanitization.

* Add UnicodeRanges too

* Changelog

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:22:11 +01:00
Moony
a9d17337a3 RT Patches for UI improvements (#4841)
* fix up buttons

* wah

* ough

* huhwuhuahsdhsfdj

* loud incorrect buzzer

* wawa

* Allow XmlnsDefinition

* wawa

* Release notes.

* Expose keybind loading.

* address reviews and other things

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:18:39 +01:00
Pieter-Jan Briers
74622bac83 Add DateTimeSerializer 2024-02-11 16:40:52 +01:00
Pieter-Jan Briers
a3047b1687 More warning fixes 2024-02-11 15:51:07 +01:00
Pieter-Jan Briers
3a55118143 Replace CVar OnValueChanged in systems with Subs.CVar 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
3c5fbc648a Add Subs.CVar helper for subscribing to CVar changes from entity systems 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
f9c39bce0b Use ValueList for EntitySystem subscriptions list.
That's a hundred something lists just gone and I don't have to ??=
2024-02-11 13:29:27 +01:00
Hannah Giovanna Dawson
0e8c803c0f Add GetEncoding to sandbox (#4892) 2024-02-10 17:59:13 +01:00
c4llv07e
6bb7b88c69 Save discord rich presense to the user config (#4884)
Signed-off-by: c4llv07e <kseandi@gmail.com>
2024-02-02 00:36:59 +01:00
metalgearsloth
9e0fc7017c Version: 209.0.1 2024-02-02 01:02:42 +11:00
metalgearsloth
76317b7ab3 Fix bad import (#4882)
fork why are you so SLOW.
2024-02-02 01:01:21 +11:00
metalgearsloth
d5f4d4bf2f Version: 209.0.0 2024-02-02 00:40:18 +11:00
metalgearsloth
156d1a6b14 Add SharedMapSystem helper for tile methods (#4845)
* Add SharedMapSystem helper for tile methods

* weh

* release

* Resolves

* Move some of these

* note

* Remove obsolete

* note
2024-02-02 00:39:20 +11:00
metalgearsloth
3fa456fd44 Fix relay refreshes where the entity gets contained to itself (#4865)
If the parent is somewhere below the transform hierarchy should still be okay, this just fixes where something getting pulled gets picked up as pulling doesn't seem to be handling this anymore.
2024-02-01 20:33:33 +11:00
LordCarve
bf9bb46154 Added ReferenceEquals() tests for RobustIntegrationTest (#4839)
* Added a test that checks that RobustIntegrationTest Client and Server do not end up with same sub-ComponentState reference objects.

* Un-ignore and adjust the test.

* review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-02-01 20:22:52 +11:00
metalgearsloth
fb3da0b53c Add entitylookup methods for parent / map (#4870)
* Add entitylookup methods for parent / map

Content's done it a bunch so make it reusable.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-31 17:21:41 +01:00
Pieter-Jan Briers
5c635c09b4 Replay int overflows, fixed again (#4878)
* Reapply "Fix replay int overflow issues." (#4802)

This reverts commit 32049e34f2.

* IConfigurationManager.LoadDefaultsFromTomlStream now does required type conversions.

This fixes scenarios like loading of `long` CVars.
2024-01-31 17:17:28 +01:00
Pieter-Jan Briers
fad539212d Cache TotalTicks in MIDI renderer
This is the "total length" of the MIDI file, but it's not cached by FluidSynth and expensive to calculate.

This property was used in the render code (for local files only) and the instrument menu. Caching it drastically improves performance.
2024-01-31 00:29:03 +01:00
Pieter-Jan Briers
7275302639 Stop multithreading FluidSynth with synth.cpu-cores.
It makes no sense for our use case, and it caused FluidSynth to allocate a different thread pool *per* mixer. And every one of those threads have *high* priority. That's like *really* bad.

Furthermore, it was based on ParallelProcessCount which is currently bugged, and because of that we were always allocating  256 (!!!) real OS threads for a MIDI synthesizer. CHRIST. (fix for ParallelProcessCount is separate)

I assume this is responsible for a ton of people's MIDI lag, it just murdering their PC's CPU scheduler.

The ability to multithread FluidSynth still exists as a CVar but it'll default to 1 and I don't think it makes sense to ever change it.

Also there was code to dynamically change the parameter, as far as I could test this just always crashed the process so out it goes.
2024-01-31 00:27:30 +01:00
metalgearsloth
5057ff97a3 Add MaxDimension property to Box2 (#4871)
* Add MaxDimension property to Box2

Sometimes I want to pretend it's a circle radius.

* a

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:28:05 +01:00
metalgearsloth
754d5a1fbb Add GetLocalPosition to controls (#4872)
* Add GetLocalPosition to controls

In my case I want the mouse's position inside of the control to show something under it unless there's a better way.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:27:28 +01:00
Kara
7cee5b67a7 More easings (#4876)
* More easings

* yeah
2024-01-30 13:24:16 +01:00
Kara
21729e7e48 Smarter EntitySystem sawmill name conversion (#4875) 2024-01-29 22:52:39 +01:00
metalgearsloth
394d1e6cc2 Add global rectangles for controls (#4873)
* Add global rectangles for controls

Like my other PR used to check if mouse is inbounds on the control without doing some skrunkly caching with mousemove.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-29 11:08:02 +01:00
metalgearsloth
f73d7f7285 Add dotted line drawing to screen handle (#4874)
Probably needs anti-aliasing but idk an easy way to do it.
2024-01-29 11:07:15 +01:00
DrSmugleaf
e505cfffd8 Fix TileEdgeOverlay flickering with tiles that are barely out of view (#4868) 2024-01-29 14:13:19 +11:00
collinlunn
d0fe3591ef Fixes parsing of ShaderBlendMode in ShaderPrototype (#4867)
* Fixes parsing of ShaderBlendMode in ShaderPrototype

* Changes ShaderPrototype parser to use case insensitive overload
2024-01-28 12:20:06 -08:00
James Simonson
1868f32457 Exposed "Bottom Margin" to itemlist (#4862)
* Exposed "Bottom Margin" to itemlist

* Changed from ItemBottomMargin to ItemSeparation

* Update ItemList.cs
2024-01-27 17:26:37 +01:00
metalgearsloth
ca82767b07 Version: 208.0.0 2024-01-25 17:47:44 +11:00
Kevin Zheng
76024330a7 Do not crash failing to load user keybindings (#4844) 2024-01-25 17:44:34 +11:00
metalgearsloth
21e8107eb1 Don't serialize metadata flags (#4861)
* Don't serialize metadata flags

Probably fine, saves us saving it to map files.

* Release notes
2024-01-25 17:44:16 +11:00
metalgearsloth
bcaa97a79b Version: 207.1.0 2024-01-21 18:55:44 +11:00
metalgearsloth
19727f6a25 Localise merge_grids command (#4850) 2024-01-21 18:20:21 +11:00
metalgearsloth
2102b96323 Update uploadfile dependencies (#4849)
* Update uploadfile dependencies

Also removed the 1 warning in there.

* Thanks rider
2024-01-21 18:20:13 +11:00
metalgearsloth
59b3ffda4f Add grid merging (#3627)
* Add grid merging

* More preliminary cleanup

* Fixes

* Fixes

* weh

* tweaks

* Tests

* weh

* Fix direction test

* Release notes
2024-01-21 17:41:04 +11:00
metalgearsloth
aae929966c Add NotNullWhen true to EntMan HasComp (#4848)
The proxy methods already have it but not the interface itself.
2024-01-20 15:03:47 +11:00
metalgearsloth
0cf842cacc Version: 207.0.0 2024-01-19 13:30:24 +11:00
metalgearsloth
96885c5b53 Quick lookup fix (#4847)
* Quick lookup fix

Forgor to push, just makes the Box2Rotated query use polyshape so it's accurate.

* a
2024-01-19 13:28:29 +11:00
metalgearsloth
d45ce7742e Entitylookup approx / shape changes (#4842)
* Entitylookup approx / shape changes

- Make the shape queries respect the approx flag.
- Make everything use shape queries.
- Hopefully reduce some of the internal cruft.
- Add some new methods I need for trade station.

* Bunch of deduplication

* Remove some more duplication abuse

* Remove intersection duplication

* Bunch more cleanup

* MapManager rejig

* Fix some stuff

* More fixes

* Release notes

* Fix TryFindGrid

* Sensor check

* Fix query

* Fix map queries

* More cleanup

* Fix whatever this is.

* also dis

* Fix entity query

* Smol optimisations

* Also this
2024-01-19 13:02:22 +11:00
Kot
50e27fd204 Fix full state updates (#4833)
* Clear entities seen by a session when the full update is requested

* Disable PVS budged for the full update

It helps preventing weird intermediate states when a client observes
themselves in a void with all the alert notifications going wild.
However it introduces a spike on CPU and networking.

* Add changelog line

* Move the changelog line to the proper place

* Update RELEASE-NOTES.md

* Revert "Disable PVS budged for the full update"

This reverts commit 6976ca04b8.

* Update RELEASE-NOTES.md

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-01-18 17:40:55 +13:00
Pieter-Jan Briers
af98933173 Isolate net messages in integration tests. (#4838)
* Isolate net messages in integration tests.

Integration tests don't use Lidgren to connect client and send, instead they just use some in-process channels to communicate. Because of this, the original implementation of net messages *directly* passed the net message instances between client and server instances. This caused issues whenever content would mutate data in a NetMessage after it "passed through".

Now we run the messages through WriteToBuffer() and ReadFromBuffer() so they pass through binary serialization. This means there's no more implicit sharing of the objects.

Note that this requires some trickery: Lidgren's message types have internal constructors. Really ideally we'd change the engine to make this more testable... but that's a content breaking change. Instead I just added InternalsVisibleTo to Lidgren so we can mess with it. We maintain the library ourselves anyways I can do what I want.

Fixes #4836

* Register Robust.UnitTesting as assembly for reflection.

This is necessary so that serialized types in the assembly can be picked up by NetSerializer.

Have to disable automatic reflection on all entity systems/components that tests register manually right now, because otherwise tests break.

* Stop shallow cloning specific net messages in integration tests.

This isn't necessary anymore now that we have a thorough fix.

* Wow I really forgot to copy-paste that line to the other side huh.

* Add test that serializer hash matches.

* Another test one I missed earlier.

* Changelog
2024-01-16 21:04:39 +01:00
metalgearsloth
e357dada65 Version: 206.0.0 2024-01-14 19:13:14 +11:00
metalgearsloth
19c48862e2 Entitylookup tweaks (#4808)
* Entitylookup tweaks

- Removed a dupe internal method.
- Removed some dupe internal code.
- Renamed some methods in line with local vs non-local.

* Update release

* Also this
2024-01-14 19:10:33 +11:00
metalgearsloth
448ce94b35 Make MIDI update rate comically low (#4830)
Was around this before but apparently avoiding lock contention not hard enough.
2024-01-14 18:19:56 +11:00
metalgearsloth
3681b7f0d5 Cleanup and tweak teleport commands (#4835)
* Cleanup and tweak teleport commands

- Cleanup the code duplication.
- Teleports to grid-local center instead.

* Release notes.
2024-01-14 17:44:02 +11:00
DrSmugleaf
7b3c883653 Add support for forcing prototypes in a file or directory to be parsed as abstract (#4829)
* Add support for forcing prototypes in a file or directory to be parsed as abstract

* Simplify mapping add abstract true call

* Fix docs

* Address reviews
2024-01-13 03:51:36 -08:00
Pieter-Jan Briers
c81004ddb4 Skip serialization generator with SkipRobustAnalyzer
Skips it in GLFW and Lidgren.
2024-01-12 23:18:53 +01:00
Pieter-Jan Briers
f844011348 Version: 205.0.0 2024-01-12 23:08:42 +01:00
Pieter-Jan Briers
0094040d68 Dependency update / fixes / skrungle bungle (#4825)
* Move to Central Package Management.

Allows us to store NuGet package versions all in one place. Yay!

* Update NuGet packages and fix code for changes.

Notable:

Changes to ILVerify.
Npgsql doesn't need hacks for inet anymore, now we need hacks to make the old code work with this new reality.
NUnit's analyzers are already complaining and I didn't even update it to 4.x yet.
TerraFX changed to GetLastSystemError so error handling had to be changed.
Buncha APIs have more NRT annotations.

* Remove dotnet-eng NuGet package source.

I genuinely don't know what this was for, and Central Package Management starts throwing warnings about it, so YEET.

* Fix double loading of assemblies due to ALC shenanigans.

Due to how the "sideloading" code for the ModLoader was set up, it would first try to load Microsoft.Extensions.Primitives from next to the content dll. But we already have that library in Robust!

Chaos ensues.

We now try to forcibly prioritize loading from the default ALC first to avoid this.

* Remove Robust.Physics project.

Never used.

* Remove erroneous NVorbis reference.

Should be VorbisPizza and otherwise wasn't used.

* Sandbox fixes

* Remove unused unit test package references.

Castle.Core and NUnit.ConsoleRunner.

* Update NUnit to 4.0.1

This requires replacing all the old assertion methods because they removed them 🥲

* Mute CA1416 (platform check) errors

TerraFX started annotating APIs with this and I can't be arsed to entertain this analyzer so out it goes.

* Fine ya cranky, no more CPM for Robust.Client.Injectors

* Changelog

* Oh so that's what dotnet-eng was used for. Yeah ok that makes sense.

* Central package management for remaining 2 robust projects

* Ok that was a bad idea let's just use NUnit 3 on the analyzer test project

* Oh right forgot to remove this one

* Update to a newer version of RemoteExecutor

* Disable RemoteExecutor test

https://github.com/dotnet/arcade/issues/8483 Yeah this package is not well maintained and clearly we can't rely on it.

* Fix immutable list serialization
2024-01-12 22:59:52 +01:00
metalgearsloth
dfb5369664 Version: 204.1.0 2024-01-12 12:57:42 +11:00
Pieter-Jan Briers
2462c906b3 Remove Herobrine
Idk according to the changelog I already did this once but clearly I didn't so ???
2024-01-11 02:01:23 +01:00
metalgearsloth
8cbc05840f Remove some unnecessary GetEntityQuery<T> (#4823) 2024-01-10 18:31:32 +01:00
Pieter-Jan Briers
510846321d BUI Improvements / fixes (#4826)
* Extension helper function for registering BUI events filtered to UI key.

Usages of events like BoundUIClosedEvent frequently did not check the UI key or do it improperly. This has, and will continue to cause, bugs.

The new helper (accessible as Subs.BuiEvents() from an entity system) makes it easy to subscribe to BUI events while also filtering to the correct UI key.

Also added missing SubscribeLocalEvent overloads with new handler types to EntitySystem.Subscriptions.

* Sprinkle [ViewVariables] around BUI

* Avoid buggy behavior if a Bound UI is closed inside the `BoundUIOpenedEvent` that's opening it.
2024-01-10 18:30:57 +01:00
metalgearsloth
ac60567583 Version: 204.0.0 2024-01-09 21:52:36 +11:00
metalgearsloth
7592997f4e Add random pick / take methods for sets (#4821)
* Add random pick / take methods for sets

I want it don't at me.

* cleanup (mraow)

* Revert "cleanup (mraow)"

This reverts commit e279957f21.

* flatpak

* notes
2024-01-09 00:37:19 +11:00
Pieter-Jan Briers
c68b3dccb7 Fix server NRE in console command completions.
Happens if you just type "tp:to<space>" into the console.

Toolshed can fail to provide completion results, in which case a null propagates and possibly crashes the server.
2024-01-08 11:18:11 +01:00
Pieter-Jan Briers
da2a2ce4ff Remove "Do not use from content" public members. 2024-01-07 04:43:59 +01:00
Pieter-Jan Briers
538418ea93 Fix exception when VVing non-networked components
The VV code forcibly dirties all components, which trips an assert for non-networked components.
2024-01-07 04:22:38 +01:00
Kara
2bf284bce8 Infer VVAccess.ReadWrite for all datafields (#4442) 2024-01-06 18:27:58 -08:00
Pieter-Jan Briers
b51cb06d53 Changelog for #4818 2024-01-06 19:23:58 +01:00
Kot
bab6c29fbe Add TextEdit.OnTextChanged event (#4818) 2024-01-06 18:59:19 +01:00
Pieter-Jan Briers
9502c86a65 Fix GLFW clipboard sometimes returning null.
Fixes #4817
2024-01-06 18:52:28 +01:00
Pieter-Jan Briers
359811f71e Fix incorrect IPostInjectInit in DebugConsole
IPostInjectInit does not run through IoCManager.InjectDependencies(). This meant the logger was not initialized, and loading failures in the console history would crash the client startup due to an NRE while trying to log a warning.

See https://github.com/space-wizards/space-station-14/issues/23624
2024-01-06 15:47:55 +01:00
DrSmugleaf
9de5840017 Make IEntityManager.EntityNetManager not nullable, make EntityManager abstract (#4445)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-01-07 00:05:02 +11:00
metalgearsloth
45af00096f Version: 203.0.0 2024-01-06 18:43:23 +11:00
Leon Friedrich
2f0283edb7 Freeze event bus dictionaries (#4727)
* Make component factory use frozen collections

* Fix integration tests

* Also freeze _entTraitDict

* A

* I love integration test setup logic

* Re-add public method

* Freeze event bus

* Remove per-component dictionary lookup on EntAddComponent

* release notes + fix test jank

* Fix merge

* fix tests

* Shutdown
2024-01-06 13:50:19 +11:00
Vasilis
d142393221 Clamp volume gain calculations (#4815) 2024-01-06 13:48:04 +11:00
metalgearsloth
ac147dc2a1 Remove physics sleep cancelling (#4813)
* Remove physics sleep cancelling

Was a bad idea, in the future I'm going to add even more asserts around this but for now I can fix content.

* Fix this
2024-01-06 13:38:41 +11:00
Pieter-Jan Briers
16af5cff9c Delete FodyWeavers.xsd
We used to use Fody. We don't anymore. Yeet.
2024-01-05 12:42:25 +01:00
ElectroJr
7725dbff78 Version: 202.1.1 2024-01-04 23:24:07 -05:00
Leon Friedrich
f2f8824678 Fix pvs error (#4812)
* Fix pvs error

* rename variable
2024-01-05 15:17:44 +11:00
Leon Friedrich
f2e9ed0b73 NetSyncEnabled fixes (#4811)
* NetSyncEnabled fixes

* A
2024-01-05 15:11:09 +11:00
Leon Friedrich
9e6d0aa44a Fix state handling bugs (#4810)
* Fix container state bug

* Fix _pendingReapplyNetStates bug
2024-01-05 15:11:00 +11:00
Leon Friedrich
1758fced75 Revert some grid/map related changes (#4809) 2024-01-05 13:48:43 +11:00
metalgearsloth
31e2a3efe4 Version: 202.1.0 2024-01-04 15:18:07 +11:00
metalgearsloth
371db7db2f Add GetLocalEntitiesIntersecting for griduid + tilesize (#4807) 2024-01-04 15:05:18 +11:00
metalgearsloth
bb23bc6acc Version: 202.0.0 2024-01-04 13:44:56 +11:00
metalgearsloth
270326e188 Add IComponentState / struct support (#4610)
Still boxes but hey at least the struct dream is real.
2024-01-04 13:42:47 +11:00
Leon Friedrich
7d27835543 Use frozen collections in component factory and EntityManager._entTraitDict (#4726)
* Make component factory use frozen collections

* Fix integration tests

* Also freeze _entTraitDict

* A

* I love integration test setup logic

* Re-add public method

* changes

* Better notes

* And the new method

* Fix notes
2024-01-04 12:21:24 +11:00
Leon Friedrich
cfb88b2e8e Arch related engine changes (#4806) 2024-01-04 12:18:03 +11:00
ElectroJr
8a83787d58 Version: 201.0.0 2024-01-03 19:16:36 -05:00
deathride58
6dd9f9e0f5 Blindness hotfix (engine side) (#4804) 2024-01-04 11:13:14 +11:00
Leon Friedrich
7f3445b1c6 Try fix pvs null exception (#4805) 2024-01-04 11:07:42 +11:00
metalgearsloth
5c15f26f09 Version: 200.0.0 2024-01-03 17:29:49 +11:00
faint
b8fbe5e465 Compiler error for AutoNetworkedField fields in a component without a AutoGenerateComponentState attribute (#4670)
* yeah

* remove autonetfield

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-01-03 17:24:08 +11:00
metalgearsloth
32049e34f2 Revert "Fix replay int overflow issues." (#4802)
This reverts commit 9877323195.
2024-01-03 16:16:15 +11:00
Leon Friedrich
26b09283f3 Prevent /tpgrid from moving grids to nullspace (#4798)
* Prevent `/tpgrid` from moving grids to nullspace

* Undo breaking change

* a
2024-01-03 16:07:33 +11:00
Leon Friedrich
6c6360e50a Avoid calling DirtyEntity() when repeatedly dirtying the same component. (#4797) 2024-01-03 16:06:10 +11:00
Pieter-Jan Briers
9877323195 Fix replay int overflow issues.
This came up while we were configuring stuff for the 24h round.
2024-01-03 01:18:50 +01:00
Letter N
b6f52f4c27 Implement OSWindowStyles.* on linux (#4790)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-03 01:04:03 +01:00
Leon Friedrich
c1789cbbaf Make MappingDataNode ordered (#4800) 2024-01-03 10:22:44 +11:00
metalgearsloth
f44f5b5a98 Version: 199.0.0 2024-01-02 17:31:55 +11:00
Leon Friedrich
f4faa1ad3d Run grid traversal on entity spawn (#4796)
* Run grid traversal on entity spawn

* Add test

* Fix tests
2024-01-02 17:25:13 +11:00
Leon Friedrich
97d03c6954 VisMask and EntityLifeStage changes (#4794) 2024-01-02 11:43:08 +11:00
Leon Friedrich
8accbc700a PVS fixes & cleanup (#4793)
* PVS fixes & cleanup

* More cleanup

* Move `CacheGlobalOverrides()` onto the main thread
2024-01-01 01:06:47 -08:00
metalgearsloth
9f013534b3 Add Box2i union for Vector2i (#4777) 2024-01-01 00:00:47 -08:00
Leon Friedrich
0b8febf6a6 FlushEntity() and EntityManager event changes (#4791) 2024-01-01 11:04:11 +11:00
Leon Friedrich
188985ecc2 Try prevent some PVS NREs (#4792) 2024-01-01 11:03:50 +11:00
Leon Friedrich
42434d1f49 Process pvs-leave in parallel (#4787)
* Process pvs-leave in parallel

* oops

* Try fix eternal tests
2024-01-01 05:53:24 +11:00
deathride58
87492cb0c3 zLibrary expansion - Grayscale, noise, and circular gradient utilities (#4784) 2023-12-30 18:35:53 -08:00
metalgearsloth
517ae7f1bd Better audio logs for gain (#4789)
* Better audio logs for gain

Need to track down the -10 gain setter.

* Release checks
2023-12-31 13:34:43 +11:00
ElectroJr
73357f022b Version: 198.1.0 2023-12-30 14:32:05 -05:00
Leon Friedrich
012faa0a16 Don't log errors when attempting to delete invalid entities (#4786) 2023-12-31 06:24:33 +11:00
Letter N
7a8abb3db7 debug ui ui update (#4782)
* UI Debugger: Make it look better

* cleanup

- Use GetOrNew()
- Use typeof(Control)
- Don't check string prefixes
2023-12-31 06:24:06 +11:00
Leon Friedrich
d6ce7e950b Add per-session force send pvs override (#4781) 2023-12-31 05:26:56 +11:00
metalgearsloth
e39b249070 Add gaussian extension for System.Random (#4776) 2023-12-31 05:26:39 +11:00
Letter N
5b889936be add styleclass on menubar so it can be styled (#4780)
* add styleclass on this

* public const string and concistent naming
2023-12-31 05:22:40 +11:00
deathride58
712809195d Allows getting the light rendertarget from overlays (#4783) 2023-12-31 05:17:42 +11:00
Leon Friedrich
64b5d6e323 Pvs cleanup & expose methods for benchmarking (#4779)
* Pvs cleanup & expose methods for benchmarking

* Fix tests

* Minimalist PvsData
2023-12-30 13:23:29 +11:00
metalgearsloth
baa607532d Version: 198.0.1 2023-12-29 13:46:29 +11:00
metalgearsloth
ff064dd859 Fix DefaultMagicAczProvider on FULL_RELEASE (#4778) 2023-12-29 13:45:24 +11:00
metalgearsloth
8305bffcac Version: 198.0.0 2023-12-29 11:51:31 +11:00
metalgearsloth
17a8972052 Fix GetEntitiesIntersecting for maps (#4764)
* Fix GetEntitiesIntersecting for maps

Still valid to call it in this way. Also added the collections versions because muh allocs.

* Add local ones too
2023-12-28 16:16:57 -08:00
metalgearsloth
56e03eae3e Add UI click sound support (#4705)
* Add UI click sound support

* Audio option

* Add Cvar

* Note and fix disabled sounds

* Fix double sound

* Comment removal

* ou

---------

Co-authored-by: Kara <lunarautomaton6@gmail.com>
2023-12-28 16:15:42 -08:00
Pieter-Jan Briers
1f948e17c4 Disable automatic UI scaling for non-main windows by default
This causes tons of problems with OpenDream's small popup windows, and even for SS14 it doesn't make much sense.

The behavior can be re-enabled per window by setting WindowRoot.DisableAutoScaling to false.
2023-12-28 03:38:20 +01:00
Pieter-Jan Briers
efaed42b24 DefaultMagicAczProvider refuses to work on FULL_RELEASE.
It's intended for development only so having it run on a FULL_RELEASE is probably a bug.
2023-12-28 02:31:34 +01:00
Pieter-Jan Briers
be2e31ff9d Print additional runtime diagnostics on server startup.
Brought to you by "I went insane debugging something stupid my own fault for 30 minutes"
2023-12-28 02:31:34 +01:00
ElectroJr
a891cacae5 Version: 197.1.0 2023-12-27 20:26:57 -05:00
Leon Friedrich
e9e0117402 Fix pvs bug (#4775)
* Fix pvs bug

* Moar asserts
2023-12-28 12:24:41 +11:00
Pieter-Jan Briers
24114d87e6 Add Full Hybrid ACZ
This is Hybrid ACZ (Content.Client.zip in server package) but with the ability to add extra files on top (OpenDream rsc)

Also added IStatusHost.InvalidateAcz().
2023-12-28 01:37:57 +01:00
Leon Friedrich
7cd78f3f4e Fix sprite animation bug (#4774)
* Fix sprite animation bug

* CL
2023-12-28 11:13:25 +11:00
ElectroJr
ca64aae7f0 Version: 197.0.0 2023-12-27 17:29:01 -05:00
ElectroJr
be0fb4250c Replace scale with override 2023-12-27 17:22:07 -05:00
Leon Friedrich
eba58cb893 Remove RobustTree & PVSCollection (#4759)
* Cut down RobustTree

* Better LoD

* Add PvsPriority flag

* Undo cvar name change

* reorganize grafana metrics

* Fix tests

* Fix replays

* Don't try process empty chunks

* Fix move benchmark

* Fix benchmark

* Remove obsolete audio methods

* Moar benchmarks

* Rename EntityData

* A

* B
2023-12-28 09:10:13 +11:00
ElectroJr
ced6d5a8b0 Version: 196.0.0 2023-12-27 16:58:33 -05:00
metalgearsloth
495671576e Add cycling for auto-animated states (#4763)
* Add cycling for auto-animated states

* Non-nullable and AdvanceFrameAnimation() tweak
2023-12-28 08:51:00 +11:00
Leon Friedrich
0611674915 Fix toolshed commands not working on reconnect (#4766)
* Fix toolshed commands not working on reconnect

* fix tests?

* space

* I love #if block errors
2023-12-28 08:48:33 +11:00
Leon Friedrich
e6ab61fe42 Prevent recursion in GridTraversalSystem (#4773) 2023-12-28 08:47:09 +11:00
Leon Friedrich
5232e61d38 Add multi-component Dirty() methods (#4770) 2023-12-28 08:20:06 +11:00
Leon Friedrich
a3e90aa04e Add NetEntity support to SpriteView (#4771) 2023-12-28 08:19:42 +11:00
Leon Friedrich
7a719d9f61 Stop toolshed from generating completions for non-toolshed commands (#4767)
* Stop generating toolshed completions for non-toolshed commands

* Better readability????

* Fix missing completions
2023-12-28 08:19:25 +11:00
Pieter-Jan Briers
61385b7c21 Fix WebView processes hanging around on unclean shutdown.
The processes now voluntarily exit themselves if the parent process exits.
2023-12-27 15:33:31 +01:00
Leon Friedrich
2503f9c4e0 Reduce Loc.GetString allocs (#4757)
* Reduce LocalizationManager allocs

* Make _fallbackCultures non-nullable

* Reduce allocs even more
2023-12-27 07:44:18 +11:00
ElectroJr
eb092e90ef Version: 195.0.1 2023-12-25 23:03:00 -05:00
Leon Friedrich
eb556c8728 Fix prototype manager enumeration exception (#4769) 2023-12-26 15:01:00 +11:00
ike709
2e398ded08 Fix audiostreams double-setting their source (#4768)
* Fix audiostreams double-setting their source

* loaded bool

* ok

---------

Co-authored-by: ike709 <ike709@github.com>
2023-12-26 14:51:27 +11:00
metalgearsloth
75cf15caa2 Fix placementmgr rotation (#4765)
If the entity is a self-deleting one this fixes it getting placed and immediately throwing.
2023-12-26 14:25:33 +11:00
ike709
8d7d6a26ba Fix playing audio streams (#4761)
* Fix playing audio streams

* mostly address review

---------

Co-authored-by: ike709 <ike709@github.com>
2023-12-25 18:36:24 -08:00
Pieter-Jan Briers
d593ffbb47 The small innocent change that broke the packaging workflow and surely there's nothing else going to break from this. 2023-12-25 18:18:53 +01:00
Pieter-Jan Briers
15b377dbd6 Version: 195.0.0 2023-12-25 18:00:13 +01:00
Leon Friedrich
d212479689 Don't defer adding joints (#4762) 2023-12-25 17:41:04 +11:00
Pieter-Jan Briers
2d3379d7f4 Bring CEF up to snuff (#4760) 2023-12-25 00:28:51 +01:00
Leon Friedrich
7b171b2212 Fix some physics bugs (#4746)
* Fix SetAwake()

* Remove bad debug assert

* EntityQuery & misc optimizations

* Remove bad(?) code, and add new test

* I love engine tests

* Add debug assert
2023-12-24 17:28:43 +11:00
metalgearsloth
7e8a5e199f Don't defer grid traversals (#4750)
* Don't defer grid traversals

Transform is in a slightly better state so we can remove this hacky thing.

It got to the point it was doing a bunch of unnecessary checks (no need to check container if you're just checking map / grid parent assuming no map / grid has containers).

The main concern now is: Does not deferring cause any issues, which it doesn't seem to (previously this mostly happened with transform state handling). On server-side the main worry is broadphasesystem seeing as this is being run manually to check for entities entering grids.

* Comment cleanup

* Obsolete MoveEvent .FromStateHandling

* Faster map position calculation

* Check Xform.GridUid instead of checking for `MapGridComponent`

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-12-24 16:33:00 +11:00
faint
ae74a5d7d4 PrototypeAttribute name parameter is optional now (#4672)
* PrototypeAttribute name parameter is optional now

* (^///^)

* suggestion

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>

* add anther attribute ctor

* [Virtual] -> sealed
2023-12-23 17:36:44 +11:00
Leon Friedrich
9446ab76f9 Make integration tests fail when logging console errors (#4747)
* Make tests fail when logging console errors

* Add log convenience property
2023-12-23 13:53:23 +11:00
Kara
bb5cb10d57 Generic variants of AssertEqual methods (#4756)
* Generic variants of `AssertEqual` methods

* Remove invalid AssertionMethod
2023-12-23 13:52:08 +11:00
Pieter-Jan Briers
d6ccaee6d4 Version: 194.1.0 2023-12-22 22:25:26 +01:00
metalgearsloth
ce8982e371 Make audiostream methods public (#4752) 2023-12-22 21:14:08 +01:00
metalgearsloth
371cd7ddf1 Reduce robust mapped string allocs (#4754) 2023-12-22 17:08:07 +01:00
metalgearsloth
6573531d5d GetEntitiesInRange flag + TryDelta (#4749)
Needed for tesla too lazy to split
2023-12-22 14:17:46 +11:00
Leon Friedrich
b1e1f27aa3 Add stack trace to audio errors (#4748) 2023-12-22 13:23:28 +11:00
Jordan Dominion
80ce583454 Minor note about TGS using the watchdog API (#4742)
* Minor note about TGS using the watchdog API

* Move comment to new line

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>

---------

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
2023-12-22 09:20:37 +11:00
ElectroJr
2b4a428f9f Version: 194.0.2 2023-12-20 01:57:01 -05:00
Leon Friedrich
41ee330828 Add more PVS null checks (#4743) 2023-12-20 17:54:24 +11:00
ElectroJr
f82452c855 Version: 194.0.1 2023-12-19 19:33:05 -05:00
Leon Friedrich
785e2f84f6 Turn EntityData struct into a class (#4729)
* Turn EntityData struct into a class

* Stop overbudget dictionary removals

* Merge additional logs

* More logging
2023-12-20 11:32:14 +11:00
Leon Friedrich
2bdc1d77ca Add grid deletion/reparenting logs (#4741) 2023-12-20 11:24:40 +11:00
metalgearsloth
51205beb56 Fix audio occlusion (#4738)
Saw this a week ago but forgot to PR.
2023-12-20 11:17:30 +11:00
metalgearsloth
ef325b4780 Fix SetPositionInParent throwing for end position (#4740)
If a child is at the end spot this will ensure it doesn't throw.
2023-12-20 11:15:20 +11:00
ElectroJr
ede337a869 Version: 194.0.0 2023-12-18 20:29:41 -05:00
Kara
d416344aef MoveEvent broadcast -> C# event (#4732)
* MoveEvent broadcast -> C# event

* Broadcast MoveEvent -> C# event

* oops

* forgob

* release notes

* Update description of moveevent
2023-12-19 12:26:13 +11:00
Kara
fb98eb1a0c TreeRecursiveMoveEvent -> C# event (#4733)
* TreeRecursiveMoveEvent -> C# event

* entity query

* Make query protected
2023-12-19 12:19:19 +11:00
Leon Friedrich
ed9a0b4812 Fix freezing while loading ogg files (#4731)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2023-12-19 00:45:21 +01:00
ElectroJr
e7c6151310 Version: 193.2.0 2023-12-17 20:39:52 -05:00
Leon Friedrich
005c2e784a Add more PVS logs (#4730) 2023-12-18 12:36:48 +11:00
ElectroJr
1009dd3ea0 Version: 193.1.1 2023-12-16 23:29:50 -05:00
ElectroJr
87b82160b0 Fix #if FULL_RELEASE error 2023-12-16 23:28:58 -05:00
ElectroJr
fc318c9ecd Version: 193.1.0 2023-12-16 22:56:16 -05:00
Leon Friedrich
ac957ca7fc Try fix PVS bugs (#4728)
* Try fix PVS bugs

* change order
2023-12-17 14:55:46 +11:00
Leon Friedrich
1bdd82b0bf Raise ECS event on prototype-reload (#4724)
* Raise system event on prototype-reload

* Oops
2023-12-17 08:30:53 +11:00
Leon Friedrich
0150c5e6ff Add IPrototypeManager.GetInstances() (#4723) 2023-12-17 08:29:31 +11:00
Pieter-Jan Briers
b2cc90d00f Convert static readonly dictionaries to FrozenDictionary (#4722)
* Convert static readonly dictionaries to FrozenDictionary

* Also `IReadOnlyDictionary`
2023-12-17 04:47:01 +11:00
Leon Friedrich
1f8b89e92f Add frozen collections to sandbox and add yaml type serializers (#4721) 2023-12-16 18:36:58 +01:00
ElectroJr
33d394295e Version: 193.0.0 2023-12-16 11:42:37 -05:00
Leon Friedrich
4934a9c5a5 Combine PVS dictionaries (#4693)
* Try combine dictionaries

* Remove last-ack

* Remove lastSent

* Use structs

* Don't clear previous-sent on deletion

* Pass data into ProcessEntry

* Store visibility data in EntityData

* Turn ToSend into hashset

* Add EntityStringRepresentation constructors

* Add MetaDataComponent to EntityData

* Remove metadata cache

* Create GetStates partial class

* add ToSend partial class

* fix debug asserts

* Cleanup

* Rename partial class

* Try to optimize `ProcessLeavePvs`

* Add ProcessLeavePvsBenchmark

* Turn HashSet into List

* Collect expensive asserts into one block

* Revert "Add ProcessLeavePvsBenchmark"

This reverts commit 93d984e929.

* remove comment
2023-12-17 03:37:48 +11:00
Leon Friedrich
91ebc3eb02 Use FrozenDictionary for prototype instances (#4719)
* Use FrozenDictionary for prototype instances

* Add assert

* Change log to verbose

* RStopwatch.StartNew

* Rename TryGetKindInstances
2023-12-17 03:22:18 +11:00
Leon Friedrich
bc84590a33 Make ChildEnumerator out value non-nullable (#4707)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-17 03:10:20 +11:00
Kelrak
e3944dc6fb Should resolve some error spam in non EFX systems (#4704)
* Should resolve some error spam in non EFX systems

* Exit early if not supported
2023-12-16 15:57:59 +11:00
Leon Friedrich
6a77f4c27b Use frozen dictionaries for prototype kinds (#4718)
* Use frozen dictionaries for prototype kinds

* More logs

* AAAA

* Change log to verbose

* RStopwatch.StartNew()
2023-12-16 15:51:55 +11:00
Leon Friedrich
6246ae412e Make RobustMappedStringSerializer use frozen dictionaries (#4717)
* Make RobustMappedStringSerializer use frozen dictionaries

* Update RobustMappedStringSerializer.MappedStringDict.cs

* Change log to verbose

* RStopwatch.StartNew()
2023-12-16 15:48:57 +11:00
ElectroJr
ac86accc20 Version: 192.0.0 2023-12-15 23:43:53 -05:00
Leon Friedrich
19ff7f25ca Slightly speed up IDependencyCollection (#4716) 2023-12-16 00:23:12 +01:00
Pieter-Jan Briers
3f83733a03 Decode ogg files as shorts instead of floats (#4715) 2023-12-15 20:48:07 +01:00
Leon Friedrich
438fed2f0e Make TryGetEntity protected (#4713)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2023-12-15 19:40:43 +01:00
Pieter-Jan Briers
816a535a92 Fix obsoletion warnings for BinaryFormatter stuff
This became an outright warning in .NET 8. Into the trash, never used it.
2023-12-15 19:37:13 +01:00
Pieter-Jan Briers
01df42aa8f Fix ICommonSession.ConnectedClient obsoletion warnings 2023-12-15 19:34:30 +01:00
Pieter-Jan Briers
a200d73ef9 Use FrozenDictionary in DependencyCollection.
Did not test the perf of this at all but it's the perfect use case so...
2023-12-15 19:28:56 +01:00
metalgearsloth
8d30735ffb Run ProcessQueuedAcks asynchronously (#4696) 2023-12-15 19:10:09 +01:00
Pieter-Jan Briers
2686150f9d Version: 191.0.1 2023-12-15 19:08:35 +01:00
Pieter-Jan Briers
d720e9393b Fix .NET 8 sandbox.
Oops.
2023-12-15 19:08:06 +01:00
Pieter-Jan Briers
f5b1c26bec Update Lidgren to fix nullable compilation issue 2023-12-15 18:28:10 +01:00
Pieter-Jan Briers
3204002c72 Version: 191.0.0 2023-12-15 18:09:15 +01:00
Pieter-Jan Briers
4c79d0c6d0 .NET 8 (#4712) 2023-12-15 18:07:41 +01:00
Pieter-Jan Briers
e0d38fb8bd Fix TryGetResource silently eating exceptions
why god
2023-12-15 16:13:17 +01:00
Leon Friedrich
250f6ca7db Obsolete TransformComponent.ChildEntities (#4706)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-14 17:50:29 +11:00
KP
773365c185 Add a workaround for MacOS' bad CoreAudio OpenAL gain behaviour (#4710)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-14 17:41:52 +11:00
metalgearsloth
9a1e6af586 Remove DeferMoveEvent (#4709) 2023-12-14 17:20:02 +11:00
Pieter-Jan Briers
dc96318379 Fix Lidgren compile on GitHub hopefully 2023-12-13 19:08:08 +01:00
Pieter-Jan Briers
31a3f145de Add [ViewVariables] to BaseButton.EnableAllKeybinds
Makes it show up in the devwindow UI inspector.
2023-12-13 00:28:33 +01:00
Pieter-Jan Briers
331e1fcc81 Update Lidgren to v0.2.7
This adds NRT annotations. Fix compatibility with those.
2023-12-13 00:06:00 +01:00
Leon Friedrich
dc7a51e582 Fix showrays networking (#4701) 2023-12-12 20:17:36 +11:00
metalgearsloth
bfe8e687da Fix audio params variation (#4699) 2023-12-12 20:16:43 +11:00
metalgearsloth
04b6d60d76 Fix TileEdgeOverlay drawing over grids (#4698) 2023-12-12 20:13:20 +11:00
Leon Friedrich
5bc5bfd58a Add more implicit Entity<T> casts (#4552) 2023-12-12 13:27:31 +11:00
TemporalOroboros
56899b4e64 Some ContainerManagerComponent ECSing (#4622) 2023-12-12 13:24:18 +11:00
DrSmugleaf
a23915e0dd Make MapLoaderSystem.GetSaveData public (#4702) 2023-12-12 13:09:39 +11:00
metalgearsloth
726d91c5e8 Version: 190.1.1 2023-12-11 21:20:58 +11:00
metalgearsloth
d8a8783680 Revert "Avoid recontruction broadphase job every tick (#4653)" (#4695) 2023-12-11 20:11:57 +11:00
metalgearsloth
8839dd9a3b Version: 190.1.0 2023-12-11 19:56:10 +11:00
metalgearsloth
0296d9635c Fix some grid setting asserts (#4580) 2023-12-11 19:49:55 +11:00
metalgearsloth
f24d9751d4 Add GetRotation for matrices and better precision (#4691) 2023-12-11 18:40:57 +11:00
wixoa
58ac82ae55 Add OnGrabbed and OnReleased events to the Slider control (#4694) 2023-12-11 18:39:09 +11:00
metalgearsloth
b9130bf236 Version: 190.0.0 2023-12-10 21:33:56 +11:00
metalgearsloth
a2cd33afe5 Add better OpenAL logging (#4687) 2023-12-10 21:25:43 +11:00
metalgearsloth
1772651049 Fix color_picker shader compilation (#4690) 2023-12-10 21:21:55 +11:00
Leon Friedrich
826fa4d131 Ensure shaders parameters are updated when swapping instances (#4689) 2023-12-10 20:36:23 +11:00
Leon Friedrich
0b712ae86c Add colour gradients to sliders (#4688) 2023-12-10 20:35:43 +11:00
Łukasz Mędrek
22528fc484 Fix colour space translation feedback loop(#3839) (#4681) 2023-12-10 14:10:43 +11:00
Leon Friedrich
20a411e6ce Remove non-networked comps from NetComponents dictionary (#4686) 2023-12-10 13:56:34 +11:00
Leon Friedrich
6f9ed8a242 Stop terminating entities from being prematurely detached to nullspace (#4675) 2023-12-10 13:18:42 +11:00
Łukasz Mędrek
790f4c1309 Fix HSV and HSL producing black color on hue 360 (#4682) 2023-12-10 13:14:15 +11:00
metalgearsloth
0b62cb6445 Change netsyncenabled to an assert (#4604) 2023-12-10 13:03:25 +11:00
metalgearsloth
9d0f4d8a08 Update release notes (#4684) 2023-12-10 12:49:47 +11:00
metalgearsloth
5069b0ccf9 Version: 189.0.0 2023-12-10 12:44:00 +11:00
chromiumboy
12cfdb2175 Tweaked how ExpandPvsEvent is raised (#4665) 2023-12-10 12:35:00 +11:00
metalgearsloth
b5e079815d Add MIDI semaphore (#4643) 2023-12-10 12:34:24 +11:00
metalgearsloth
ca3a3279c5 Set priorGain to 0 if gain is 0 (#4680) 2023-12-10 12:31:06 +11:00
Leon Friedrich
003752a161 Improve yaml linter error messages (#4683) 2023-12-10 12:18:52 +11:00
Leon Friedrich
55e51cba9c Fix client-side entity error spam (#4673) 2023-12-10 12:17:46 +11:00
Leon Friedrich
2cd829f4f6 SpriteView Modulation (#4584) 2023-12-10 12:02:21 +11:00
metalgearsloth
43138669ec Network base not adjusted audio params (#4679) 2023-12-09 17:12:09 +11:00
metalgearsloth
3fe30bc00f Version: 188.0.0 2023-12-09 15:17:37 +11:00
metalgearsloth
3ccbdeac6a Fix GetDimensions for screenhandle (#4677) 2023-12-09 15:04:45 +11:00
metalgearsloth
9eb9c91da6 Fix predicted audio not using adjust params (#4676) 2023-12-09 14:59:22 +11:00
metalgearsloth
28d2b47a2c Log errors on spawning audio to deleting ents (#4629) 2023-12-09 14:30:18 +11:00
metalgearsloth
049ffa05e4 Remove EntityQuery<T> from MapVelocity API (#4648) 2023-12-09 14:12:04 +11:00
metalgearsloth
2f36a0a5fc Change midi volume to gain (#4639) 2023-12-09 14:03:01 +11:00
Kelrak
41d03db59d Possible fix to some audio issues (#4640) 2023-12-09 13:39:09 +11:00
Łukasz Mędrek
68df887a65 Fix sorting order in entity spawn panel (#3767) (#4671) 2023-12-09 12:46:57 +11:00
metalgearsloth
e0bbcd7b08 Return null buffered audio on exception (#4624) 2023-12-09 12:44:16 +11:00
metalgearsloth
525815427e Version: 187.2.0 2023-12-06 20:07:30 +11:00
Tom Richardson
70224ac100 Update map physics after events (#4660)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-06 20:02:10 +11:00
metalgearsloth
dabb090dc2 Version: 187.1.2 2023-12-06 14:00:54 +11:00
metalgearsloth
ace8334a3e Bandaid physics contacts getting modified during collision (#4666) 2023-12-06 13:36:33 +11:00
metalgearsloth
d8e70b4d52 Version: 187.1.1 2023-12-05 00:44:56 +11:00
metalgearsloth
2fca0e03ee Don't RegenerateContacts for disabled bodies (#4658) 2023-12-05 00:42:09 +11:00
metalgearsloth
b6980964b6 Revert physics jobs (#4663) 2023-12-05 00:30:02 +11:00
metalgearsloth
34d02256fd Version: 187.1.0 2023-12-03 00:59:49 +11:00
metalgearsloth
34637fb430 Avoid recontruction broadphase job every tick (#4653) 2023-12-03 00:49:05 +11:00
metalgearsloth
d905ef2a50 Apply default audio to MIDIs (#4626) 2023-12-03 00:34:28 +11:00
metalgearsloth
c3f7ef1b5c Version: 187.0.0 2023-12-02 19:38:56 +11:00
metalgearsloth
ced2a5c6cd Make PVS overrides less bad and fix audio (#4656) 2023-12-02 19:36:55 +11:00
metalgearsloth
9ec927543f Remove audio logging (#4654) 2023-12-02 10:43:06 +11:00
Pieter-Jan Briers
215fc8c229 Begone, toolshed logs
They're verbose now
2023-12-01 22:30:44 +01:00
Pieter-Jan Briers
f82ff9e581 Improve error message for network failing to initialize.
The "make sure port XXXX is open" thing now only appears if the error was an actual "address in use" error.
2023-12-01 22:18:38 +01:00
metalgearsloth
906f4598a2 Version: 186.1.0 2023-12-01 20:37:02 +11:00
metalgearsloth
0eb3c37bd8 Add detailed audio logging (#4652) 2023-12-01 20:33:29 +11:00
metalgearsloth
9cc7cf80ba Version: 186.0.0 2023-12-01 19:14:21 +11:00
metalgearsloth
7ba02b5ca6 Store audio on its own map (#4651) 2023-12-01 18:48:33 +11:00
metalgearsloth
3ca7121f5b Don't stop playing audio on game disposal (#4649) 2023-12-01 18:45:37 +11:00
metalgearsloth
d3b31c1d58 Fix out of range MIDI (#4650) 2023-12-01 17:36:58 +11:00
metalgearsloth
92b5bb4660 Version: 185.2.0 2023-12-01 00:18:51 +11:00
metalgearsloth
33caf9c1ba Cap MIDI update rate (#4644) 2023-11-30 21:56:56 +11:00
metalgearsloth
58da8a6001 Fix deleted entity spam for midis (#4647) 2023-11-30 21:56:11 +11:00
metalgearsloth
962f5dc650 Version: 185.1.1 2023-11-30 11:24:34 +11:00
metalgearsloth
c324562513 Fix audio z-offset not applying correctly (#4641) 2023-11-30 10:10:33 +11:00
metalgearsloth
f5a2a710f0 Nuke some non-approx grid queries (#4637) 2023-11-29 22:55:33 +11:00
metalgearsloth
357283e2bc Version: 185.1.0 2023-11-29 16:40:44 +11:00
metalgearsloth
5991bfa106 Fix audio position floating point imprecision (#4634) 2023-11-29 16:39:23 +11:00
metalgearsloth
4160b120e0 Set listener velocity for audio (#4635) 2023-11-29 16:31:22 +11:00
metalgearsloth
98a1fa1fba Version: 185.0.0 2023-11-29 11:02:53 +11:00
metalgearsloth
fb08451849 Replace Parallel.For with ParallelManager (#4588) 2023-11-29 10:57:52 +11:00
metalgearsloth
ebea0d7572 Add grid audio flag (#4632) 2023-11-29 10:19:17 +11:00
metalgearsloth
eb6f28cce0 Version: 184.1.0 2023-11-28 23:55:55 +11:00
metalgearsloth
a1d02d7c55 Add gain setter for audio params + API cleanup (#4627) 2023-11-28 22:54:19 +11:00
metalgearsloth
777ab85cff Version: 184.0.1 2023-11-28 20:46:53 +11:00
metalgearsloth
d33a8465b0 Fix global audio (#4625) 2023-11-28 20:44:40 +11:00
metalgearsloth
6572fdb404 Midi tweaks (#4618) 2023-11-28 20:39:59 +11:00
Uriende
6273b1b80d Only change the offset if has already started (#4619) 2023-11-28 20:29:19 +11:00
Nemanja
a09a60efe9 Adjust how KeyBindUp retreives the focused control (#4620) 2023-11-28 19:22:05 +11:00
metalgearsloth
d3339964ee Version: 184.0.0 2023-11-28 19:13:18 +11:00
metalgearsloth
3ffef625ec Pool MsgState streams (#4582) 2023-11-28 19:10:30 +11:00
metalgearsloth
4fd9b2bc3b Add another GetEntitiesInRange overload (#4587) 2023-11-28 14:18:40 +11:00
metalgearsloth
adc5051841 Version: 183.0.0 2023-11-27 22:14:46 +11:00
metalgearsloth
2733435218 Audio rework unrevert + audio packaging (#4555)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2023-11-27 22:12:26 +11:00
metalgearsloth
24b0165ec9 Revert Arch (#4613) 2023-11-27 21:41:01 +11:00
metalgearsloth
7b9aa09b18 Version: 182.1.1 2023-11-26 13:36:08 +11:00
metalgearsloth
7bee6f6fc1 Update Arch (#4609) 2023-11-26 13:35:26 +11:00
metalgearsloth
ff75495894 Version: 182.1.0 2023-11-26 12:45:21 +11:00
metalgearsloth
4cb51af733 Add arch trimming back (#4608) 2023-11-26 12:35:13 +11:00
Leon Friedrich
89c1e90646 Add IRobustRandom.SetSeed() (#4606) 2023-11-25 15:32:16 -08:00
metalgearsloth
b6cadfedd5 Update arch (#4605) 2023-11-25 14:56:56 +11:00
metalgearsloth
9f57b705d7 Version: 182.0.0 2023-11-24 00:21:00 +11:00
metalgearsloth
68be9712ad Add entity gen to hashcode (#4601) 2023-11-24 00:19:58 +11:00
metalgearsloth
aaa446254c Version: 181.0.2 2023-11-23 23:43:47 +11:00
metalgearsloth
5e2d2ab317 Fix too many pointlights causing blackscreen (#4599) 2023-11-23 23:39:51 +11:00
metalgearsloth
20ae63fbbd Replace tile intersecting with enumerator (#4595) 2023-11-23 22:36:40 +11:00
metalgearsloth
a92c0cbef4 Fix nullable comps being raised for client gamestates (#4596) 2023-11-23 22:07:27 +11:00
metalgearsloth
95649a2dd0 Version: 181.0.1 2023-11-23 16:53:03 +11:00
metalgearsloth
861807f8b4 Fix HasComp(uid, Type) (#4594) 2023-11-23 16:52:31 +11:00
metalgearsloth
bd73f1c05a Version: 181.0.0 2023-11-23 15:28:35 +11:00
metalgearsloth
7dce51e2cf Arch PR two electric boogaloo (#4388)
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-23 14:29:37 +11:00
DrSmugleaf
d9b0f3a227 Version: 180.2.1 2023-11-22 17:02:13 -08:00
Vasilis
05766a2eaa Fix not using dotnet 7 for actions in engine (#4591) 2023-11-23 00:43:15 +01:00
metalgearsloth
a761fbc09e Version: 180.2.0 2023-11-22 22:00:05 +11:00
metalgearsloth
f69440b3f2 Minor PVS stuff (#4573)
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-22 21:54:07 +11:00
Leon Friedrich
b459d2ce21 Add more map system helper methods. (#4589) 2023-11-22 21:51:46 +11:00
Leon Friedrich
202182e3d4 Add new EnsureEntity variants (#4586) 2023-11-20 17:44:36 +11:00
metalgearsloth
96cb52e5d2 Version: 180.1.0 2023-11-20 16:31:41 +11:00
metalgearsloth
82e0c0baeb Add cvar for lidgren pool size (#4585) 2023-11-20 16:28:13 +11:00
Leon Friedrich
54d6552164 Fix shape lookups for non-hard fixtures (#4583) 2023-11-20 16:15:27 +11:00
Jordan Dominion
c21b6c993c Fix potential error when writing runtime log (#4575) 2023-11-19 15:47:53 +01:00
metalgearsloth
2fe4a8b859 Add map name to lsmap (#4576) 2023-11-19 15:47:19 +01:00
metalgearsloth
8325966dbb Fix contact constraints allocs (#4581) 2023-11-19 15:47:07 +01:00
metalgearsloth
2459a9d688 Version: 180.0.0 2023-11-19 15:09:59 +11:00
Leon Friedrich
2cd2d1edd6 Add misc helpful methods (#4577) 2023-11-19 15:05:26 +11:00
metalgearsloth
b982350851 Use NetEntities for F3 panel (#4571) 2023-11-16 20:53:23 +11:00
DrSmugleaf
4a50bc2154 Add AddEntitiesIntersecting for phys shapes, change float range overload to use circles, remove obsolete methods (#4572) 2023-11-16 20:44:21 +11:00
metalgearsloth
4c85e205b9 Add chain support to TryGetNearest (#4567) 2023-11-16 20:40:23 +11:00
ElectroJr
0b447d9d82 Version: 179.0.0 2023-11-12 13:35:17 -05:00
Leon Friedrich
ceb205ad52 Allow per-eye lighting toggling. (#4569) 2023-11-13 05:30:00 +11:00
Leon Friedrich
a48ff3dbf1 Fix PlacementManager bug (#4568) 2023-11-13 05:29:18 +11:00
DrSmugleaf
2b85fa88c1 Print stack trace when adding a component while iterating net comps in ResetPredictedEntities (#4541) 2023-11-13 05:26:13 +11:00
Leon Friedrich
19564a421b Fix deserialization of empty grid chunks (#4565) 2023-11-13 05:22:20 +11:00
Leon Friedrich
b3f0e467ee Improve UnknownPrototypeException error message (#4566) 2023-11-13 05:22:05 +11:00
Leon Friedrich
216292c849 Make EyeComponent.Eye not nullable (#4564) 2023-11-13 04:20:09 +11:00
Jerry
68753d15e0 Fix stack overflow error on planet station (#4563) 2023-11-12 13:28:35 +11:00
ElectroJr
2a357051ae Version: 178.0.0 2023-11-10 20:58:35 -05:00
Leon Friedrich
58e0b62145 Merge ActorSystem and IPlayerManager (#4530) 2023-11-11 12:50:21 +11:00
Leon Friedrich
14cc273997 Add NetListAsArray<T>.Value to sandbox whitelist (#4537) 2023-11-11 11:57:58 +11:00
DrSmugleaf
93f4428635 Version: 177.0.0 2023-11-08 00:21:12 -08:00
DrSmugleaf
164bf68aca Move TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi to SharedUserInterfaceSystem (#4562) 2023-11-08 16:52:38 +11:00
Leon Friedrich
773b87672b Fix terminating entity reparenting bug (#4549) 2023-11-08 15:39:08 +11:00
metalgearsloth
eecf834039 Fix PlacementManager warnings (#4557) 2023-11-08 15:34:54 +11:00
Leon Friedrich
325fe46aa3 Add More Entity<T> query methods (#4550) 2023-11-07 20:24:42 -08:00
metalgearsloth
2f6c29ab43 Add GetMapCoordinates to TransformSystem (#4556) 2023-11-07 20:23:05 -08:00
metalgearsloth
aab1a2dba9 Fix transform test warnings (#4558) 2023-11-07 20:22:07 -08:00
DrSmugleaf
f36fbd9c83 Fix inverted GetAllMapGrids mapid check (#4561) 2023-11-07 14:26:01 -08:00
metalgearsloth
126c863f45 Hotfix containersystem.remove (#4560) 2023-11-07 16:18:08 +11:00
Leon Friedrich
618a8491bf Add BeforeApplyState event to replay playback (#4536) 2023-11-07 15:07:26 +11:00
Leon Friedrich
2743b64a2b Mark container methods as obsolete (#4551) 2023-11-07 15:05:32 +11:00
Leon Friedrich
28cc91934c Change PVS error log into warning (#4548) 2023-11-07 15:02:13 +11:00
metalgearsloth
eadfcd4c09 Specify RichTextLabel VAlignment as Center (#4520) 2023-11-07 10:27:49 +11:00
metalgearsloth
7871b0010e Version: 176.0.0 2023-11-07 09:51:32 +11:00
metalgearsloth
3da04ed17e Robust.Packaging updates (#4547) 2023-11-07 09:36:33 +11:00
metalgearsloth
170d192791 Revert audio rework (#4554) 2023-11-07 09:34:09 +11:00
Leon Friedrich
dcd9939554 Fix PVS initial list capacity bug (#4546) 2023-11-06 04:41:56 +11:00
Leon Friedrich
98ef58eca6 Add max game state buffer size cvar (#4543) 2023-11-05 02:58:48 +11:00
metalgearsloth
ab1e99a0df Add GetEntitiesInRange that takes in a set (#4544) 2023-11-04 15:02:20 +11:00
Leon Friedrich
499c236798 Fix replay lerp error spam (#4534) 2023-10-30 04:29:47 +11:00
metalgearsloth
8dc2345ceb Fix audio position on first tick (#4533) 2023-10-29 15:30:59 +11:00
metalgearsloth
9b04270178 Version: 175.0.0 2023-10-29 15:03:09 +11:00
metalgearsloth
d75dbc901f Audio rework (#4421) 2023-10-29 14:58:19 +11:00
Leon Friedrich
19a3e82848 Cache prototype data for IEntityManager.IsDefault() (#4531) 2023-10-29 12:54:52 +11:00
Leon Friedrich
911abf2693 Remove empty planet-map chunks (#4529) 2023-10-29 12:52:03 +11:00
605 changed files with 29680 additions and 12302 deletions

View File

@@ -10,5 +10,8 @@ charset = utf-8
[*.{csproj,xml,yml,dll.config,targets,props}]
indent_size = 2
[nuget.config]
indent_size = 2
[*.gdsl]
indent_style = tab

View File

@@ -14,7 +14,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE

61
Directory.Packages.props Normal file
View File

@@ -0,0 +1,61 @@
<Project>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
<PackageVersion Include="Linguini.Bundle" Version="0.1.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.8.0" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
<PackageVersion Include="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.24059.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
<PackageVersion Include="Microsoft.ILVerification" Version="8.0.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="NUnit" Version="4.0.1" />
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.2.2" />
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
<PackageVersion Include="Robust.Natives.Cef" Version="120.1.9" />
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.7" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<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.3" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using System.Runtime.CompilerServices;
// So I wanted to mess with NetIncomingMessage and NetOutgoingMessage from tests.
// Now.. the instructors are internal...
// Unless...
// I mean we have this project here from the weird way we're compiling Lidgren.
// I could just put this in here... it wouldn't touch the main Lidgren repo at all...
[assembly: InternalsVisibleTo("Robust.UnitTesting")]

View File

@@ -10,9 +10,14 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="CursedHorrorsBeyondOurWildestImagination.cs" />
<Compile Include="Lidgren.Network\Lidgren.Network\**\*.cs">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>

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

@@ -1,8 +1,8 @@
<Project>
<!-- Engine-specific properties. Content should not use this file. -->
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>11</LangVersion>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

View File

@@ -3,7 +3,8 @@
<!-- Import this at the end of any project files in Robust and Content. -->
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<Import Project="Robust.Custom.targets" Condition="Exists('Robust.Custom.targets')"/>
@@ -27,5 +28,5 @@
<Import Project="Robust.Analyzers.targets" Condition="'$(SkipRobustAnalyzer)' != 'true'" />
<!-- serialization generator -->
<Import Project="Robust.Serialization.Generator.targets" />
<Import Project="Robust.Serialization.Generator.targets" Condition="'$(SkipRobustAnalyzer)' != 'true'" />
</Project>

View File

@@ -61,18 +61,5 @@ namespace OpenToolkit.GraphicsLibraryFramework
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GLFWException"/> class with the specified context
/// and the serialization information.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> associated with this exception.</param>
/// <param name="context">
/// A <see cref="StreamingContext"/> that represents the context of this exception.
/// </param>
protected GLFWException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
- type: entity
id: Audio
name: Audio
description: Audio entity used by engine
save: false

File diff suppressed because it is too large Load Diff

View File

@@ -12,3 +12,8 @@
id: bgra
kind: source
path: "/Shaders/Internal/bgra.swsl"
- type: shader
id: ColorPicker
kind: source
path: "/Shaders/color_picker.swsl"

View File

@@ -9,6 +9,7 @@ cmd-parse-failure-float = {$arg} is not a valid float.
cmd-parse-failure-bool = {$arg} is not a valid bool.
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-error-file-not-found = Could not find file: {$file}.
@@ -561,3 +562,7 @@ 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-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-arg-file-name = <file name>

View File

@@ -0,0 +1,8 @@
cmd-merge_grids-desc = Combines 2 grids into 1 grid
cmd-merge_grids-help = merge_grids <gridUid1> <gridUid2> <offsetX> <offsetY> [angle]
cmd-merge_grids-hintA = Grid A
cmd-merge_grids-hintB = Grid B
cmd-merge_grids-xOffset = X offset
cmd-merge_grids-yOffset = Y offset
cmd-merge_grids-angle = [Angle]

View File

@@ -0,0 +1,46 @@
// Simple shader for creating a box with colours varying along the x and y axes.
uniform highp vec2 size;
uniform highp vec2 offset;
uniform highp vec4 xAxis;
uniform highp vec4 yAxis;
uniform highp vec4 baseColor;
uniform bool hsv;
void fragment()
{
// Calculate local uv coordinates.
// I.e., if using this shader to draw a box to the screen, (0,0) is the bottom left of the box.
highp float yCoords = 1.0/SCREEN_PIXEL_SIZE.y - FRAGCOORD.y;
highp vec2 uv = vec2(FRAGCOORD.x - offset.x, yCoords - offset.y);
uv /= size;
uv.y = 1.0 - uv.y;
highp vec4 modulate = baseColor + uv.x * xAxis + uv.y * yAxis;
if (hsv)
{
modulate.xyz = hsv2rgb(modulate.xyz);
}
// The UV used for the texture lookup is the TEXTURE UV coordinate, which is different from the coordinates computed above.
COLOR = zTexture(UV) * modulate;
}
// hsv to RGB conversion taken from www.shadertoy.com/view/MsS3Wc
// The MIT License
// Copyright © 2014 Inigo Quilez
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// https://www.youtube.com/c/InigoQuilez
// https://iquilezles.org
highp vec3 hsv2rgb( in highp vec3 c )
{
highp vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );
return c.z * mix( vec3(1.0), rgb, c.y);
}

View File

@@ -1,23 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<Import Project="..\MSBuild\Robust.Properties.targets"/>
<Import Project="..\MSBuild\Robust.Engine.props"/>
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1"/>
<PackageReference Include="NUnit" Version="3.13.2"/>
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageVersion Update="NUnit" Version="3.14.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
<PackageReference Include="NUnit"/>
<PackageReference Include="NUnit3TestAdapter"/>
<PackageReference Include="NUnit.Analyzers"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Analyzers\Robust.Analyzers.csproj"/>
</ItemGroup>

View File

@@ -3,19 +3,20 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<ItemGroup>
<!-- Needed for NotNullableFlagAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
</ItemGroup>
<ItemGroup>
<!-- Needed for FriendAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" />

View File

@@ -1,15 +1,18 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;
using Robust.Benchmarks.Exporters;
@@ -44,10 +47,16 @@ public sealed class DefaultSQLConfig : IConfig
public IEnumerable<BenchmarkLogicalGroupRule> GetLogicalGroupRules() => DefaultConfig.Instance.GetLogicalGroupRules();
public IEnumerable<EventProcessor> GetEventProcessors() => DefaultConfig.Instance.GetEventProcessors();
public IEnumerable<IColumnHidingRule> GetColumnHidingRules() => DefaultConfig.Instance.GetColumnHidingRules();
public IOrderer Orderer => DefaultConfig.Instance.Orderer!;
public ICategoryDiscoverer? CategoryDiscoverer => DefaultConfig.Instance.CategoryDiscoverer;
public SummaryStyle SummaryStyle => DefaultConfig.Instance.SummaryStyle;
public ConfigUnionRule UnionRule => DefaultConfig.Instance.UnionRule;
public string ArtifactsPath => DefaultConfig.Instance.ArtifactsPath;
public CultureInfo CultureInfo => DefaultConfig.Instance.CultureInfo!;
public ConfigOptions Options => DefaultConfig.Instance.Options;
public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout;
public IReadOnlyList<Conclusion> ConfigAnalysisConclusion => DefaultConfig.Instance.ConfigAnalysisConclusion;
}

View File

@@ -13,12 +13,12 @@
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets" />

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Robust.Server.Containers;
using Robust.Server.GameStates;
@@ -11,7 +12,9 @@ using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.UnitTesting;
namespace Robust.Benchmarks.Transform;
@@ -19,114 +22,177 @@ namespace Robust.Benchmarks.Transform;
/// This benchmark tests various transform/move related functions with an entity that has many children.
/// </summary>
[Virtual, MemoryDiagnoser]
public class RecursiveMoveBenchmark
public class RecursiveMoveBenchmark : RobustIntegrationTest
{
private ISimulation _simulation = default!;
private IEntityManager _entMan = default!;
private SharedTransformSystem _transform = default!;
private ContainerSystem _container = default!;
private PvsSystem _pvs = default!;
private EntityCoordinates _mapCoords;
private EntityCoordinates _gridCoords;
private EntityCoordinates _gridCoords2;
private EntityUid _ent;
private EntityUid _child;
private TransformComponent _childXform = default!;
private EntityQuery<TransformComponent> _query;
private ICommonSession[] _players = default!;
private PvsSession _session = default!;
[GlobalSetup]
public void GlobalSetup()
{
_simulation = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
ProgramShared.PathOffset = "../../../../";
var server = StartServer();
var client = StartClient();
if (!_simulation.Resolve<IConfigurationManager>().GetCVar(CVars.NetPVS))
throw new InvalidOperationException("PVS must be enabled");
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();
var mapMan = server.ResolveDependency<IMapManager>();
_entMan = server.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
_entMan = _simulation.Resolve<IEntityManager>();
_transform = _entMan.System<SharedTransformSystem>();
_container = _entMan.System<ContainerSystem>();
_pvs = _entMan.System<PvsSystem>();
_query = _entMan.GetEntityQuery<TransformComponent>();
// Create map & grid
var mapMan = _simulation.Resolve<IMapManager>();
var mapSys = _entMan.System<SharedMapSystem>();
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_mapCoords = new EntityCoordinates(map, 100, 100);
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
_ent = _entMan.Spawn();
var netMan = client.ResolveDependency<IClientNetManager>();
client.SetConnectTarget(server);
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
// Quick check that SetCoordinates actually changes the parent as expected
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
_transform.SetCoordinates(_ent, _gridCoords);
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
throw new Exception("Grid traversal error.");
_transform.SetCoordinates(_ent, _mapCoords);
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
throw new Exception("Grid traversal error.");
// Add 5 direct children in slots to represent clothing.
for (var i = 0; i < 5; i++)
for (int i = 0; i < 10; i++)
{
var id = $"inventory{i}";
_container.EnsureContainer<ContainerSlot>(_ent, id);
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
throw new Exception($"Failed to setup entity");
server.WaitRunTicks(1).Wait();
client.WaitRunTicks(1).Wait();
}
// body parts
_container.EnsureContainer<Container>(_ent, "body");
for (var i = 0; i < 5; i++)
// Ensure client & server ticks are synced.
// Client runs 1 tick ahead
{
// Simple organ
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
throw new Exception($"Failed to setup entity");
var sTick = (int)server.Timing.CurTick.Value;
var cTick = (int)client.Timing.CurTick.Value;
var delta = cTick - sTick;
// body part that has another body part / limb
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
throw new Exception($"Failed to setup entity");
if (delta > 1)
server.WaitRunTicks(delta - 1).Wait();
else if (delta < 1)
client.WaitRunTicks(1 - delta).Wait();
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
throw new Exception($"Failed to setup entity");
sTick = (int)server.Timing.CurTick.Value;
cTick = (int)client.Timing.CurTick.Value;
delta = cTick - sTick;
if (delta != 1)
throw new Exception("Failed setup");
}
// Backpack
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
throw new Exception($"Failed to setup entity");
// Misc backpack contents.
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
for (var i = 0; i < 10; i++)
// Set up map and spawn player
server.WaitPost(() =>
{
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_gridCoords2 = new EntityCoordinates(grid, .5f, .6f);
_mapCoords = new EntityCoordinates(map, 100, 100);
var playerUid = _entMan.SpawnEntity(null, _mapCoords);
// Attach player.
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, playerUid);
sPlayerMan.JoinGame(session);
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
_ent = _entMan.Spawn();
// Quick check that SetCoordinates actually changes the parent as expected
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
_transform.SetCoordinates(_ent, _gridCoords);
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
throw new Exception("Grid traversal error.");
_transform.SetCoordinates(_ent, _mapCoords);
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
throw new Exception("Grid traversal error.");
// Add 5 direct children in slots to represent clothing.
for (var i = 0; i < 5; i++)
{
var id = $"inventory{i}";
_container.EnsureContainer<ContainerSlot>(_ent, id);
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
throw new Exception($"Failed to setup entity");
}
// body parts
_container.EnsureContainer<Container>(_ent, "body");
for (var i = 0; i < 5; i++)
{
// Simple organ
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
throw new Exception($"Failed to setup entity");
// body part that has another body part / limb
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
throw new Exception($"Failed to setup entity");
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
throw new Exception($"Failed to setup entity");
}
// Backpack
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
throw new Exception($"Failed to setup entity");
// Misc backpack contents.
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
throw new Exception($"Failed to setup entity");
}
// Emergency box inside of the backpack
var box = backpackStorage.ContainedEntities.First();
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
throw new Exception($"Failed to setup entity");
}
// Deepest child.
_child = boxContainer.ContainedEntities.First();
_childXform = _query.GetComponent(_child);
_players = new[] {session};
_session = _pvs.PlayerData[session];
}).Wait();
for (int i = 0; i < 10; i++)
{
server.WaitRunTicks(1).Wait();
client.WaitRunTicks(1).Wait();
}
// Emergency box inside of the backpack
var box = backpackStorage.ContainedEntities.First();
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
for (var i = 0; i < 10; i++)
{
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
throw new Exception($"Failed to setup entity");
}
PvsTick();
PvsTick();
}
// Deepest child.
_child = boxContainer.ContainedEntities.First();
_childXform = _query.GetComponent(_child);
_pvs.ProcessCollections();
private void PvsTick()
{
_session.ClearState();
_pvs.CacheSessionData(_players);
_pvs.GetVisibleChunks();
_pvs.ProcessVisibleChunksSequential();
}
/// <summary>
@@ -140,6 +206,13 @@ public class RecursiveMoveBenchmark
_transform.SetCoordinates(_ent, _mapCoords);
}
[Benchmark]
public void MoveEntityASmidge()
{
_transform.SetCoordinates(_ent, _gridCoords);
_transform.SetCoordinates(_ent, _gridCoords2);
}
/// <summary>
/// Like <see cref="MoveEntity"/>, but also processes queued PVS chunk updates.
/// </summary>
@@ -147,9 +220,18 @@ public class RecursiveMoveBenchmark
public void MoveAndUpdateChunks()
{
_transform.SetCoordinates(_ent, _gridCoords);
_pvs.ProcessCollections();
PvsTick();
_transform.SetCoordinates(_ent, _mapCoords);
_pvs.ProcessCollections();
PvsTick();
}
[Benchmark]
public void MoveASmidgeAndUpdateChunk()
{
_transform.SetCoordinates(_ent, _gridCoords);
PvsTick();
_transform.SetCoordinates(_ent, _gridCoords2);
PvsTick();
}
[Benchmark]

View File

@@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.0.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>

View File

@@ -2,11 +2,12 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,85 @@
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef;
/// <summary>
/// Shared functionality for all <see cref="CefClient"/> implementations.
/// </summary>
/// <remarks>
/// Locks down a bunch of CEF functionality we absolutely do not need content to do right now.
/// </remarks>
internal abstract class BaseRobustCefClient : CefClient
{
private readonly RobustCefPrintHandler _printHandler = new();
private readonly RobustCefPermissionHandler _permissionHandler = new();
private readonly RobustCefDialogHandler _dialogHandler = new();
private readonly RobustCefDragHandler _dragHandler = new();
protected override CefPrintHandler GetPrintHandler() => _printHandler;
protected override CefPermissionHandler GetPermissionHandler() => _permissionHandler;
protected override CefDialogHandler GetDialogHandler() => _dialogHandler;
protected override CefDragHandler GetDragHandler() => _dragHandler;
private sealed class RobustCefPrintHandler : CefPrintHandler
{
protected override void OnPrintSettings(CefBrowser browser, CefPrintSettings settings, bool getDefaults)
{
}
protected override bool OnPrintDialog(CefBrowser browser, bool hasSelection, CefPrintDialogCallback callback)
{
return false;
}
protected override bool OnPrintJob(CefBrowser browser, string documentName, string pdfFilePath, CefPrintJobCallback callback)
{
return false;
}
protected override void OnPrintReset(CefBrowser browser)
{
}
}
private sealed class RobustCefPermissionHandler : CefPermissionHandler
{
protected override bool OnRequestMediaAccessPermission(
CefBrowser browser,
CefFrame frame,
string requestingOrigin,
CefMediaAccessPermissionTypes requestedPermissions,
CefMediaAccessCallback callback)
{
callback.Cancel();
return true;
}
}
private sealed class RobustCefDialogHandler : CefDialogHandler
{
protected override bool OnFileDialog(
CefBrowser browser,
CefFileDialogMode mode,
string title,
string defaultFilePath,
string[] acceptFilters,
CefFileDialogCallback callback)
{
callback.Cancel();
return true;
}
}
private sealed class RobustCefDragHandler : CefDragHandler
{
protected override bool OnDragEnter(CefBrowser browser, CefDragData dragData, CefDragOperationsMask mask)
{
return true;
}
protected override void OnDraggableRegionsChanged(CefBrowser browser, CefFrame frame, CefDraggableRegion[] regions)
{
}
}
}

View File

@@ -1,4 +1,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
@@ -19,6 +22,8 @@ namespace Robust.Client.WebView.Cef
var mainArgs = new CefMainArgs(argv);
StartWatchThread();
// This will block executing until the subprocess is shut down.
var code = CefRuntime.ExecuteProcess(mainArgs, new RobustCefApp(null), IntPtr.Zero);
@@ -29,5 +34,44 @@ namespace Robust.Client.WebView.Cef
return code;
}
private static void StartWatchThread()
{
//
// CEF has this nasty habit of not shutting down all its processes if the parent crashes.
// Great!
//
// We use a separate thread in each CEF child process to watch the main PID.
// If it exits, we kill ourselves after a couple seconds.
//
if (Environment.GetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_ID") is not { } parentIdString)
return;
if (Environment.GetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_MODULE") is not { } parentModuleString)
return;
if (!int.TryParse(parentIdString, CultureInfo.InvariantCulture, out var parentId))
return;
var process = Process.GetProcessById(parentId);
if ((process.MainModule?.FileName ?? "") != parentModuleString)
{
process.Dispose();
return;
}
new Thread(() => WatchThread(process)) { Name = "CEF Watch Thread", IsBackground = true }
.Start();
}
private static void WatchThread(Process p)
{
p.WaitForExit();
Thread.Sleep(3000);
Environment.Exit(1);
}
}
}

View File

@@ -76,12 +76,9 @@ namespace Robust.Client.WebView.Cef
return true;
}
protected override unsafe bool Read(IntPtr dataOut, int bytesToRead, out int bytesRead, CefResourceReadCallback callback)
protected override bool Read(Span<byte> response, out int bytesRead, CefResourceReadCallback callback)
{
var byteSpan = new Span<byte>((void*) dataOut, bytesToRead);
bytesRead = _stream.Read(byteSpan);
bytesRead = _stream.Read(response);
return bytesRead != 0;
}

View File

@@ -31,10 +31,12 @@ namespace Robust.Client.WebView.Cef
// Disable zygote on Linux.
commandLine.AppendSwitch("--no-zygote");
// Work around https://bitbucket.org/chromiumembedded/cef/issues/3213/ozone-egl-initialization-does-not-have
// Work around https://github.com/chromiumembedded/cef/issues/3213
// Desktop GL force makes Chromium not try to load its own ANGLE/Swiftshader so load paths aren't problematic.
if (OperatingSystem.IsLinux())
commandLine.AppendSwitch("--use-gl", "desktop");
// UPDATE: That bug got fixed and now this workaround breaks CEF.
// Keeping all this comment history in case I ever wanan remember what the `--use-gl` flag is.
//if (OperatingSystem.IsLinux())
// commandLine.AppendSwitch("--use-gl", "desktop");
// commandLine.AppendSwitch("--single-process");

View File

@@ -3,7 +3,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
// Simple CEF client.
internal sealed class RobustCefClient : CefClient
internal sealed class RobustCefClient : BaseRobustCefClient
{
private readonly CefRenderHandler _renderHandler;
private readonly CefRequestHandler _requestHandler;

View File

@@ -150,7 +150,7 @@ namespace Robust.Client.WebView.Cef
}
}
private sealed class WindowCefClient : CefClient
private sealed class WindowCefClient : BaseRobustCefClient
{
private readonly CefLifeSpanHandler _lifeSpanHandler;
private readonly CefRequestHandler _requestHandler;

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
@@ -85,6 +86,10 @@ namespace Robust.Client.WebView.Cef
_app = new RobustCefApp(_sawmill);
var process = Process.GetCurrentProcess();
Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_ID", process.Id.ToString());
Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_MODULE", process.MainModule?.FileName ?? "");
// So these arguments look like nonsense, but it turns out CEF is just *like that*.
// The first argument is literally nonsense, but it needs to be there as otherwise the second argument doesn't apply
// The second argument turns off CEF's bullshit error handling, which breaks dotnet's error handling.

View File

@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="Robust.Natives.Cef" Version="102.0.9" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Robust.Natives.Cef" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,28 @@
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Console;
namespace Robust.Client.WebView;
internal sealed class TestBrowseWindow : DefaultWindow
{
protected override Vector2 ContentsMinimumSize => new Vector2(640, 480);
public TestBrowseWindow()
{
var wv = new WebViewControl();
wv.Url = "https://spacestation14.io";
Contents.AddChild(wv);
}
}
internal sealed class TestBrowseWindowCommand : LocalizedCommands
{
public override string Command => "test_browse_window";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
new TestBrowseWindow().Open();
}
}

View File

@@ -5,6 +5,7 @@ using Robust.Client.WebView.Headless;
using Robust.Client.WebViewHook;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
[assembly: WebViewManagerImpl(typeof(WebViewManager))]
@@ -22,6 +23,9 @@ namespace Robust.Client.WebView
var cfg = dependencies.Resolve<IConfigurationManagerInternal>();
cfg.LoadCVarsFromAssembly(typeof(WebViewManager).Assembly);
var refl = dependencies.Resolve<IReflectionManager>();
refl.LoadAssemblies(typeof(WebViewManager).Assembly);
dependencies.RegisterInstance<IWebViewManager>(this);
dependencies.RegisterInstance<IWebViewManagerInternal>(this);

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.Animations
@@ -37,7 +39,12 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
SoundSystem.Play(keyFrame.Resource, Filter.Local(), entity, keyFrame.AudioParamsFunc.Invoke());
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
}
return (keyFrameIndex, playingTime);

View File

@@ -0,0 +1,58 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Audio;
internal partial class AudioManager
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
public void FlushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
internal void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
internal void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
internal void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}

View File

@@ -0,0 +1,374 @@
using System;
using System.IO;
using System.Numerics;
using System.Threading;
using OpenTK.Audio.OpenAL;
using Robust.Client.Audio.Sources;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
internal partial class AudioManager
{
private float _zOffset;
public void SetZOffset(float offset)
{
_zOffset = offset;
}
/// <inheritdoc />
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
{
switch (_attenuation)
{
case Attenuation.LinearDistance:
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
case Attenuation.LinearDistanceClamped:
distance = MathF.Max(referenceDistance, MathF.Min(distance, maxDistance));
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
default:
// TODO: If you see this you can implement
throw new NotImplementedException();
}
}
public void InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
InitializeAudio();
}
public void Shutdown()
{
DisposeAllAudio();
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
/// <inheritdoc/>
public void SetVelocity(Vector2 velocity)
{
AL.Listener(ALListener3f.Velocity, velocity.X, velocity.Y, 0f);
}
/// <inheritdoc/>
public void SetPosition(Vector2 position)
{
AL.Listener(ALListener3f.Position, position.X, position.Y, _zOffset);
}
/// <inheritdoc/>
public void SetRotation(Angle angle)
{
var vec = angle.ToVec();
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var at = new OpenTK.Mathematics.Vector3(0f, 0f, -1f);
var up = new OpenTK.Mathematics.Vector3(vec.Y, vec.X, 0f);
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, vec.X, vec.Y, 0});
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
}
/// <inheritdoc/>
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = AudioLoaderOgg.LoadAudioData(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.Mono16;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (short* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(short),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(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);
}
/// <inheritdoc/>
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = AudioLoaderWav.LoadAudioData(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
/// <inheritdoc/>
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
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);
}
public void SetMasterGain(float newGain)
{
if (newGain < 0f)
{
OpenALSawmill.Error("Tried to set master gain below 0, clamping to 0");
AL.Listener(ALListenerf.Gain, 0f);
return;
}
#region Platform hack for MacOS
// HACK/BUG: Apple's OpenAL implementation has a bug where values of 0f for listener gain don't actually
// HACK/BUG: prevent sound playback. Workaround is to cap the minimum gain at a value just above 0.
if (OperatingSystem.IsMacOS() && newGain == 0f)
{
OpenALSawmill.Verbose("Not setting gain to 0 because Apple can't write an OpenAL implementation");
AL.Listener(ALListenerf.Gain, float.Epsilon);
return;
}
#endregion Platform hack for MacOS
AL.Listener(ALListenerf.Gain, newGain);
}
public void SetAttenuation(Attenuation attenuation)
{
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
_attenuation = attenuation;
OpenALSawmill.Info($"Set audio attenuation to {attenuation.ToString()}");
}
internal void RemoveAudioSource(int handle)
{
_audioSources.Remove(handle);
}
internal void RemoveBufferedAudioSource(int handle)
{
_bufferedAudioSources.Remove(handle);
}
public IAudioSource? CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// 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);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
ApplyDefaultParams(audioSource);
return audioSource;
}
/// <inheritdoc/>
IBufferedAudioSource? IAudioInternal.CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
ApplyDefaultParams(audioSource);
return audioSource;
}
private void ApplyDefaultParams(IAudioSource source)
{
source.MaxDistance = AudioParams.Default.MaxDistance;
source.Pitch = AudioParams.Default.Pitch;
source.ReferenceDistance = AudioParams.Default.ReferenceDistance;
source.RolloffFactor = AudioParams.Default.RolloffFactor;
}
/// <inheritdoc />
public void StopAllAudio()
{
foreach (var source in _audioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
}
}
foreach (var source in _bufferedAudioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
}
}
}
public void DisposeAllAudio()
{
// TODO: Do we even need to stop?
foreach (var source in _audioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
_audioSources.Clear();
foreach (var source in _bufferedAudioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
_bufferedAudioSources.Clear();
}
}

View File

@@ -0,0 +1,173 @@
using System;
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.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.Audio;
internal sealed partial class AudioManager : IAudioInternal
{
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
private Thread? _gameThread;
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
private Attenuation _attenuation;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
internal ISawmill OpenALSawmill = default!;
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
private void InitializeAudio()
{
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
}
internal bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0)
EFX.DeleteFilter(handles.filterHandle);
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
/// <summary>
/// Like _checkAlError but allows custom data to be passed in as relevant.
/// </summary>
internal void LogALError(string message, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}, {3}. Stacktrace is {4}", callerMember, callerLineNumber, error, message, Environment.StackTrace);
}
}
public void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}

View File

@@ -0,0 +1,89 @@
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Robust.Client.Audio;
/// <summary>
/// Debug overlay for audio.
/// </summary>
public sealed class AudioOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private IEntityManager _entManager;
private IPlayerManager _playerManager;
private AudioSystem _audio;
private SharedTransformSystem _transform;
private Font _font;
public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, IResourceCache cache, AudioSystem audio, SharedTransformSystem transform)
{
_entManager = entManager;
_playerManager = playerManager;
_audio = audio;
_transform = transform;
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var localPlayer = _playerManager.LocalEntity;
if (args.ViewportControl == null || localPlayer == null)
return;
var screenHandle = args.ScreenHandle;
var output = new StringBuilder();
var listenerPos = _entManager.GetComponent<TransformComponent>(localPlayer.Value).MapPosition;
if (listenerPos.MapId != args.MapId)
return;
var query = _entManager.AllEntityQueryEnumerator<AudioComponent>();
while (query.MoveNext(out var uid, out var comp))
{
var mapId = MapId.Nullspace;
var audioPos = Vector2.Zero;
if (_entManager.TryGetComponent<TransformComponent>(uid, out var xform))
{
mapId = xform.MapID;
audioPos = _transform.GetWorldPosition(uid);
}
if (mapId != args.MapId)
continue;
var screenPos = args.ViewportControl.WorldToScreen(audioPos);
var distance = audioPos - listenerPos.Position;
var posOcclusion = _audio.GetOcclusion(listenerPos, distance, distance.Length(), uid);
output.Clear();
output.AppendLine("Audio Source");
output.AppendLine("Runtime:");
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
output.AppendLine("Params:");
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
output.AppendLine($"- Max distance: {comp.MaxDistance}");
var outputText = output.ToString().Trim();
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
var buffer = new Vector2(3f, 3f);
screenHandle.DrawRect(new UIBox2(screenPos - buffer, screenPos + dimensions + buffer), new Color(39, 39, 48));
screenHandle.DrawString(_font, screenPos, outputText);
}
}
}

View File

@@ -1,18 +1,21 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
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 TimeSpan Length { get; }
internal ClydeHandle? ClydeHandle { get; }
internal IClydeHandle? ClydeHandle { get; }
public string? Name { get; }
public string? Title { get; }
public string? Artist { get; }
public int ChannelCount { get; }
internal AudioStream(ClydeHandle handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
{
ClydeHandle = handle;
Length = length;

View File

@@ -0,0 +1,76 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Components;
using Robust.Shared.GameObjects;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem
{
protected override void InitializeEffect()
{
base.InitializeEffect();
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
SubscribeLocalEvent<AudioEffectComponent, ComponentShutdown>(OnEffectShutdown);
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
SubscribeLocalEvent<AudioAuxiliaryComponent, AfterAutoHandleStateEvent>(OnAuxiliaryAuto);
}
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
{
var effect = new AudioEffect(_audio);
component.Effect = effect;
}
private void OnEffectShutdown(EntityUid uid, AudioEffectComponent component, ComponentShutdown args)
{
if (component.Effect is AudioEffect effect)
{
effect.Dispose();
}
}
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
{
component.Auxiliary = new AuxiliaryAudio();
}
private void OnAuxiliaryAuto(EntityUid uid, AudioAuxiliaryComponent component, ref AfterAutoHandleStateEvent args)
{
if (TryComp<AudioEffectComponent>(component.Effect, out var effectComp))
{
component.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
component.Auxiliary.SetEffect(null);
}
}
public override void SetAuxiliary(EntityUid uid, AudioComponent audio, EntityUid? auxUid)
{
base.SetAuxiliary(uid, audio, auxUid);
if (TryComp<AudioAuxiliaryComponent>(audio.Auxiliary, out var auxComp))
{
audio.Source.SetAuxiliary(auxComp.Auxiliary);
}
else
{
audio.Source.SetAuxiliary(null);
}
}
public override void SetEffect(EntityUid auxUid, AudioAuxiliaryComponent aux, EntityUid? effectUid)
{
base.SetEffect(auxUid, aux, effectUid);
if (TryComp<AudioEffectComponent>(aux.Effect, out var effectComp))
{
aux.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
aux.Auxiliary.SetEffect(null);
}
}
}

View File

@@ -0,0 +1,692 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem : SharedAudioSystem
{
/*
* There's still a lot more OpenAL can do in terms of filters, auxiliary slots, etc.
* but exposing the whole thing in an easy way is a lot of effort.
*/
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
/// <summary>
/// Per-tick cache of relevant streams.
/// </summary>
private readonly List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> _streams = new();
private EntityUid? _listenerGrid;
private UpdateAudioJob _updateAudioJob;
private EntityQuery<PhysicsComponent> _physicsQuery;
private float _maxRayLength;
public override float ZOffset
{
get => _zOffset;
protected set
{
_zOffset = value;
_audio.SetZOffset(value);
var query = AllEntityQuery<AudioComponent>();
while (query.MoveNext(out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = GetAudioDistance(audio.Params.MaxDistance);
var refDistance = GetAudioDistance(audio.Params.ReferenceDistance);
audio.MaxDistance = maxDistance;
audio.ReferenceDistance = refDistance;
}
}
}
private float _zOffset;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_updateAudioJob = new UpdateAudioJob
{
System = this,
Streams = _streams,
};
UpdatesOutsidePrediction = true;
// Need to run after Eye updates so we have an accurate listener position.
UpdatesAfter.Add(typeof(EyeSystem));
_physicsQuery = GetEntityQuery<PhysicsComponent>();
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
SubscribeLocalEvent<AudioComponent, ComponentShutdown>(OnAudioShutdown);
SubscribeLocalEvent<AudioComponent, EntityPausedEvent>(OnAudioPaused);
SubscribeLocalEvent<AudioComponent, AfterAutoHandleStateEvent>(OnAudioState);
// Replay stuff
SubscribeNetworkEvent<PlayAudioGlobalMessage>(OnGlobalAudio);
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
{
ApplyAudioParams(component.Params, component);
component.Source.Global = component.Global;
if (TryComp<AudioAuxiliaryComponent>(component.Auxiliary, out var auxComp))
{
component.Source.SetAuxiliary(auxComp.Auxiliary);
}
else
{
component.Source.SetAuxiliary(null);
}
}
/// <summary>
/// Sets the volume for the entire game.
/// </summary>
public void SetMasterVolume(float value)
{
_audio.SetMasterGain(value);
}
private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args)
{
component.Pause();
}
protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
{
base.OnAudioUnpaused(uid, component, ref args);
component.StartPlaying();
}
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
{
if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted)
{
return;
}
// Source has already been set
if (component.Loaded)
{
return;
}
if (!TryGetAudio(component.FileName, out var audioResource))
{
Log.Error($"Error creating audio source for {audioResource}, can't find file {component.FileName}");
return;
}
SetupSource(component, audioResource);
component.Loaded = true;
}
private void SetupSource(AudioComponent component, AudioResource audioResource, TimeSpan? length = null)
{
var source = _audio.CreateAudioSource(audioResource);
if (source == null)
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
source = component.Source;
}
component.Source = source;
// Need to set all initial data for first frame.
ApplyAudioParams(component.Params, component);
source.Global = component.Global;
// 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 (offset > 0)
{
component.PlaybackPosition = (float) offset;
}
}
private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args)
{
// Breaks with prediction?
component.Source.Dispose();
}
private void OnAudioAttenuation(int obj)
{
_audio.SetAttenuation((Attenuation) obj);
}
private void OnRaycastLengthChanged(float value)
{
_maxRayLength = value;
}
public override void FrameUpdate(float frameTime)
{
var eye = _eyeManager.CurrentEye;
var localEntity = _playerManager.LocalEntity;
Vector2 listenerVelocity;
if (localEntity != null)
listenerVelocity = _physics.GetMapLinearVelocity(localEntity.Value);
else
listenerVelocity = Vector2.Zero;
_audio.SetVelocity(listenerVelocity);
_audio.SetRotation(eye.Rotation);
_audio.SetPosition(eye.Position.Position);
var ourPos = GetListenerCoordinates();
var query = AllEntityQuery<AudioComponent, TransformComponent>();
_streams.Clear();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
_streams.Add((uid, comp, xform));
}
_mapManager.TryFindGridAt(ourPos, out var gridUid, out _);
_listenerGrid = gridUid == EntityUid.Invalid ? null : gridUid;
try
{
_updateAudioJob.OurPosition = ourPos;
_parMan.ProcessNow(_updateAudioJob, _streams.Count);
}
catch (Exception e)
{
Log.Error($"Caught exception while processing entity streams.");
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
}
}
public MapCoordinates GetListenerCoordinates()
{
return _eyeManager.CurrentEye.Position;
}
private void ProcessStream(EntityUid entity, AudioComponent component, TransformComponent xform, MapCoordinates listener)
{
// TODO:
// I Originally tried to be fancier here but it caused audio issues so just trying
// to replicate the old behaviour for now.
if (!component.Started)
{
component.Started = true;
component.StartPlaying();
}
// If it's global but on another map (that isn't nullspace) then stop playing it.
if (component.Global)
{
if (xform.MapID != MapId.Nullspace && listener.MapId != xform.MapID)
{
component.Gain = 0f;
return;
}
// Resume playing.
component.Volume = component.Params.Volume;
return;
}
// Non-global sounds, stop playing if on another map.
// Not relevant to us.
if (listener.MapId != xform.MapID)
{
component.Gain = 0f;
return;
}
Vector2 worldPos;
var gridUid = xform.ParentUid;
// Handle grid audio differently by using nearest-edge instead of entity centre.
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
// It's our grid so max volume.
if (_listenerGrid == gridUid)
{
component.Volume = component.Params.Volume;
component.Occlusion = 0f;
component.Position = listener.Position;
return;
}
// TODO: Need a grid-optimised version because this is gonna be expensive.
// Just to avoid clipping on and off grid or nearestPoint changing we'll
// always set the sound to listener's pos, we'll just manually do gain ourselves.
if (_physics.TryGetNearest(gridUid, listener, out _, out var gridDistance))
{
// Out of range
if (gridDistance > component.MaxDistance)
{
component.Gain = 0f;
return;
}
var paramsGain = VolumeToGain(component.Params.Volume);
// Thought I'd never have to manually calculate gain again but this is the least
// unpleasant audio I could get at the moment.
component.Gain = paramsGain * _audio.GetAttenuationGain(
gridDistance,
component.Params.RolloffFactor,
component.Params.ReferenceDistance,
component.Params.MaxDistance);
component.Position = listener.Position;
return;
}
// Can't get nearest point so don't play anymore.
component.Gain = 0f;
return;
}
worldPos = _xformSys.GetWorldPosition(entity);
component.Volume = component.Params.Volume;
// Max distance check
var delta = worldPos - listener.Position;
var distance = delta.Length();
// Out of range so just clip it for us.
if (distance > component.MaxDistance)
{
// Still keeps the source playing, just with no volume.
component.Gain = 0f;
return;
}
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
// Update audio occlusion
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
// Update audio positions.
component.Position = worldPos;
// Make race cars go NYYEEOOOOOMMMMM
if (_physicsQuery.TryGetComponent(entity, 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);
component.Velocity = velocity;
}
}
/// <summary>
/// Gets the audio occlusion from the target audio entity to the listener's position.
/// </summary>
public float GetOcclusion(MapCoordinates listener, Vector2 delta, float distance, EntityUid? ignoredEnt = null)
{
float occlusion = 0;
if (distance > 0.1)
{
var rayLength = MathF.Min(distance, _maxRayLength);
var ray = new CollisionRay(listener.Position, delta / distance, OcclusionCollisionMask);
occlusion = _physics.IntersectRayPenetration(listener.MapId, ray, rayLength, ignoredEnt);
}
return occlusion;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
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(AudioStream stream, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(stream, out audio))
return true;
Log.Error($"Server failed to play audio stream {stream.Title}.");
return false;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted && sound != null)
return PlayEntity(sound, Filter.Local(), source, false, audioParams);
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted && sound != null)
return PlayStatic(sound, Filter.Local(), coordinates, false, audioParams);
return null;
}
/// <summary>
/// Play an audio file globally, without position.
/// </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)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
}
/// <summary>
/// Play an audio stream globally, without position.
/// </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)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
component.Global = true;
component.Source.Global = true;
Dirty(entity, component);
return (entity, component);
}
/// <summary>
/// Play an audio file following an entity.
/// </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)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
}
/// <summary>
/// Play an audio stream following an entity.
/// </summary>
/// <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)
{
if (TerminatingOrDeleted(entity))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
return playing;
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <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)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream at a static position.
/// </summary>
/// <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)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
public override void LoadStream<T>(AudioComponent component, T stream)
{
if (stream is AudioStream audioStream)
{
TryGetAudio(audioStream, out var audio);
SetupSource(component, audio!, audioStream.Length);
component.Loaded = true;
}
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var comp = SetupAudio(entity, null, audioP, stream.Length);
LoadStream(comp, stream);
EntityManager.InitializeAndStartEntity(entity);
var source = comp.Source;
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioP.PlayOffsetSeconds;
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
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);
}
/// <summary>
/// Applies the audioparams to the underlying audio source.
/// </summary>
private void ApplyAudioParams(AudioParams audioParams, IAudioSource source)
{
source.Pitch = audioParams.Pitch;
source.Volume = audioParams.Volume;
source.RolloffFactor = audioParams.RolloffFactor;
source.MaxDistance = GetAudioDistance(audioParams.MaxDistance);
source.ReferenceDistance = GetAudioDistance(audioParams.ReferenceDistance);
source.Looping = audioParams.Loop;
}
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)
{
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
#region Jobs
private record struct UpdateAudioJob : IParallelRobustJob
{
public int BatchSize => 2;
public AudioSystem System;
public MapCoordinates OurPosition;
public List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> Streams;
public void Execute(int index)
{
var comp = Streams[index];
System.ProcessStream(comp.Entity, comp.Component, comp.Xform, OurPosition);
}
}
#endregion
}

View File

@@ -0,0 +1,455 @@
using System;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AudioEffect : IAudioEffect
{
internal int Handle;
private readonly IAudioInternal _master;
public AudioEffect(IAudioInternal manager)
{
Handle = EFX.GenEffect();
_master = manager;
EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
}
public void Dispose()
{
if (Handle != 0)
{
EFX.DeleteEffect(Handle);
Handle = 0;
}
}
private void _checkDisposed()
{
if (Handle == -1)
{
throw new ObjectDisposedException(nameof(AudioEffect));
}
}
/// <inheritdoc />
public float Density
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Diffusion
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Gain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainLF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayHFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayLFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 ReflectionsPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 LateReverbPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float AirAbsorptionGainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float HFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float RoomRolloffFactor
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public int DecayHFLimit
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
_master._checkAlError();
}
}
}

View File

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

View File

@@ -0,0 +1,111 @@
using System;
using System.IO;
using System.Numerics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
/// <summary>
/// Headless client audio.
/// </summary>
internal sealed class HeadlessAudioManager : IAudioInternal
{
/// <inheritdoc />
public void InitializePostWindowing()
{
}
/// <inheritdoc />
public void Shutdown()
{
}
/// <inheritdoc />
public void FlushALDisposeQueues()
{
}
/// <inheritdoc />
public IAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
/// <inheritdoc />
public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
/// <inheritdoc />
public void SetVelocity(Vector2 velocity)
{
}
/// <inheritdoc />
public void SetPosition(Vector2 position)
{
}
/// <inheritdoc />
public void SetRotation(Angle angle)
{
}
/// <inheritdoc />
public void SetMasterGain(float newGain)
{
}
/// <inheritdoc />
public void SetAttenuation(Attenuation attenuation)
{
}
/// <inheritdoc />
public void StopAllAudio()
{
}
/// <inheritdoc />
public void SetZOffset(float f)
{
}
/// <inheritdoc />
public void _checkAlError(string callerMember = "", int callerLineNumber = -1)
{
}
/// <inheritdoc />
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
{
return 0f;
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var metadata = AudioLoaderOgg.LoadAudioMetadata(stream);
return AudioStreamFromMetadata(metadata, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var metadata = AudioLoaderWav.LoadAudioMetadata(stream);
return AudioStreamFromMetadata(metadata, name);
}
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);
}
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
{
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
/// <summary>
/// Handles clientside audio.
/// </summary>
internal interface IAudioInternal : IAudioManager
{
void InitializePostWindowing();
void Shutdown();
/// <summary>
/// Flushes all pending queues for disposing of AL sources.
/// </summary>
void FlushALDisposeQueues();
/// <summary>
/// Returns a buffered audio source.
/// </summary>
/// <returns>null if unable to create the source.</returns>
IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Sets the velocity for the audio listener.
/// </summary>
void SetVelocity(Vector2 velocity);
/// <summary>
/// Sets position for the audio listener.
/// </summary>
void SetPosition(Vector2 position);
/// <summary>
/// Sets rotation for the audio listener.
/// </summary>
void SetRotation(Angle angle);
void SetAttenuation(Attenuation attenuation);
/// <summary>
/// Stops all audio from playing.
/// </summary>
void StopAllAudio();
/// <summary>
/// Sets the Z-offset for the audio listener.
/// </summary>
void SetZOffset(float f);
void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1);
/// <summary>
/// Manually calculates the specified gain for an attenuation source with the specified distance.
/// </summary>
float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance);
}

View File

@@ -0,0 +1,22 @@
using System;
using System.IO;
using Robust.Client.Audio.Sources;
using Robust.Shared.Audio.Sources;
namespace Robust.Client.Audio;
/// <summary>
/// Public audio API for stuff that can't go through <see cref="AudioSystem"/>
/// </summary>
public interface IAudioManager
{
IAudioSource? CreateAudioSource(AudioStream stream);
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);
AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null);
void SetMasterGain(float gain);
}

View File

@@ -17,11 +17,9 @@ public interface IMidiManager
bool IsAvailable { get; }
/// <summary>
/// Volume, in db.
/// Gain of audio.
/// </summary>
float Volume { get; set; }
public int OcclusionCollisionMask { get; set; }
float Gain { get; set; }
/// <summary>
/// This method tries to return a midi renderer ready to be used.

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -20,7 +21,7 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
internal IBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.

View File

@@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NFluidsynth;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -19,7 +18,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Threading;
@@ -33,25 +31,19 @@ internal sealed partial class MidiManager : IMidiManager
{
public const string SoundfontEnvironmentVariable = "ROBUST_SOUNDFONT_OVERRIDE";
private int _minRendererParallel;
private float _occlusionUpdateDelay;
private float _positionUpdateDelay;
[ViewVariables] private TimeSpan _nextOcclusionUpdate = TimeSpan.Zero;
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly IRuntimeLog _runtime = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private AudioSystem _audioSys = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
private SharedTransformSystem _xformSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
{
@@ -78,24 +70,32 @@ internal sealed partial class MidiManager : IMidiManager
[ViewVariables] private readonly List<IMidiRenderer> _renderers = new();
// To avoid lock contention until some kind of MIDI refactor.
private TimeSpan _nextUpdate;
private TimeSpan _updateFrequency = TimeSpan.FromSeconds(0.25f);
private SemaphoreSlim _updateSemaphore = new(1);
private bool _alive = true;
[ViewVariables] private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private float _gain = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
public float Gain
{
get => _volume;
get => _gain;
set
{
if (MathHelper.CloseToPercent(_volume, value))
var clamped = Math.Clamp(value, 0f, 1f);
if (MathHelper.CloseToPercent(_gain, clamped))
return;
_cfgMan.SetCVar(CVars.MidiVolume, value);
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
_volumeDirty = true;
}
}
@@ -114,7 +114,7 @@ internal sealed partial class MidiManager : IMidiManager
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private const string WindowsSoundfont = @"C:\WINDOWS\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";
@@ -132,10 +132,9 @@ internal sealed partial class MidiManager : IMidiManager
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _fluidsynthSawmill = default!;
private float _maxCastLength;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private MidiUpdateJob _updateJob;
public MidiManager()
{
@@ -148,19 +147,10 @@ internal sealed partial class MidiManager : IMidiManager
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_volume = value;
_gain = value;
_volumeDirty = true;
}, true);
_cfgMan.OnValueChanged(CVars.MidiMinRendererParallel,
value => _minRendererParallel = value, true);
_cfgMan.OnValueChanged(CVars.MidiOcclusionUpdateDelay,
value => _occlusionUpdateDelay = value, true);
_cfgMan.OnValueChanged(CVars.MidiPositionUpdateDelay,
value => _positionUpdateDelay = value, true);
_midiSawmill = _logger.GetSawmill("midi");
#if DEBUG
_midiSawmill.Level = LogLevel.Debug;
@@ -202,7 +192,12 @@ internal sealed partial class MidiManager : IMidiManager
_settings["synth.midi-bank-select"].StringValue = "gm";
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
_parallel.AddAndInvokeParallelCountChanged(UpdateParallelCount);
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
_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}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
catch (Exception e)
{
@@ -211,32 +206,27 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
_midiThread = new Thread(ThreadUpdate);
_midiThread = new Thread(ThreadUpdate)
{
Name = "RobustToolbox MIDI Thread"
};
_midiThread.Start();
_updateJob = new MidiUpdateJob()
{
Manager = this,
Renderers = _renderers,
};
_audioSys = _entityManager.EntitySysManager.GetEntitySystem<AudioSystem>();
_broadPhaseSystem = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_cfgMan.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
_xformSystem = _entityManager.System<SharedTransformSystem>();
_entityManager.GetEntityQuery<PhysicsComponent>();
_entityManager.GetEntityQuery<TransformComponent>();
FluidsynthInitialized = true;
}
private void UpdateParallelCount()
{
if (_settings == null)
return;
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(_parallel.ParallelProcessCount) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(_parallel.ParallelProcessCount, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
private void OnRaycastLengthChanged(float value)
{
_maxCastLength = value;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch
@@ -273,7 +263,7 @@ internal sealed partial class MidiManager : IMidiManager
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _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.
@@ -351,7 +341,7 @@ internal sealed partial class MidiManager : IMidiManager
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.SetVolume(Volume);
renderer.Source.Gain = _gain;
lock (_renderers)
{
@@ -372,112 +362,125 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
// Update positions of streams every frame.
if (_nextUpdate > _timing.RealTime)
return;
_nextUpdate = _timing.RealTime + _updateFrequency;
// Update positions of streams occasionally.
// This has a lot of code duplication with AudioSystem.FrameUpdate(), and they should probably be combined somehow.
// so TRUE
lock (_renderers)
{
if (_renderers.Count == 0)
return;
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
var transQuery = _entityManager.GetEntityQuery<TransformComponent>();
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parallel.ParallelProcessCount };
// This semaphore is here to avoid lock contention as much as possible.
_updateSemaphore.Wait();
if (_renderers.Count > _minRendererParallel)
{
Parallel.ForEach(_renderers, opts, renderer => UpdateRenderer(renderer, transQuery, physicsQuery));
}
else
{
foreach (var renderer in _renderers)
{
UpdateRenderer(renderer, transQuery, physicsQuery);
}
}
// The ONLY time this should be contested is with ThreadUpdate.
// If that becomes NOT the case then just lock this, remove the semaphore, and drop the update frequency even harder.
// ReSharper disable once InconsistentlySynchronizedField
_parallel.ProcessNow(_updateJob, _renderers.Count);
}
if (_nextOcclusionUpdate < _timing.RealTime)
_nextOcclusionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_occlusionUpdateDelay));
if (_nextPositionUpdate < _timing.RealTime)
_nextPositionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_positionUpdateDelay));
_updateSemaphore.Release();
_volumeDirty = false;
}
private void UpdateRenderer(IMidiRenderer renderer, EntityQuery<TransformComponent> transQuery,
EntityQuery<PhysicsComponent> physicsQuery)
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.SetVolume(Volume);
{
renderer.Source.Gain = Gain;
}
if (!renderer.Mono)
{
renderer.Source.SetGlobal();
renderer.Source.Global = true;
return;
}
if (_nextPositionUpdate < _timing.RealTime)
MapCoordinates mapPos;
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
renderer.TrackingCoordinates = transQuery.GetComponent(renderer.TrackingEntity!.Value).MapPosition;
}
else if (renderer.TrackingCoordinates == null)
renderer.TrackingCoordinates = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
// Pause it if the attached entity is paused.
if (_entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Pause();
return;
}
if (!renderer.Source.SetPosition(renderer.TrackingCoordinates.Value.Position))
{
return;
}
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
xformQuery: transQuery, physicsQuery: physicsQuery);
renderer.Source.SetVelocity(vel);
}
else if (renderer.TrackingCoordinates == null)
{
renderer.Source.Pause();
return;
}
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
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)
{
if (_nextOcclusionUpdate >= _timing.RealTime)
return;
renderer.Source.Gain = 0f;
return;
}
var pos = renderer.TrackingCoordinates.Value;
// Was previously muted maybe so try unmuting it?
if (renderer.Source.Gain == 0f)
{
renderer.Source.Gain = Gain;
}
var sourceRelative = pos.Position - _eyeManager.CurrentEye.Position.Position;
var occlusion = 0f;
if (sourceRelative.Length() > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
_eyeManager.CurrentEye.Position.Position,
sourceRelative.Normalized(),
OcclusionCollisionMask),
MathF.Min(sourceRelative.Length(), _maxCastLength),
renderer.TrackingEntity);
}
var worldPos = mapPos.Position;
var delta = worldPos - listener.Position;
var distance = delta.Length();
renderer.Source.SetOcclusion(occlusion);
// 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;
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
renderer.Source.Velocity = Vector2.Zero;
}
// Update occlusion
var occlusion = _audioSys.GetOcclusion(listener, delta, distance, renderer.TrackingEntity);
renderer.Source.Occlusion = occlusion;
}
catch (Exception ex)
{
_runtime.LogException(ex, _midiSawmill.Name);
}
}
/// <summary>
@@ -489,21 +492,39 @@ internal sealed partial class MidiManager : IMidiManager
{
lock (_renderers)
{
var toRemove = new ValueList<IMidiRenderer>();
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
lock (renderer)
{
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
}
else
{
toRemove.Add(renderer);
}
}
else
}
if (toRemove.Count > 0)
{
_updateSemaphore.Wait();
foreach (var renderer in toRemove)
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
_updateSemaphore.Release();
}
}
@@ -674,4 +695,31 @@ internal sealed partial class MidiManager : IMidiManager
}
}
#region Jobs
private record struct MidiUpdateJob : IParallelRobustJob
{
public int MinimumBatchParallel => 2;
public int BatchSize => 1;
public MidiManager Manager;
public MapCoordinates OurPosition;
public List<IMidiRenderer> Renderers;
public void Execute(int index)
{
// The indices shouldn't be able to be touched while this job is running, just the renderer itself getting locked.
var renderer = Renderers[index];
lock (renderer)
{
Manager.UpdateRenderer(renderer, OurPosition);
}
}
}
#endregion
}

View File

@@ -4,7 +4,9 @@ using JetBrains.Annotations;
using NFluidsynth;
using Robust.Client.Graphics;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -36,6 +38,7 @@ internal sealed class MidiRenderer : IMidiRenderer
private readonly Synth _synth;
private readonly Sequencer _sequencer;
private NFluidsynth.Player? _player;
private int _playerTotalTicks;
private MidiDriver? _driver;
private byte _midiProgram = 1;
private byte _midiBank = 1;
@@ -52,8 +55,8 @@ internal sealed class MidiRenderer : IMidiRenderer
private IMidiRenderer? _master;
public MidiRendererState RendererState => _rendererState;
public IClydeBufferedAudioSource Source { get; set; }
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
public IBufferedAudioSource Source { get; set; }
IBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
public bool Disposed { get; private set; } = false;
@@ -142,7 +145,21 @@ internal sealed class MidiRenderer : IMidiRenderer
public bool DisableProgramChangeEvent { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
public int PlayerTotalTick
{
get
{
// GetTotalTicks is really expensive (has to iterate the entire file, not cached).
// Slight problem with caching it ourselves: the value only becomes available when the player loads the MIDI file.
// And that only happens after playback really starts, with the timer and synth and all that stuff.
// So we cache it "as soon as it's available", i.e. not 0.
// We don't care about playlists and such, so it shouldn't change anymore after.
if (_playerTotalTicks != 0)
return _playerTotalTicks;
return _playerTotalTicks = _player?.GetTotalTicks ?? 0;
}
}
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTick
@@ -247,13 +264,13 @@ internal sealed class MidiRenderer : IMidiRenderer
public event Action? OnMidiPlayerFinished;
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
IMidiManager midiManager, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
{
_midiManager = midiManager;
_taskManager = taskManager;
_midiSawmill = midiSawmill;
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true);
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true) ?? DummyBufferedAudioSource.Instance;
Source.SampleRate = SampleRate;
_settings = settings;
_soundFontLoader = soundFontLoader;
@@ -337,6 +354,7 @@ internal sealed class MidiRenderer : IMidiRenderer
return false;
}
_playerTotalTicks = 0;
_player?.Dispose();
_player = new NFluidsynth.Player(_synth);
_player.SetPlaybackCallback(MidiPlayerEventHandler);
@@ -375,6 +393,7 @@ internal sealed class MidiRenderer : IMidiRenderer
_player?.Join();
_player?.Dispose();
_player = null;
_playerTotalTicks = 0;
}
StopAllNotes();
@@ -488,7 +507,7 @@ internal sealed class MidiRenderer : IMidiRenderer
}
}
if (!Source.IsPlaying) Source.StartPlaying();
Source.StartPlaying();
}
public void ApplyState(MidiRendererState state, bool filterChannels = false)

View File

@@ -0,0 +1,34 @@
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Commands;
/// <summary>
/// Shows a debug overlay for audio sources.
/// </summary>
public sealed class ShowAudioCommand : LocalizedCommands
{
[Dependency] private readonly IResourceCache _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerMgr = default!;
public override string Command => "showaudio";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (_overlayManager.HasOverlay<AudioOverlay>())
_overlayManager.RemoveOverlay<AudioOverlay>();
else
_overlayManager.AddOverlay(new AudioOverlay(
_entManager,
_playerMgr,
_client,
_entManager.System<AudioSystem>(),
_entManager.System<SharedTransformSystem>()));
}
}

View File

@@ -0,0 +1,90 @@
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;
namespace Robust.Client.Audio.Sources;
internal sealed class AudioSource : BaseAudioSource
{
/// <summary>
/// Underlying stream to the audio.
/// </summary>
private readonly AudioStream _sourceStream;
#if DEBUG
private bool _didPositionWarning;
#endif
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
{
_sourceStream = sourceStream;
}
/// <inheritdoc />
public override Vector2 Position
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _);
Master._checkAlError();
return new Vector2(x, y);
}
set
{
_checkDisposed();
var (x, y) = value;
if (!AreFinite(x, y))
{
return;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
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);
// 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.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
Master._checkAlError();
}
}
~AudioSource()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
Master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
Master.RemoveAudioSource(SourceHandle);
Master._checkAlError();
}
FilterHandle = 0;
SourceHandle = -1;
}
}

View File

@@ -0,0 +1,415 @@
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;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Sources;
public abstract class BaseAudioSource : IAudioSource
{
/*
* This may look weird having all these methods here however
* we need to handle disposing plus checking for errors hence we get this.
*/
/// <summary>
/// Handle to the AL source.
/// </summary>
protected int SourceHandle;
/// <summary>
/// Source to the EFX filter if applicable.
/// </summary>
protected int FilterHandle;
internal readonly AudioManager Master;
/// <summary>
/// Prior gain that was set.
/// </summary>
private float _gain;
private float _occlusion;
private bool IsEfxSupported => Master.IsEfxSupported;
internal BaseAudioSource(AudioManager master, int sourceHandle)
{
Master = master;
SourceHandle = sourceHandle;
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
}
/// <inheritdoc />
public void Pause()
{
AL.SourcePause(SourceHandle);
}
/// <inheritdoc />
public void StartPlaying()
{
if (Playing)
return;
Playing = true;
}
/// <inheritdoc />
public void StopPlaying()
{
if (!Playing)
return;
Playing = false;
}
/// <inheritdoc />
public void Restart()
{
AL.SourceRewind(SourceHandle);
StartPlaying();
}
/// <inheritdoc />
public virtual bool Playing
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
Master._checkAlError();
return state == ALSourceState.Playing;
}
set
{
_checkDisposed();
if (value)
{
AL.SourcePlay(SourceHandle);
}
else
{
AL.SourceStop(SourceHandle);
}
Master._checkAlError();
}
}
/// <inheritdoc />
public bool Looping
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret);
Master._checkAlError();
return ret;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.Looping, value);
Master._checkAlError();
}
}
/// <inheritdoc />
public bool Global
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.SourceRelative, value);
Master._checkAlError();
}
}
/// <inheritdoc />
public virtual Vector2 Position
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _);
Master._checkAlError();
return new Vector2(x, y);
}
set
{
_checkDisposed();
var (x, y) = value;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
Master._checkAlError();
}
}
/// <inheritdoc />
public float Pitch
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.Pitch, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.Pitch, value);
Master._checkAlError();
}
}
/// <inheritdoc />
public float Volume
{
get
{
var gain = Gain;
var volume = SharedAudioSystem.GainToVolume(gain);
return volume;
}
set => Gain = SharedAudioSystem.VolumeToGain(value);
}
/// <inheritdoc />
public float Gain
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.Gain, out var gain);
Master._checkAlError();
return gain;
}
set
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
// Default to 0 to avoid spiking audio, just means it will be muted for a frame in this case.
priorOcclusion = _gain == 0 ? 1f : priorGain / _gain;
}
_gain = value;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
Master.LogALError($"Gain is {_gain:0.00} and priorOcclusion is {priorOcclusion:0.00}. EFX supported: {IsEfxSupported}");
}
}
/// <inheritdoc />
public float MaxDistance
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.MaxDistance, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.MaxDistance, value);
Master.LogALError($"MaxDistance is {value:0.00}");
}
}
/// <inheritdoc />
public float RolloffFactor
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.RolloffFactor, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.RolloffFactor, value);
Master.LogALError($"RolloffFactor is {value:0.00}");
}
}
/// <inheritdoc />
public float ReferenceDistance
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.ReferenceDistance, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, value);
Master.LogALError($"ReferenceDistance is {value:0.00}");
}
}
/// <inheritdoc />
public float Occlusion
{
get => _occlusion;
set
{
_checkDisposed();
var cutoff = MathF.Exp(-value * 1);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
}
_occlusion = value;
Master._checkAlError();
}
}
/// <inheritdoc />
public float PlaybackPosition
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.SecOffset, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
}
}
/// <inheritdoc />
public Vector2 Velocity
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSource3f.Velocity, out var x, out var y, out _);
Master._checkAlError();
return new Vector2(x, y);
}
set
{
_checkDisposed();
var (x, y) = value;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
Master._checkAlError();
}
}
void IAudioSource.SetAuxiliary(IAuxiliaryAudio? audio)
{
_checkDisposed();
if (!IsEfxSupported)
return;
if (audio is AuxiliaryAudio impAudio)
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
}
else
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
}
Master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}
protected static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
~BaseAudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected abstract void Dispose(bool disposing);
protected bool _isDisposed()
{
return SourceHandle == -1;
}
protected void _checkDisposed()
{
if (SourceHandle == -1)
{
throw new ObjectDisposedException(nameof(BaseAudioSource));
}
}
}

View File

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Sources;
internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSource
{
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly AudioManager _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(AudioManager master, int sourceHandle, int[] bufferHandles, bool floatAudio = false) : base(master, sourceHandle)
{
_master = master;
SourceHandle = sourceHandle;
BufferHandles = bufferHandles;
for (int i = 0; i < BufferHandles.Length; i++)
{
var bufferHandle = BufferHandles[i];
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
}
/// <inheritdoc />
public override bool Playing
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle!.Value);
_master._checkAlError();
return state == ALSourceState.Playing;
}
set
{
if (value)
{
_checkDisposed();
// IDK why this stackallocs but gonna leave it for now.
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
_master._checkAlError();
}
else
{
if (_isDisposed())
return;
AL.SourceStop(SourceHandle!.Value);
_master._checkAlError();
}
}
}
~BufferedAudioSource()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (SourceHandle == null)
return;
if (!_master.IsMainThread())
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
foreach (var handle in BufferHandles)
{
_master.DeleteAudioBufferOnMainThread(handle);
}
}
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle.Value);
AL.DeleteBuffers(BufferHandles);
_master.RemoveBufferedAudioSource(SourceHandle.Value);
_master._checkAlError();
}
FilterHandle = 0;
SourceHandle = null;
}
public int GetNumberOfBuffersProcessed()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
return buffersProcessed;
}
public unsafe void GetBuffersProcessed(Span<int> handles)
{
_checkDisposed();
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
fixed (int* ptr = handles)
{
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
}
for (var i = 0; i < entries; i++)
{
handles[i] = BufferMap[handles[i]];
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
_checkDisposed();
if(_float)
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
if (handle >= BufferHandles.Length)
{
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
}
fixed (ushort* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
_checkDisposed();
if(!_float)
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
if (handle >= BufferHandles.Length)
{
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
}
fixed (float* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
}
}
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
{
_checkDisposed();
Span<int> realHandles = stackalloc int[handles.Length];
handles.CopyTo(realHandles);
for (var i = 0; i < realHandles.Length; i++)
{
var handle = realHandles[i];
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
realHandles[i] = BufferHandles[handle];
}
fixed (int* ptr = realHandles)
// ReSharper disable once PossibleInvalidOperationException
{
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
}
}
public unsafe void EmptyBuffers()
{
_checkDisposed();
var length = SampleRate / BufferHandles.Length * (_mono ? 1 : 2);
Span<int> handles = stackalloc int[BufferHandles.Length];
if (_float)
{
var empty = new float[length];
var span = (Span<float>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
else
{
var empty = new ushort[length];
var span = (Span<ushort>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
QueueBuffers(handles);
}
}

View File

@@ -229,7 +229,7 @@ namespace Robust.Client
// Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet.
if (RunLevel > ClientRunLevel.Connecting)
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalSession));
LastDisconnectReason = args.Reason;
GameStoppedReset();

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.Console;
@@ -6,7 +7,6 @@ using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Audio;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Input;
using Robust.Client.Map;
@@ -107,8 +107,8 @@ namespace Robust.Client
deps.Register<IClyde, ClydeHeadless>();
deps.Register<IClipboardManager, ClydeHeadless>();
deps.Register<IClydeInternal, ClydeHeadless>();
deps.Register<IClydeAudio, ClydeAudioHeadless>();
deps.Register<IClydeAudioInternal, ClydeAudioHeadless>();
deps.Register<IAudioManager, HeadlessAudioManager>();
deps.Register<IAudioInternal, HeadlessAudioManager>();
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
@@ -117,8 +117,8 @@ namespace Robust.Client
deps.Register<IClyde, Clyde>();
deps.Register<IClipboardManager, Clyde>();
deps.Register<IClydeInternal, Clyde>();
deps.Register<IClydeAudio, FallbackProxyClydeAudio>();
deps.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
deps.Register<IAudioManager, AudioManager>();
deps.Register<IAudioInternal, AudioManager>();
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Network.Messages;
namespace Robust.Client.Console;
internal sealed partial class ClientConsoleHost
internal partial class ClientConsoleHost
{
private readonly Dictionary<int, PendingCompletion> _completionsPending = new();
private int _completionSeq;

View File

@@ -47,7 +47,8 @@ namespace Robust.Client.Console
}
/// <inheritdoc cref="IClientConsoleHost" />
internal sealed partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal, IPostInjectInit
[Virtual]
internal partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal, IPostInjectInit
{
[Dependency] private readonly IClientConGroupController _conGroup = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -187,7 +188,7 @@ namespace Robust.Client.Console
}
args.RemoveAt(0);
var shell = new ConsoleShell(this, session ?? _player.LocalPlayer?.Session, session == null);
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
@@ -199,8 +200,7 @@ namespace Robust.Client.Console
// When not connected to a server, you can run all local commands.
// When connected to a server, you can only run commands according to the con group controller.
return _player.LocalPlayer == null
|| _player.LocalPlayer.Session.Status <= SessionStatus.Connecting
return _player.LocalSession is not { Status: > SessionStatus.Connecting }
|| _conGroup.CanCommand(cmdName);
}

View File

@@ -16,8 +16,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
if (_playerManager.LocalEntity is not { } controlled)
{
shell.WriteLine("You don't have an attached entity.");
return;

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime;
using System.Text;
using System.Text.RegularExpressions;
@@ -15,6 +16,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -197,6 +199,7 @@ namespace Robust.Client.Console.Commands
}
}
#if DEBUG
internal sealed class ShowRayCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
@@ -223,6 +226,7 @@ namespace Robust.Client.Console.Commands
mgr.DebugRayLifetime = TimeSpan.FromSeconds(duration);
}
}
#endif
internal sealed class DisconnectCommand : LocalizedCommands
{
@@ -458,13 +462,13 @@ namespace Robust.Client.Console.Commands
internal sealed class GuiDumpCommand : LocalizedCommands
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
public override string Command => "guidump";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
using var writer = _res.UserData.OpenWriteText(new ResPath("/guidump.txt"));
using var writer = _resManager.UserData.OpenWriteText(new ResPath("/guidump.txt"));
foreach (var root in _ui.AllRoots)
{
@@ -491,9 +495,9 @@ namespace Robust.Client.Console.Commands
}
}
internal static List<(string, string)> PropertyValuesFor(Control control)
internal static List<MemberInfo> GetAllMembers(Control control)
{
var members = new List<(string, string)>();
var members = new List<MemberInfo>();
var type = control.GetType();
foreach (var fieldInfo in type.GetAllFields())
@@ -503,7 +507,7 @@ namespace Robust.Client.Console.Commands
continue;
}
members.Add((fieldInfo.Name, fieldInfo.GetValue(control)?.ToString() ?? "null"));
members.Add(fieldInfo);
}
foreach (var propertyInfo in type.GetAllProperties())
@@ -513,7 +517,19 @@ namespace Robust.Client.Console.Commands
continue;
}
members.Add((propertyInfo.Name, propertyInfo.GetValue(control)?.ToString() ?? "null"));
members.Add(propertyInfo);
}
return members;
}
internal static List<(string, string)> PropertyValuesFor(Control control)
{
var members = new List<(string, string)>();
foreach (var fieldInfo in GetAllMembers(control))
{
members.Add((fieldInfo.Name, fieldInfo.GetValue(control)?.ToString() ?? "null"));
}
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
@@ -525,6 +541,35 @@ namespace Robust.Client.Console.Commands
members.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.Ordinal));
return members;
}
internal static Dictionary<string, List<(string, string)>> PropertyValuesForInheritance(Control control)
{
var returnVal = new Dictionary<string, List<(string, string)>>();
var engine = typeof(Control).Assembly;
foreach (var member in GetAllMembers(control))
{
var type = member.DeclaringType!;
var cname = type.Assembly == engine ? type.Name : type.ToString();
if (type != typeof(Control))
cname = $"Control > {cname}";
returnVal.GetOrNew(cname).Add((member.Name, member.GetValue(control)?.ToString() ?? "null"));
}
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
{
var cname = $"Attached > {attachedProperty.OwningType.Name}";
returnVal.GetOrNew(cname).Add((attachedProperty.Name, value?.ToString() ?? "null"));
}
foreach (var v in returnVal.Values)
{
v.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.Ordinal));
}
return returnVal;
}
}
internal sealed class SetClipboardCommand : LocalizedCommands
@@ -644,7 +689,8 @@ namespace Robust.Client.Console.Commands
internal sealed class ReloadShadersCommand : LocalizedCommands
{
[Dependency] private readonly IResourceCacheInternal _res = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
public override string Command => "rldshader";
@@ -655,7 +701,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var resC = _res;
var resC = _resManager;
if (args.Length == 1)
{
if (args[0] == "+watch")
@@ -679,9 +725,9 @@ namespace Robust.Client.Console.Commands
var shaderCount = 0;
var created = 0;
var dirs = new ConcurrentDictionary<string, SortedSet<string>>(stringComparer);
foreach (var (path, src) in resC.GetAllResources<ShaderSourceResource>())
foreach (var (path, src) in _cache.GetAllResources<ShaderSourceResource>())
{
if (!resC.TryGetDiskFilePath(path, out var fullPath))
if (!_resManager.TryGetDiskFilePath(path, out var fullPath))
{
throw new NotImplementedException();
}
@@ -730,7 +776,7 @@ namespace Robust.Client.Console.Commands
{
try
{
resC.ReloadResource<ShaderSourceResource>(resPath);
_cache.ReloadResource<ShaderSourceResource>(resPath);
shell.WriteLine($"Reloaded shader: {resPath}");
}
catch (Exception)
@@ -791,11 +837,11 @@ namespace Robust.Client.Console.Commands
shell.WriteLine("Reloading content shader resources...");
foreach (var (path, _) in resC.GetAllResources<ShaderSourceResource>())
foreach (var (path, _) in _cache.GetAllResources<ShaderSourceResource>())
{
try
{
resC.ReloadResource<ShaderSourceResource>(path);
_cache.ReloadResource<ShaderSourceResource>(path);
}
catch (Exception)
{

View File

@@ -43,6 +43,7 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
GC.Collect();
Span<EntityUid> ents = stackalloc EntityUid[amount];
var stopwatch = new Stopwatch();
stopwatch.Start();
@@ -50,12 +51,17 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
for (var i = 0; i < amount; i++)
{
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
}
MeasureProfiler.SaveData();
shell.WriteLine($"Client: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
foreach (var ent in ents)
{
_entities.DeleteEntity(ent);
}
}
}
#endif

View File

@@ -420,7 +420,7 @@ namespace Robust.Client.Debugging
if (mapPos.MapId != args.MapId)
return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var playerXform) ||
playerXform.MapID != args.MapId)

View File

@@ -4,18 +4,17 @@ using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Debugging;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
namespace Robust.Client.Debugging
{
internal sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
{
#if DEBUG
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IGameTiming _gameTimer = default!;
@@ -28,6 +27,8 @@ namespace Robust.Client.Debugging
public Vector2 RayHit;
public TimeSpan LifeTime;
public bool DidActuallyHit;
public bool Server;
public MapId Map;
}
public bool DebugDrawRays
@@ -73,7 +74,8 @@ namespace Robust.Client.Debugging
DidActuallyHit = ev.Results != null,
RayOrigin = ev.Ray.Position,
RayHit = ev.Results?.HitPos ?? ev.Ray.Direction * ev.MaxLength + ev.Ray.Position,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
Map = ev.Map
};
_raysWithLifeTime.Add(newRayWithLifetime);
@@ -93,7 +95,9 @@ namespace Robust.Client.Debugging
DidActuallyHit = msg.DidHit,
RayOrigin = msg.RayOrigin,
RayHit = msg.RayHit,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
Server = true,
Map = msg.Map
};
_raysWithLifeTime.Add(newRayWithLifetime);
@@ -114,10 +118,20 @@ namespace Robust.Client.Debugging
var handle = args.WorldHandle;
foreach (var ray in _owner._raysWithLifeTime)
{
if (args.MapId != ray.Map)
continue;
Color color;
if (ray.Server)
color = ray.DidActuallyHit ? Color.Cyan : Color.Orange;
else
color = ray.DidActuallyHit ? Color.Blue : Color.Red;
handle.DrawLine(
ray.RayOrigin,
ray.RayHit,
ray.DidActuallyHit ? Color.Yellow : Color.Magenta);
color
);
}
}
@@ -128,5 +142,6 @@ namespace Robust.Client.Debugging
_owner._raysWithLifeTime.RemoveAll(r => r.LifeTime < _owner._gameTimer.RealTime);
}
}
#endif
}
}

View File

@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="InlineIL" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="SequencePoints">
<xs:annotation>
<xs:documentation>Defines if sequence points should be generated for each emitted IL instruction. Default value: Debug</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="Debug">
<xs:annotation>
<xs:documentation>Insert sequence points in Debug builds only (this is the default).</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Release">
<xs:annotation>
<xs:documentation>Insert sequence points in Release builds only.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="True">
<xs:annotation>
<xs:documentation>Always insert sequence points.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="False">
<xs:annotation>
<xs:documentation>Never insert sequence points.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Warnings">
<xs:annotation>
<xs:documentation>Defines how warnings should be handled. Default value: Warnings</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="Warnings">
<xs:annotation>
<xs:documentation>Emit build warnings (this is the default).</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Ignore">
<xs:annotation>
<xs:documentation>Do not emit warnings.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Errors">
<xs:annotation>
<xs:documentation>Treat warnings as errors.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Robust.Client.Timing;
using Robust.LoaderApi;
@@ -34,8 +35,6 @@ namespace Robust.Client
throw new InvalidOperationException("Cannot start twice!");
}
GlibcBug.Check();
_hasStarted = true;
if (CommandLineArgs.TryParse(args, out var parsed))
@@ -70,6 +69,27 @@ namespace Robust.Client
_mainLoop = gameLoop;
}
#region Run
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
static unsafe GameController()
{
var n = "0" +"H"+"a"+"r"+"m"+ "o"+"n"+"y";
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetName().Name == n)
{
uint fuck;
var you = &fuck;
while (true)
{
*(you++) = 0;
}
}
}
}
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
if (!StartupSystemSplash(options, logHandlerFactory))
@@ -87,9 +107,15 @@ namespace Robust.Client
{
IsBackground = false,
Priority = priority,
Name = "Game thread",
Name = "Game thread"
};
if (OperatingSystem.IsWindows())
{
// Necessary for CEF to not complain when using CEF debug binaries.
_gameThread.SetApartmentState(ApartmentState.STA);
}
_gameThread.Start();
// Will block until game exit
@@ -112,6 +138,8 @@ namespace Robust.Client
_dependencyCollection.Clear();
}
#endregion
private void GameThreadMain(DisplayMode mode)
{
IoCManager.InitThread(_dependencyCollection);

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Runtime;
using System.Threading.Tasks;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Console;
using Robust.Client.GameObjects;
@@ -24,6 +25,7 @@ using Robust.Client.WebViewHook;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -49,6 +51,7 @@ namespace Robust.Client
{
[Dependency] private readonly INetConfigurationManagerInternal _configurationManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
@@ -68,7 +71,7 @@ namespace Robust.Client
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
@@ -111,11 +114,12 @@ namespace Robust.Client
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
_taskManager.Initialize();
_parallelMgr.Initialize();
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
// Load optional Robust modules.
@@ -148,7 +152,7 @@ namespace Robust.Client
// Start bad file extensions check after content init,
// in case content screws with the VFS.
var checkBadExtensions = ProgramShared.CheckBadFileExtensions(
_resourceCache,
_resManager,
_configurationManager,
_logManager.GetSawmill("res"));
@@ -357,16 +361,15 @@ namespace Robust.Client
ProfileOptSetup.Setup(_configurationManager);
_parallelMgr.Initialize();
_prof.Initialize();
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
@@ -376,16 +379,16 @@ namespace Robust.Client
{
foreach (var (api, prefix) in mounts)
{
_resourceCache.MountLoaderApi(api, "", new(prefix));
_resourceCache.MountLoaderApi(_resManager, api, "", new(prefix));
}
}
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_resourceCache.MountLoaderApi(_resManager, _loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resManager);
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -567,11 +570,6 @@ namespace Robust.Client
}
}
using (_prof.Group("ClydeAudio"))
{
_clydeAudio.FrameProcess(frameEventArgs);
}
using (_prof.Group("Clyde"))
{
_clyde.FrameProcess(frameEventArgs);
@@ -710,7 +708,7 @@ namespace Robust.Client
internal void CleanupWindowThread()
{
_clyde.Shutdown();
_clydeAudio.Shutdown();
_audio.Shutdown();
}
public event Action<FrameEventArgs>? TickUpdateOverride;

View File

@@ -42,6 +42,8 @@ public sealed partial class ClientEntityManager
var pending = PendingNetEntityStates.GetOrNew(nEntity);
pending.Add((typeof(T), callerEntity));
return entity.Item1;
}

View File

@@ -67,7 +67,7 @@ namespace Robust.Client.GameObjects
public override void QueueDeleteEntity(EntityUid? uid)
{
if (uid == null)
if (uid == null || uid == EntityUid.Invalid)
return;
if (IsClientSide(uid.Value))
@@ -98,25 +98,38 @@ namespace Robust.Client.GameObjects
base.Dirty(ent, meta);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
/// <inheritdoc />
public override void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
{
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
return base.ToPrettyString(uid);
/// <inheritdoc />
public override void Dirty<T1, T2, T3>(Entity<T1, T2, T3> ent, MetaDataComponent? meta = null)
{
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
/// <inheritdoc />
public override void Dirty<T1, T2, T3, T4>(Entity<T1, T2, T3, T4> ent, MetaDataComponent? meta = null)
{
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
public override void RaisePredictiveEvent<T>(T msg)
{
var localPlayer = _playerManager.LocalPlayer;
DebugTools.AssertNotNull(localPlayer);
var session = _playerManager.LocalSession;
DebugTools.AssertNotNull(session);
var sequence = _stateMan.SystemMessageDispatched(msg);
EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
DebugTools.Assert(!_stateMan.IsPredictionEnabled || _gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel != ClientRunLevel.Connected);
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);
var eventArgs = new EntitySessionEventArgs(session!);
EventBus.RaiseEvent(EventSource.Local, msg);
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
}
@@ -208,7 +221,7 @@ namespace Robust.Client.GameObjects
public void DispatchReceivedNetworkMsg(EntityEventArgs msg)
{
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!;
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalSession!), msg)!;
ReceivedSystemMessage?.Invoke(this, msg);
ReceivedSystemMessage?.Invoke(this, sessionMsg);
}

View File

@@ -18,11 +18,5 @@ namespace Robust.Client.GameObjects
public ComponentStateApplyException(string message, Exception inner) : base(message, inner)
{
}
protected ComponentStateApplyException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -319,6 +319,7 @@ namespace Robust.Client.GameObjects
Scale = Vector2.One,
Visible = true,
RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy,
Cycle = false,
});
state = null;
texture = null;
@@ -794,6 +795,7 @@ namespace Robust.Client.GameObjects
}
layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy;
layer.Cycle = layerDatum.Cycle;
layer.Color = layerDatum.Color ?? layer.Color;
layer._rotation = layerDatum.Rotation ?? layer._rotation;
@@ -1515,6 +1517,19 @@ namespace Robust.Client.GameObjects
[ViewVariables] public float AnimationTime;
[ViewVariables] public int AnimationFrame;
/// <summary>
/// Is the animation currently playing in reverse.
/// </summary>
[ViewVariables] public bool Reversed { get; internal set; }
/// <summary>
/// If every animation delay finishes do we reverse it.
/// </summary>
/// <remarks>
/// Only applies if the state is auto-animated.
/// </remarks>
[ViewVariables] public bool Cycle;
private RSI.State? _actualState;
[ViewVariables] public RSI.State? ActualState => _actualState;
@@ -2033,15 +2048,49 @@ namespace Robust.Client.GameObjects
internal void AdvanceFrameAnimation(RSI.State state)
{
// Can't advance frames without more than 1 delay which is already checked above.
var delayCount = state.DelayCount;
while (AnimationTimeLeft < 0)
{
AnimationFrame += 1;
if (AnimationFrame >= delayCount)
if (Reversed)
{
AnimationFrame = 0;
AnimationTime = -AnimationTimeLeft;
AnimationFrame -= 1;
// Animation finished, do we cycle back to positive or reset.
if (AnimationFrame < 0)
{
if (Cycle)
{
AnimationFrame = 1;
Reversed = false;
}
else
{
AnimationFrame = delayCount - 1;
}
AnimationTime = -AnimationTimeLeft;
}
}
else
{
AnimationFrame += 1;
// Animation finished, do we reverse or reset.
if (AnimationFrame >= delayCount)
{
if (Cycle)
{
AnimationFrame = delayCount - 2;
Reversed = true;
}
else
{
AnimationFrame = 0;
}
AnimationTime = -AnimationTimeLeft;
}
}
AnimationTimeLeft += state.GetDelay(AnimationFrame);

View File

@@ -1,626 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
[UsedImplicitly]
public sealed class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly SharedPhysicsSystem _broadPhaseSystem = default!;
[Dependency] private readonly IClydeAudio _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly List<PlayingStream> _playingClydeStreams = new();
private ISawmill _sawmill = default!;
private float _maxRayLength;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<PlayAudioEntityMessage>(PlayAudioEntityHandler);
SubscribeNetworkEvent<PlayAudioGlobalMessage>(PlayAudioGlobalHandler);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(PlayAudioPositionalHandler);
SubscribeNetworkEvent<StopAudioMessageClient>(StopAudioMessageHandler);
_sawmill = _logManager.GetSawmill("audio");
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
}
public override void Shutdown()
{
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
foreach (var stream in _playingClydeStreams)
{
stream.Source.Dispose();
}
_playingClydeStreams.Clear();
base.Shutdown();
}
private void OnRaycastLengthChanged(float value)
{
_maxRayLength = value;
}
#region Event Handlers
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
{
var uid = GetEntity(ev.NetEntity);
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = EntityManager.EntityExists(uid)
? (PlayingStream?) Play(ev.FileName, uid, fallback, ev.AudioParams, false)
: (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void PlayAudioGlobalHandler(PlayAudioGlobalMessage ev)
{
var stream = (PlayingStream?) Play(ev.FileName, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
{
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void StopAudioMessageHandler(StopAudioMessageClient ev)
{
var stream = _playingClydeStreams.Find(p => p.NetIdentifier == ev.Identifier);
if (stream == null)
return;
stream.Done = true;
stream.Source.Dispose();
_playingClydeStreams.Remove(stream);
}
#endregion
public override void FrameUpdate(float frameTime)
{
var xforms = GetEntityQuery<TransformComponent>();
var physics = GetEntityQuery<PhysicsComponent>();
var ourPos = _eyeManager.CurrentEye.Position;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
try
{
Parallel.ForEach(_playingClydeStreams, opts, (stream) => ProcessStream(stream, ourPos, xforms, physics));
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while processing entity streams.");
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
}
finally
{
for (var i = _playingClydeStreams.Count - 1; i >= 0; i--)
{
var stream = _playingClydeStreams[i];
if (stream.Done)
{
stream.Source.Dispose();
_playingClydeStreams.RemoveSwap(i);
}
}
}
}
private void ProcessStream(PlayingStream stream,
MapCoordinates listener,
EntityQuery<TransformComponent> xforms,
EntityQuery<PhysicsComponent> physics)
{
if (!stream.Source.IsPlaying)
{
stream.Done = true;
return;
}
if (stream.Source.IsGlobal)
{
DebugTools.Assert(stream.TrackingCoordinates == null
&& stream.TrackingEntity == null
&& stream.TrackingFallbackCoordinates == null);
return;
}
DebugTools.Assert(stream.TrackingCoordinates != null
|| stream.TrackingEntity != null
|| stream.TrackingFallbackCoordinates != null);
// Get audio Position
if (!TryGetStreamPosition(stream, xforms, out var mapPos)
|| mapPos == MapCoordinates.Nullspace
|| mapPos.Value.MapId != listener.MapId)
{
stream.Done = true;
return;
}
// Max distance check
var delta = mapPos.Value.Position - listener.Position;
var distance = delta.Length();
if (distance > stream.MaxDistance)
{
stream.Source.SetVolumeDirect(0);
return;
}
// Update audio occlusion
float occlusion = 0;
if (distance > 0.1)
{
var rayLength = MathF.Min(distance, _maxRayLength);
var ray = new CollisionRay(listener.Position, delta/distance, OcclusionCollisionMask);
occlusion = _broadPhaseSystem.IntersectRayPenetration(listener.MapId, ray, rayLength, stream.TrackingEntity);
}
stream.Source.SetOcclusion(occlusion);
// Update attenuation dependent volume.
UpdatePositionalVolume(stream, distance);
// Update audio positions.
var audioPos = stream.Attenuation != Attenuation.NoAttenuation ? mapPos.Value : listener;
if (!stream.Source.SetPosition(audioPos.Position))
{
_sawmill.Warning("Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
return;
}
// Make race cars go NYYEEOOOOOMMMMM
if (stream.TrackingEntity != null && physics.TryGetComponent(stream.TrackingEntity, 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(stream.TrackingEntity.Value, physicsComp, null, xforms, physics);
stream.Source.SetVelocity(velocity);
}
}
private void UpdatePositionalVolume(PlayingStream stream, float distance)
{
// OpenAL also limits the distance to <= AL_MAX_DISTANCE, but since we cull
// sources that are further away than stream.MaxDistance, we don't do that.
distance = MathF.Max(stream.ReferenceDistance, distance);
float gain;
// Technically these are formulas for gain not decibels but EHHHHHHHH.
switch (stream.Attenuation)
{
case Attenuation.Default:
gain = 1f;
break;
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
case Attenuation.InverseDistanceClamped:
case Attenuation.InverseDistance:
gain = stream.ReferenceDistance
/ (stream.ReferenceDistance
+ stream.RolloffFactor * (distance - stream.ReferenceDistance));
break;
case Attenuation.LinearDistanceClamped:
case Attenuation.LinearDistance:
gain = 1f
- stream.RolloffFactor
* (distance - stream.ReferenceDistance)
/ (stream.MaxDistance - stream.ReferenceDistance);
break;
case Attenuation.ExponentDistanceClamped:
case Attenuation.ExponentDistance:
gain = MathF.Pow(distance / stream.ReferenceDistance, -stream.RolloffFactor);
break;
default:
throw new ArgumentOutOfRangeException(
$"No implemented attenuation for {stream.Attenuation}");
}
var volume = MathF.Pow(10, stream.Volume / 10);
var actualGain = MathF.Max(0f, volume * gain);
stream.Source.SetVolumeDirect(actualGain);
}
private bool TryGetStreamPosition(PlayingStream stream, EntityQuery<TransformComponent> xformQuery, [NotNullWhen(true)] out MapCoordinates? mapPos)
{
if (stream.TrackingCoordinates != null)
{
mapPos = stream.TrackingCoordinates.Value.ToMap(EntityManager);
if (mapPos != MapCoordinates.Nullspace)
return true;
}
if (xformQuery.TryGetComponent(stream.TrackingEntity, out var xform)
&& xform.MapID != MapId.Nullspace)
{
mapPos = new MapCoordinates(_xformSys.GetWorldPosition(xform, xformQuery), xform.MapID);
return true;
}
if (stream.TrackingFallbackCoordinates != null)
{
mapPos = stream.TrackingFallbackCoordinates.Value.ToMap(EntityManager);
return mapPos != MapCoordinates.Nullspace;
}
mapPos = MapCoordinates.Nullspace;
return false;
}
#region Play AudioStream
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResPath(filename), out audio))
return true;
_sawmill.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryCreateAudioSource(AudioStream stream, [NotNullWhen(true)] out IClydeAudioSource? source)
{
if (!_timing.IsFirstTimePredicted)
{
source = null;
_sawmill.Error($"Tried to create audio source outside of prediction!");
DebugTools.Assert(false);
return false;
}
source = _clyde.CreateAudioSource(stream);
return source != null;
}
private PlayingStream CreateAndStartPlayingStream(IClydeAudioSource source, AudioParams? audioParams, AudioStream stream)
{
ApplyAudioParams(audioParams, source, stream);
source.StartPlaying();
var playing = new PlayingStream
{
Source = source,
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
RolloffFactor = audioParams?.RolloffFactor ?? 1f,
Volume = audioParams?.Volume ?? 0
};
_playingClydeStreams.Add(playing);
return playing;
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, audioParams) : default;
}
/// <summary>
/// Play an audio stream globally, without position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up global audio for {stream.Name}: {0}", Environment.StackTrace);
return null;
}
source.SetGlobal();
return CreateAndStartPlayingStream(source, audioParams, stream);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, EntityUid entity, EntityCoordinates? fallbackCoordinates,
AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
NetEntity = GetNetEntity(entity),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates) ?? default,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, entity, fallbackCoordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream following an entity.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityUid entity, EntityCoordinates? fallbackCoordinates = null,
AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up entity audio for {stream.Name} / {ToPrettyString(entity)}: {0}", Environment.StackTrace);
return null;
}
var query = GetEntityQuery<TransformComponent>();
var xform = query.GetComponent(entity);
var worldPos = _xformSys.GetWorldPosition(xform, query);
fallbackCoordinates ??= GetFallbackCoordinates(new MapCoordinates(worldPos, xform.MapID));
if (!source.SetPosition(worldPos))
return Play(stream, fallbackCoordinates.Value, fallbackCoordinates.Value, audioParams);
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
playing.TrackingEntity = entity;
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
return playing;
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <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="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates,
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(coordinates),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, coordinates, fallbackCoordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream at a static position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up coordinates audio for {stream.Name} / {coordinates}: {0}", Environment.StackTrace);
return null;
}
if (!source.SetPosition(fallbackCoordinates.Position))
{
source.Dispose();
_sawmill.Warning($"Can't play positional audio \"{stream.Name}\", can't set position.");
return null;
}
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
playing.TrackingCoordinates = coordinates;
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
return playing;
}
#endregion
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user,
AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted || sound == null)
return Play(sound, Filter.Local(), source, false, audioParams);
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user,
AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted || sound == null)
return Play(sound, Filter.Local(), coordinates, false, audioParams);
return null;
}
private void ApplyAudioParams(AudioParams? audioParams, IClydeAudioSource source, AudioStream audio)
{
if (!audioParams.HasValue)
return;
if (audioParams.Value.Variation.HasValue)
source.SetPitch(audioParams.Value.PitchScale
* (float) RandMan.NextGaussian(1, audioParams.Value.Variation.Value));
else
source.SetPitch(audioParams.Value.PitchScale);
source.SetVolume(audioParams.Value.Volume);
source.SetRolloffFactor(audioParams.Value.RolloffFactor);
source.SetMaxDistance(audioParams.Value.MaxDistance);
source.SetReferenceDistance(audioParams.Value.ReferenceDistance);
source.IsLooping = audioParams.Value.Loop;
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioParams.Value.PlayOffsetSeconds;
offset = Math.Clamp(offset, 0f, (float) audio.Length.TotalSeconds);
source.SetPlaybackPosition(offset);
}
public sealed class PlayingStream : IPlayingAudioStream
{
public uint? NetIdentifier;
public IClydeAudioSource Source = default!;
public EntityUid? TrackingEntity;
public EntityCoordinates? TrackingCoordinates;
public EntityCoordinates? TrackingFallbackCoordinates;
public bool Done;
public float Volume
{
get => _volume;
set
{
_volume = value;
Source.SetVolume(value);
}
}
private float _volume;
public float MaxDistance;
public float ReferenceDistance;
public float RolloffFactor;
public Attenuation Attenuation
{
get => _attenuation;
set
{
if (value == _attenuation) return;
_attenuation = value;
if (_attenuation != Attenuation.Default)
{
// Need to disable default attenuation when using a custom one
// Damn Sloth wanting linear ambience sounds so they smoothly cut-off and are short-range
Source.SetRolloffFactor(0f);
}
}
}
private Attenuation _attenuation = Attenuation.Default;
public void Stop()
{
Source.StopPlaying();
}
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, entity, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return Play(filename, uid, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return Play(filename, uid, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
}

View File

@@ -1,14 +1,11 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Enumerators;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using static Robust.Shared.GameObjects.OccluderComponent;
namespace Robust.Client.GameObjects;
@@ -30,17 +27,15 @@ internal sealed class ClientOccluderSystem : OccluderSystem
base.Initialize();
SubscribeLocalEvent<OccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<OccluderComponent, ReAnchorEvent>(OnReAnchor);
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(OnShutdown);
}
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
{
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
return;
comp.Enabled = enabled;
Dirty(uid, comp);
base.SetEnabled(uid, enabled, comp, meta);
var xform = Transform(uid);
QueueTreeUpdate(uid, comp, xform);
@@ -94,11 +89,6 @@ internal sealed class ClientOccluderSystem : OccluderSystem
AnchorStateChanged(uid, comp, args.Transform);
}
private void OnReAnchor(EntityUid uid, OccluderComponent comp, ref ReAnchorEvent args)
{
AnchorStateChanged(uid, comp, args.Xform);
}
private void QueueOccludedDirectionUpdate(EntityUid sender, OccluderComponent occluder, TransformComponent? xform = null)
{
if (!Resolve(sender, ref xform))
@@ -178,8 +168,9 @@ internal sealed class ClientOccluderSystem : OccluderSystem
var tile = grid.TileIndicesFor(xform.Coordinates);
DebugTools.Assert(occluder.LastPosition == null
|| occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
// TODO: Sub to parent changes instead or something.
// DebugTools.Assert(occluder.LastPosition == null
// || occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
occluder.LastPosition = (xform.GridUid.Value, tile);
// dir starts at the relative effective south direction;

View File

@@ -54,12 +54,24 @@ namespace Robust.Client.GameObjects
DebugTools.Assert(ExpectedEntities.TryGetValue(netEntity, out var expectedContainer) && expectedContainer == cont && cont.ExpectedEntities.Contains(netEntity));
}
private void HandleEntityInitialized(EntityUid uid)
private void HandleEntityInitialized(Entity<MetaDataComponent> ent)
{
if (!RemoveExpectedEntity(GetNetEntity(uid), out var container))
var (uid, meta) = ent;
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
return;
container.Insert(uid, EntityManager, transform: TransformQuery.GetComponent(uid), meta: MetaQuery.GetComponent(uid));
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
}
public override void ShutdownContainer(BaseContainer container)
{
foreach (var ent in container.ExpectedEntities)
{
if (ExpectedEntities.Remove(ent, out var c))
DebugTools.Assert(c == container);
}
base.ShutdownContainer(container);
}
private void HandleComponentState(EntityUid uid, ContainerManagerComponent component, ref ComponentHandleState args)
@@ -81,17 +93,17 @@ namespace Robust.Client.GameObjects
foreach (var entity in container.ContainedEntities.ToArray())
{
container.Remove(entity,
EntityManager,
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
container,
force: true,
reparent: false);
reparent: false
);
DebugTools.Assert(!container.Contains(entity));
}
container.Shutdown(EntityManager, _netMan);
ShutdownContainer(container);
toDelete.Add(id);
}
@@ -108,7 +120,7 @@ namespace Robust.Client.GameObjects
{
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
container.Init(id, uid, component);
InitContainer(container, (uid, component), id);
component.Containers.Add(id, container);
}
@@ -132,13 +144,12 @@ namespace Robust.Client.GameObjects
foreach (var entity in toRemove)
{
container.Remove(
entity,
EntityManager,
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
container,
force: true,
reparent: false);
reparent: false
);
DebugTools.Assert(!container.Contains(entity));
}
@@ -188,11 +199,12 @@ namespace Robust.Client.GameObjects
continue;
RemoveExpectedEntity(netEnt, out _);
container.Insert(entity, EntityManager,
TransformQuery.GetComponent(entity),
Insert(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity), null),
container,
xform,
MetaQuery.GetComponent(entity),
force: true);
force: true
);
DebugTools.Assert(container.Contains(entity));
}
@@ -222,7 +234,7 @@ namespace Robust.Client.GameObjects
return;
}
container.Insert(message.Entity, EntityManager);
Insert(message.Entity, container);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
@@ -324,32 +336,30 @@ namespace Robust.Client.GameObjects
if (_pointLightQuery.TryGetComponent(entity, out var light))
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
var childEnumerator = xform.ChildEnumerator;
// Try to avoid TryComp if we already know stuff is occluded.
if ((!spriteOccluded || !lightOccluded) && TryComp<ContainerManagerComponent>(entity, out var manager))
{
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
// Thank god it's by value and not by ref.
var childSpriteOccluded = spriteOccluded;
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child.Value, out var container))
if (manager.TryGetContainer(child, out var container))
{
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;
}
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), childSpriteOccluded, childLightOccluded);
UpdateEntity(child, TransformQuery.GetComponent(child), childSpriteOccluded, childLightOccluded);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), spriteOccluded, lightOccluded);
UpdateEntity(child, TransformQuery.GetComponent(child), spriteOccluded, lightOccluded);
}
}
}

View File

@@ -1,9 +1,8 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.GameObjects;
@@ -26,17 +25,13 @@ public sealed class EyeSystem : SharedEyeSystem
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateEye(component);
UpdateEye((uid, component));
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
{
_eyeManager.CurrentEye = component._eye;
}
UpdateEye((uid, component));
_eyeManager.CurrentEye = component.Eye;
var ev = new EyeAttachedEvent(uid, component);
RaiseLocalEvent(uid, ref ev, true);
}
@@ -48,13 +43,7 @@ public sealed class EyeSystem : SharedEyeSystem
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
{
component._eye = new Eye
{
Position = Transform(uid).MapPosition,
Zoom = component.Zoom,
DrawFov = component.DrawFov,
Rotation = component.Rotation,
};
UpdateEye((uid, component));
}
/// <inheritdoc />
@@ -64,7 +53,7 @@ public sealed class EyeSystem : SharedEyeSystem
while (query.MoveNext(out var uid, out var eyeComponent))
{
if (eyeComponent._eye == null)
if (eyeComponent.Eye == null)
continue;
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
@@ -73,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
eyeComponent.Target = null;
}
eyeComponent._eye.Position = xform.MapPosition;
eyeComponent.Eye.Position = xform.MapPosition;
}
}
}

View File

@@ -105,12 +105,10 @@ namespace Robust.Client.GameObjects
/// <param name="inputCmd">Input command to handle as predicted.</param>
public void PredictInputCommand(IFullInputCmdMessage inputCmd)
{
DebugTools.AssertNotNull(_playerManager.LocalPlayer);
var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId);
Predicted = true;
var session = _playerManager.LocalPlayer!.Session;
var session = _playerManager.LocalSession;
foreach (var handler in BindRegistry.GetHandlers(keyFunc))
{
if (handler.HandleCmdMessage(EntityManager, session, inputCmd))
@@ -145,27 +143,22 @@ namespace Robust.Client.GameObjects
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
{
var localPlayer = _playerManager.LocalPlayer;
if(localPlayer is null)
return;
var pent = localPlayer.ControlledEntity;
if(pent is null)
if (_playerManager.LocalEntity is not { } pent)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
var pxform = Transform(pent.Value);
var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
}
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
@@ -208,11 +201,8 @@ namespace Robust.Client.GameObjects
/// </summary>
public void SetEntityContextActive()
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
{
if (_playerManager.LocalEntity is not { } controlled)
return;
}
SetEntityContextActive(_inputManager, controlled);
}

View File

@@ -8,30 +8,28 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Dynamics;
namespace Robust.Client.GameObjects
namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
public sealed class MapSystem : SharedMapSystem
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
public override void Initialize()
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
}
public override void Initialize()
{
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _mapManager, _resource, _tileDefinitionManager));
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
}

View File

@@ -1,3 +1,4 @@
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -8,6 +9,13 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IMidiManager _midiManager = default!;
public override void Initialize()
{
base.Initialize();
// AudioSystem sets eye position and rotation so rely on those.
UpdatesAfter.Add(typeof(AudioSystem));
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);

View File

@@ -68,6 +68,10 @@ namespace Robust.Client.GameObjects
return RemCompDeferred<PointLightComponent>(uid);
}
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
{
}
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
{
SetMask(component.MaskPath, component);
@@ -95,28 +99,23 @@ namespace Robust.Client.GameObjects
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || enabled == comp.Enabled || comp is not PointLightComponent clientComp)
return;
comp.Enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(uid, comp);
base.SetEnabled(uid, enabled, comp, meta);
if (!comp.ContainerOccluded)
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
{
if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius) ||
comp is not PointLightComponent clientComp)
return;
comp.Radius = radius;
Dirty(uid, comp);
base.SetRadius(uid, radius, comp, meta);
if (clientComp.TreeUid != null)
_lightTree.QueueTreeUpdate(uid, clientComp);
}

View File

@@ -122,14 +122,13 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
// Check if any EntityPrototype has been changed.
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var (prototype, _) in changedSet.Modified)
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);

View File

@@ -58,11 +58,11 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(SpriteTreeSystem));
_proto.PrototypesReloaded += OnPrototypesReloaded;
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
}
@@ -72,13 +72,6 @@ namespace Robust.Client.GameObjects
QueueUpdateInert(uid, component);
}
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
}
private void OnBiasChanged(double value)
{
SpriteComponent.DirectionBias = value;

View File

@@ -54,9 +54,10 @@ namespace Robust.Client.GameObjects
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the final position.
// the entity in the state's final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
// was predicted. I.e., we assume the entity was already rendered in the final position that was
// previously predicted.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.

View File

@@ -29,10 +29,7 @@ namespace Robust.Client.GameObjects
var uiKey = ev.UiKey;
var message = ev.Message;
// This should probably not happen at this point, but better make extra sure!
if (_playerManager.LocalPlayer != null)
message.Session = _playerManager.LocalPlayer.Session;
message.Session = _playerManager.LocalSession!;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
@@ -75,8 +72,7 @@ namespace Robust.Client.GameObjects
boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if (playerSession != null)
if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
return;
}
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{

View File

@@ -43,7 +43,7 @@ public sealed class ClientDirtySystem : EntitySystem
return;
// Client-side entity deletion is not supported and will cause errors.
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity)}. Trace: {Environment.StackTrace}");
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity.Owner, ev.Entity.Comp)}. Trace: {Environment.StackTrace}");
}
private void OnCompRemoved(RemovedComponentEventArgs args)
@@ -71,9 +71,9 @@ public sealed class ClientDirtySystem : EntitySystem
RemovedComponents.Clear();
}
private void OnEntityDirty(EntityUid e)
private void OnEntityDirty(Entity<MetaDataComponent> e)
{
if (_timing.InPrediction && !IsClientSide(e))
if (_timing.InPrediction && !IsClientSide(e, e))
DirtyEntities.Add(e);
}
}

View File

@@ -49,16 +49,16 @@ namespace Robust.Client.GameStates
// Game state dictionaries that get used every tick.
private readonly Dictionary<EntityUid, (NetEntity NetEntity, MetaDataComponent Meta, bool EnteringPvs, GameTick LastApplied, EntityState? curState, EntityState? nextState)> _toApply = new();
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
private readonly Dictionary<ushort, (IComponent Component, ComponentState? curState, ComponentState? nextState)> _compStateWork = new();
private readonly Dictionary<ushort, (IComponent Component, IComponentState? curState, IComponentState? nextState)> _compStateWork = new();
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
private readonly List<EntityUid> _toDelete = new();
private readonly List<IComponent> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
private readonly ObjectPool<Dictionary<ushort, ComponentState>> _compDataPool =
new DefaultObjectPool<Dictionary<ushort, ComponentState>>(new DictPolicy<ushort, ComponentState>(), 256);
private readonly ObjectPool<Dictionary<ushort, IComponentState>> _compDataPool =
new DefaultObjectPool<Dictionary<ushort, IComponentState>>(new DictPolicy<ushort, IComponentState>(), 256);
private uint _metaCompNetId;
@@ -124,6 +124,8 @@ namespace Robust.Client.GameStates
public bool DropStates;
#endif
private bool _resettingPredictedEntities;
/// <inheritdoc />
public void Initialize()
{
@@ -146,6 +148,7 @@ namespace Robust.Client.GameStates
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
_config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
@@ -160,6 +163,8 @@ namespace Robust.Client.GameStates
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
_entities.ComponentAdded += OnComponentAdded;
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
if (!metaId.HasValue)
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
@@ -167,6 +172,25 @@ namespace Robust.Client.GameStates
_metaCompNetId = metaId.Value;
}
private void OnComponentAdded(AddedComponentEventArgs args)
{
if (!_resettingPredictedEntities)
return;
var comp = args.ComponentType;
if (comp.NetID == null)
return;
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
return;
_sawmill.Error($"""
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
/// <inheritdoc />
public void Reset()
{
@@ -208,9 +232,9 @@ namespace Robust.Client.GameStates
return default;
}
DebugTools.AssertNotNull(_players.LocalPlayer);
DebugTools.Assert(_players.LocalSession != null);
var evArgs = new EntitySessionEventArgs(_players.LocalPlayer!.Session);
var evArgs = new EntitySessionEventArgs(_players.LocalSession);
_pendingSystemMessages.Enqueue((_nextInputCmdSeq, _timing.CurTick, message,
new EntitySessionMessage<T>(evArgs, message)));
@@ -233,7 +257,7 @@ namespace Robust.Client.GameStates
public void UpdateFullRep(GameState state, bool cloneDelta = false)
=> _processor.UpdateFullRep(state, cloneDelta);
public Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep()
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
=> _processor.GetFullRep();
private void HandlePvsLeaveMessage(MsgStateLeavePvs message)
@@ -319,7 +343,7 @@ namespace Robust.Client.GameStates
}
// If we were waiting for a new state, we are now applying it.
if (_processor.WaitingForFull)
if (curState.FromSequence == GameTick.Zero)
{
_processor.OnFullStateReceived();
_timing.LastProcessedTick = curState.ToSequence;
@@ -327,7 +351,10 @@ namespace Robust.Client.GameStates
PartialStateReset(curState, true);
}
else
{
DebugTools.Assert(!_processor.WaitingForFull);
_timing.LastProcessedTick += 1;
}
_timing.CurTick = _timing.LastRealTick = _timing.LastProcessedTick;
@@ -508,7 +535,6 @@ namespace Robust.Client.GameStates
using var __ = _timing.StartStateApplicationArea();
// This is terrible, and I hate it. This also needs to run even when prediction is disabled.
_entitySystemManager.GetEntitySystem<SharedGridTraversalSystem>().QueuedEvents.Clear();
_entitySystemManager.GetEntitySystem<TransformSystem>().Reset();
if (!PredictionNeedsResetting)
@@ -536,40 +562,50 @@ namespace Robust.Client.GameStates
countReset += 1;
foreach (var (netId, comp) in meta.NetComponents)
try
{
if (!comp.NetSyncEnabled)
continue;
_resettingPredictedEntities = true;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
foreach (var (netId, comp) in meta.NetComponents)
{
if (last.ContainsKey(netId))
if (!comp.NetSyncEnabled)
continue;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
if (last.ContainsKey(netId))
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
}
else
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
}
else
if (comp.LastModifiedTick <= _timing.LastRealTick ||
!last.TryGetValue(netId, out var compState))
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
{
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
finally
{
_resettingPredictedEntities = false;
}
// Remove predicted component additions
@@ -631,14 +667,13 @@ namespace Robust.Client.GameStates
foreach (var netEntity in createdEntities)
{
var (createdEntity, meta) = _entityManager.GetEntityData(netEntity);
var (_, meta) = _entityManager.GetEntityData(netEntity);
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
foreach (var (netId, component) in meta.NetComponents)
{
if (!component.NetSyncEnabled)
continue;
DebugTools.Assert(component.NetSyncEnabled);
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
@@ -744,15 +779,13 @@ namespace Robust.Client.GameStates
newMeta.LastStateApplied = curState.ToSequence;
// Check if there's any component states awaiting this entity.
if (_entityManager.PendingNetEntityStates.TryGetValue(es.NetEntity, out var value))
if (_entityManager.PendingNetEntityStates.Remove(es.NetEntity, out var value))
{
foreach (var (type, owner) in value)
{
var pending = _pendingReapplyNetStates.GetOrNew(owner);
pending.Add(type);
}
_entityManager.PendingNetEntityStates.Remove(es.NetEntity);
}
}
@@ -816,6 +849,17 @@ namespace Robust.Client.GameStates
if (!metas.TryGetComponent(uid, out var meta))
continue;
// It may also have been queued for deletion, in which case its last server state entry has already been removed.
// I love me some spaghetti order-of-operation dependent code
if (!_processor._lastStateFullRep.ContainsKey(meta.NetEntity))
{
DebugTools.Assert(curState.EntityDeletions.Value.Contains(meta.NetEntity));
continue;
}
DebugTools.Assert(!curState.EntityDeletions.Value.Contains(meta.NetEntity));
// State already being re-applied so don't bulldoze it.
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid, out var exists);
@@ -949,16 +993,15 @@ namespace Robust.Client.GameStates
xformSys.DetachParentToNull(ent, xform);
// Then detach all children.
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
if (deleteClientChildren
&& !deleteClientEntities // don't add duplicates
&& _entities.IsClientSide(child.Value))
&& _entities.IsClientSide(child))
{
_toDelete.Add(child.Value);
_toDelete.Add(child);
}
}
@@ -1006,7 +1049,7 @@ namespace Robust.Client.GameStates
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
}
// Finally, delete the entity.
@@ -1097,7 +1140,7 @@ namespace Robust.Client.GameStates
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
{
container.Remove(ent.Value, _entities, xform, meta, false, true);
containerSys.Remove((ent.Value, xform, meta), container, false, true);
}
meta._flags |= MetaDataFlags.Detached;
@@ -1185,7 +1228,9 @@ namespace Robust.Client.GameStates
foreach (var (id, comp) in meta.NetComponents)
{
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
DebugTools.Assert(comp.NetSyncEnabled);
if (!curState.NetComponents.Contains(id))
_toRemove.Add(comp);
}
@@ -1395,7 +1440,7 @@ namespace Robust.Client.GameStates
void _recursiveRemoveState(NetEntity netEntity, TransformComponent xform, EntityQuery<MetaDataComponent> metaQuery, EntityQuery<TransformComponent> xformQuery)
{
_processor._lastStateFullRep.Remove(netEntity);
foreach (var child in xform.ChildEntities)
foreach (var child in xform._children)
{
if (xformQuery.TryGetComponent(child, out var childXform) &&
metaQuery.TryGetComponent(child, out var childMeta))

View File

@@ -14,8 +14,6 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor
{
public const int MaxBufferSize = 512;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
@@ -28,11 +26,13 @@ namespace Robust.Client.GameStates
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
private int _maxBufferSize = 512;
public const int MinimumMaxBufferSize = 256;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
/// </summary>
internal readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _lastStateFullRep
internal readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _lastStateFullRep
= new();
/// <inheritdoc />
@@ -48,7 +48,14 @@ namespace Robust.Client.GameStates
public int BufferSize
{
get => _bufferSize;
set => _bufferSize = value < 0 ? 0 : value;
set => _bufferSize = Math.Max(value, 0);
}
public int MaxBufferSize
{
get => _maxBufferSize;
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
}
/// <inheritdoc />
@@ -100,21 +107,21 @@ namespace Robust.Client.GameStates
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
if (LastFullState == null && state.FromSequence == GameTick.Zero)
{
LastFullState = state;
if (Logging)
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
return true;
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
}
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
{
if (Logging)
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
return false;
}
@@ -205,7 +212,7 @@ Had full state: {LastFullState != null}"
{
if (!_lastStateFullRep.TryGetValue(entityState.NetEntity, out var compData))
{
compData = new Dictionary<ushort, ComponentState>();
compData = new Dictionary<ushort, IComponentState>();
_lastStateFullRep.Add(entityState.NetEntity, compData);
}
@@ -384,7 +391,7 @@ Had full state: {LastFullState != null}"
LastFullStateRequested = null;
}
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> implicitData)
{
foreach (var (netEntity, implicitEntState) in implicitData)
{
@@ -418,18 +425,18 @@ Had full state: {LastFullState != null}"
}
}
public Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity netEntity)
public Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity netEntity)
{
return _lastStateFullRep[netEntity];
}
public Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep()
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
{
return _lastStateFullRep;
}
public bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary)
{
return _lastStateFullRep.TryGetValue(entity, out dictionary);
}

View File

@@ -113,7 +113,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Returns the full collection of cached game states that are used to reset predicted entities.
/// </summary>
Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep();
Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep();
/// <summary>
/// This will perform some setup in order to reset the game to an earlier state. To fully reset the state

View File

@@ -83,13 +83,13 @@ namespace Robust.Client.GameStates
/// The data to merge.
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
/// </param>
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> data);
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> data);
/// <summary>
/// Get the last state data from the server for an entity.
/// </summary>
/// <returns>Dictionary (net ID -> ComponentState)</returns>
Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity entity);
Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity entity);
/// <summary>
/// Calculate the number of applicable states in the game state buffer from a given tick.
@@ -99,6 +99,6 @@ namespace Robust.Client.GameStates
int GetApplicableStateCount(GameTick? fromTick);
bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary);
}
}

View File

@@ -313,7 +313,7 @@ namespace Robust.Client.GameStates
if (args.Length == 0)
{
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
entity = _playerManager.LocalEntity ?? EntityUid.Invalid;
}
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
{

View File

@@ -1,59 +0,0 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
private void _flushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
private void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}
}

View File

@@ -1,680 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Vector2 = System.Numerics.Vector2;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private sealed class AudioSource : IClydeAudioSource
{
private int SourceHandle;
private readonly ClydeAudio _master;
private readonly AudioStream _sourceStream;
private int FilterHandle;
#if DEBUG
private bool _didPositionWarning;
#endif
private float _gain;
private bool IsEfxSupported => _master.IsEfxSupported;
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
{
_master = master;
SourceHandle = sourceHandle;
_sourceStream = sourceStream;
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
AL.SourcePlay(SourceHandle);
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
AL.SourceStop(SourceHandle);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret);
_master._checkAlError();
return ret;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.Looping, value);
_master._checkAlError();
}
}
public bool IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public void SetGlobal()
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
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);
// 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.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~AudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
_master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
_master._audioSources.Remove(SourceHandle);
_master._checkAlError();
}
SourceHandle = -1;
}
private bool _isDisposed()
{
return SourceHandle == -1;
}
private void _checkDisposed()
{
if (SourceHandle == -1)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
}
private sealed class BufferedAudioSource : IClydeBufferedAudioSource
{
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly ClydeAudio _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
{
_master = master;
SourceHandle = sourceHandle;
BufferHandles = bufferHandles;
for (int i = 0; i < BufferHandles.Length; i++)
{
var bufferHandle = BufferHandles[i];
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
// ReSharper disable once PossibleInvalidOperationException
AL.SourceStop(SourceHandle!.Value);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
var state = AL.GetSourceState(SourceHandle!.Value);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public void SetGlobal()
{
_checkDisposed();
_mono = false;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetLooping()
{
// TODO?waaaaddDDDDD
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1.5f);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, gain * _gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle!.Value, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle!.Value, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
_mono = true;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~BufferedAudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (SourceHandle == null) return;
if (!_master.IsMainThread())
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
for (var i = 0; i < BufferHandles.Length; i++)
_master.DeleteAudioBufferOnMainThread(BufferHandles[i]);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle.Value);
AL.DeleteBuffers(BufferHandles);
_master._bufferedAudioSources.Remove(SourceHandle.Value);
_master._checkAlError();
}
SourceHandle = null;
}
private bool _isDisposed()
{
return SourceHandle == null;
}
private void _checkDisposed()
{
if (SourceHandle == null)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
public int GetNumberOfBuffersProcessed()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
return buffersProcessed;
}
public unsafe void GetBuffersProcessed(Span<int> handles)
{
_checkDisposed();
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
fixed (int* ptr = handles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
for (var i = 0; i < entries; i++)
handles[i] = BufferMap[handles[i]];
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
_checkDisposed();
if(_float)
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (ushort* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
_checkDisposed();
if(!_float)
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (float* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
}
}
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
{
_checkDisposed();
Span<int> realHandles = stackalloc int[handles.Length];
handles.CopyTo(realHandles);
for (var i = 0; i < realHandles.Length; i++)
{
var handle = realHandles[i];
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
realHandles[i] = BufferHandles[handle];
}
fixed (int* ptr = realHandles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
}
public unsafe void EmptyBuffers()
{
_checkDisposed();
var length = (SampleRate / BufferHandles.Length) * (_mono ? 1 : 2);
Span<int> handles = stackalloc int[BufferHandles.Length];
if (_float)
{
var empty = new float[length];
var span = (Span<float>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
else
{
var empty = new ushort[length];
var span = (Span<ushort>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
QueueBuffers(handles);
}
}
}
}

View File

@@ -1,54 +0,0 @@
using System;
using System.IO;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private OggVorbisData _readOggVorbis(Stream stream)
{
using (var vorbis = new NVorbis.VorbisReader(stream, false))
{
var sampleRate = vorbis.SampleRate;
var channels = vorbis.Channels;
var totalSamples = vorbis.TotalSamples;
var readSamples = 0;
var buffer = new float[totalSamples * channels];
while (readSamples < totalSamples)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
if (read == 0)
{
break;
}
readSamples += read;
}
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
}
}
private readonly struct OggVorbisData
{
public readonly long TotalSamples;
public readonly long SampleRate;
public readonly long Channels;
public readonly ReadOnlyMemory<float> Data;
public readonly string Title;
public readonly string Artist;
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
{
TotalSamples = totalSamples;
SampleRate = sampleRate;
Channels = channels;
Data = data;
Title = title;
Artist = artist;
}
}
}
}

View File

@@ -1,144 +0,0 @@
using System;
using System.IO;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
/// <summary>
/// Load up a WAVE file.
/// </summary>
private static WavData _readWav(Stream stream)
{
var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
void SkipChunk()
{
var length = reader.ReadUInt32();
stream.Position += length;
}
// Read outer most chunks.
Span<byte> fourCc = stackalloc byte[4];
while (true)
{
_readFourCC(reader, fourCc);
if (!fourCc.SequenceEqual("RIFF"u8))
{
SkipChunk();
continue;
}
return _readRiffChunk(reader);
}
}
private static void _skipChunk(BinaryReader reader)
{
var length = reader.ReadUInt32();
reader.BaseStream.Position += length;
}
private static void _readFourCC(BinaryReader reader, Span<byte> fourCc)
{
fourCc[0] = reader.ReadByte();
fourCc[1] = reader.ReadByte();
fourCc[2] = reader.ReadByte();
fourCc[3] = reader.ReadByte();
}
private static WavData _readRiffChunk(BinaryReader reader)
{
Span<byte> format = stackalloc byte[4];
reader.ReadUInt32();
_readFourCC(reader, format);
if (!format.SequenceEqual("WAVE"u8))
{
throw new InvalidDataException("File is not a WAVE file.");
}
_readFourCC(reader, format);
if (!format.SequenceEqual("fmt "u8))
{
throw new InvalidDataException("Expected fmt chunk.");
}
// Read fmt chunk.
var size = reader.ReadInt32();
var afterFmtPos = reader.BaseStream.Position + size;
var audioType = (WavAudioFormatType) reader.ReadInt16();
var channels = reader.ReadInt16();
var sampleRate = reader.ReadInt32();
var byteRate = reader.ReadInt32();
var blockAlign = reader.ReadInt16();
var bitsPerSample = reader.ReadInt16();
if (audioType != WavAudioFormatType.PCM)
{
throw new NotImplementedException("Unable to support audio types other than PCM.");
}
DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
// Fmt is not of guaranteed size, so use the size header to skip to the end.
reader.BaseStream.Position = afterFmtPos;
while (true)
{
_readFourCC(reader, format);
if (!format.SequenceEqual("data"u8))
{
_skipChunk(reader);
continue;
}
break;
}
// We are in the data chunk.
size = reader.ReadInt32();
var data = reader.ReadBytes(size);
return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
}
/// <summary>
/// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
/// </summary>
[PublicAPI]
private readonly struct WavData
{
public readonly WavAudioFormatType AudioType;
public readonly short NumChannels;
public readonly int SampleRate;
public readonly int ByteRate;
public readonly short BlockAlign;
public readonly short BitsPerSample;
public readonly ReadOnlyMemory<byte> Data;
public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
short blockAlign, short bitsPerSample, ReadOnlyMemory<byte> data)
{
AudioType = audioType;
NumChannels = numChannels;
SampleRate = sampleRate;
ByteRate = byteRate;
BlockAlign = blockAlign;
BitsPerSample = bitsPerSample;
Data = data;
}
}
private enum WavAudioFormatType : short
{
Unknown = 0,
PCM = 1,
// There's a bunch of other types, those are all unsupported.
}
}
}

View File

@@ -1,51 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Vector2 = System.Numerics.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
[Robust.Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
private Thread? _gameThread;
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
return _initializeAudio();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
}
public void Shutdown()
{
_shutdownAudio();
}
private bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
}
}

View File

@@ -1,432 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.Audio
{
internal sealed partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
{
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
internal ISawmill OpenALSawmill = default!;
private bool _initializeAudio()
{
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return false;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
return true;
}
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
public void StopAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
}
public void DisposeAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
_audioSources.Clear();
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
target.Dispose();
}
}
_bufferedAudioSources.Clear();
}
private void _shutdownAudio()
{
DisposeAllAudio();
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
private void _updateAudio()
{
var eye = _eyeManager.CurrentEye;
var vec = eye.Position.Position;
AL.Listener(ALListener3f.Position, vec.X, vec.Y, -5);
var rot2d = eye.Rotation.ToVec();
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var rot = eye.Rotation.ToVec();
var at = new Vector3(0f, 0f, -1f);
var up = new Vector3(rot.Y, rot.X, 0f);
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
_flushALDisposeQueues();
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public void SetAudioAttenuation(int value)
{
var attenuation = (Attenuation) value;
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.Default:
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
OpenALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
}
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// 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.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
return audioSource;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
throw new Exception("Failed to generate source. Too many simultaneous audio streams?");
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
private void _checkAlError([CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = _readOggVorbis(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(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);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = _readWav(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
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);
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}
}

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