Compare commits

...

111 Commits

Author SHA1 Message Date
ElectroJr
9e8f7092ea Version: 267.2.0 2025-09-26 20:03:34 +12:00
pathetic meowmeow
de188cc773 Defer PredictedQueueDel detachment on the client (#6154)
* Defer PredictedQueueDel detachment on the client

* release notes

* fixes

* I love ambiguous implicit casts

* avoid deferral issues

---------

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

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

* Revert "Version: 162.2.1"

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

* Fix CollectionExtensions.TryGetValue erroring for indexes under 0

* Revert "Version: 162.2.1"

This reverts commit 9b7f4d48cf.

* release notes

---------

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

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

* Removed goobstation comments

* Addressed changes

* Add layer field, set auto animated

* release notes

---------

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

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

* Add comment

---------

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

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

* support for changing attenuation curve type + lots of docs

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

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

* calc is slang for calculator

* fix

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

* shorter

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

Also added some extra helper features.

* Finish

* Fix bug, avoid IocResolves

* grid is grid

* file filename clash

* turn hint into option

* a

---------

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

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

* Prevent remove children of erroring entities

* better logging

* Improve error tolerance

* Even more exception tolerance

* missing !

* Improve handling of category errors

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

* release notes

* Typo fix

---------

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

* even less nesting

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

* dependency injection

* remove VersionInformationPrinter

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

* unfuck cefglue

* no!

---------

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

* remove unused

* meh implementation

* cefextension

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

* Change remote debugging handling

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

Also disabled debugging by default outside dev builds.

* Lower MaxAttempts to 15

100 was way too much and gave me anxiety idk.

* Fix non-TOOLS default of remote debug port

* Rewrite locking implementation.

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

* Undo unnecessary style changes

---------

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

* test

* more tests

* test overkill

* i'm tired

* rip it out

* Keep method but mark it as obsolete

* Release notes

* grammar

---------

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

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

* Add terminating or deleted checks to TryGetGridOrMapCoordinates

* Add comment to check if TerminatingOrDeleted check is necessary

* Reorganize AttachToGridOrMap to match TryGetGridOrMapCoordinates

* Move validation to method

* Replace internals with TryGetGridOrMapCoordinates

* Explicitly set coordinates type

* Format

* Change name back for now

* Don't duplicate `TerminatingOrDeleted()` check

* Don't call `GetInvWorldMatrix` for the map

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

* Fix parenting to terminating grid

* Fix matrix error

---------

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

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

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

* yarr

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

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

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

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

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

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

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

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

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

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

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

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

* Apply suggestions from code review

---------

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

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

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

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

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

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

Fixed ServerSendToAll from the relevant callbacks sending to a disconnected channel.
2025-08-07 00:44:06 +02:00
PJB3005
ebce0daa1b Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day 2025-07-28 20:54:36 +02:00
PJB3005
c876eb1f4c Fix TextInputSetRect not accounting for pixel ratio properly.
Fixes it being positioned wrong on macOS.
2025-07-20 19:51:09 +02:00
PJB3005
1037fc735e Make SDL3 file dialogs have parent window.
Somehow needed to avoid causing it to block on macOS.
2025-07-19 18:45:14 +02:00
PJB3005
d5df765467 Package FreeBSD by default.
We won't officially support FreeBSD launcher builds, but this at least allows third-party launcher builds to have an engine to load properly.
2025-07-19 18:21:51 +02:00
PJB3005
93cf9f4227 Disable threaded window blit on macOS
Can probably do this on Linux too, but I didn't test that.

This feature is, fundamentally, a workaround to avoid WGL MakeCurrent() constantly breaking. The extra threading complexity is not a good thing on other platforms, so get rid of it.
2025-07-19 13:22:27 +02:00
PJB3005
d2977e2a63 Fix secondary window closing breaking rendering on macOS 2025-07-19 02:04:46 +02:00
PJB3005
a3f0ea19c4 Avoid WinBlit threads getting stuck forever when their window closes. 2025-07-19 00:42:31 +02:00
PJB3005
d9032b8757 Move some swapping code behind #ifdef
idk I just did this while debugging something and there's no harm committing it.
2025-07-19 00:42:00 +02:00
PJB3005
cba6e37f9f Fix SDL multiwindow freezing in some cases on macOS
Need SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH set, apparently.
2025-07-19 00:41:27 +02:00
PJB3005
90ec9a80c9 Fix publishing script not passing TargetOS properly 2025-07-16 22:04:15 +02:00
PJB3005
7eaf2f590b Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day 2025-07-15 15:23:23 +02:00
PJB3005
0439ea9893 Update packaging script to support ARM64 properly. 2025-07-15 15:14:40 +02:00
160 changed files with 3469 additions and 8872 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,161 @@ END TEMPLATE-->
*None yet*
## 267.2.0
### New features
* Sprites and Sprite layers have a new `Loop` data field that can be set to false to automatically pause animations once they have finished.
### Bugfixes
* Fixed `CollectionExtensions.TryGetValue` throwing an exception when given a negative list index.
* Fixed `EntityManager.PredictedQueueDeleteEntity()` not deferring changes for networked entities until the end of the tick.
* Fixed `EntityManager.IsQueuedForDeletion` not returning true foe entities getting deleted via `PredictedQueueDeleteEntity()`
### Other
* `IResourceManager.GetContentRoots()` has been obsoleted and returns no more results.
### Internal
* `IResourceManager.GetContentRoots()` has been replaced with a similar method on `IResourceManagerInternal`. This new method returns `string`s instead of `ResPath`s, and usage code has been updated to use these paths correctly.
## 267.1.0
### New features
* Animation:
* `AnimationTrackProperty.KeyFrame` can now have easings functions applied.
* Graphics:
* `PointLightComponent` now has two fields, `falloff` and `curveFactor`, for controlling light falloff and the shape of the light attenuation curve.
* `IClydeViewport` now has an `Id` and `ClearCachedResources` event. Together, these allow you to properly cache rendering resources per viewport.
* Miscellaneous:
* Added `display.max_fps` CVar.
* Added `IGameTiming.FrameStartTime`.
* Sandbox:
* Added `System.WeakReference<T>`.
* Added `SpaceWizards.Sodium.CryptoGenericHashBlake2B.Hash()`.
* Added `System.Globalization.UnicodeCategory`.
* Serialization:
* Added a new entity yaml deserialization option (`SerializationOptions.EntityExceptionBehaviour`) that can optionally make deserialization more exception tolerant.
* Tooling:
* `devwindow` now has a tab listing active `IRenderTarget`s, allowing insight into resource consumption.
* `loadgrid` now creates a map if passed an invalid map ID.
* Added game version information to F3 overlay.
* Added completions to more map commands.
* UI system:
* `Control.OrderedChildCollection` (gotten from `.Children`) now implements `IReadOnlyList<Control>`, allowing it to be indexed directly.
* Added `WrapContainer` control. This lays out multiple elements along an axis, wrapping them if there's not enough space. It comes with many options and can handle multiple axes.
* Popups/modals now work in secondary windows. This entails putting roots for these on each UI root.
* If you are not using `OSWindow` and are instead creating secondary windows manually, you need to call `WindowRoot.CreateRootControls()` manually for this to work.
* Added `Axis` enum, `IAxisImplementation` interface and axis implementations. These allow writing general-purpose UI layout code that can work on multiple axis at once.
* WebView:
* Added `web.remote_debug_port` CVar to change Chromium's remote debug port.
### Bugfixes
* Audio:
* Fix audio occlusion & velocity being calculated with the audio entity instead of the source entity.
* Bound UI:
* Try to fix an assert related to `UserInterfaceComponent` delta states.
* Configuration:
* The client no longer tries to send `CLIENT | REPLICATED` CVars when not connected to a server. This could cause test failures.
* Math:
* Fixed `Matrix3Helpers.TransformBounds()` returning an incorrect result. Now it effectively behaves like `Matrix3Helpers.TransformBox()` and has been marked as obsolete.
* Physics:
* Work around an undiagnosed crash processing entities without parents.
* Serialization:
* Fix `[DataRecord]`s with computed get-only properties.
* Resources:
* Fix some edge case broken path joining in `DirLoader` and `WritableDirProvider`.
* Tests:
* Fix `PlacementManager.CurrentMousePosition` in integration tests.
* UI system:
* Animations for the debug console and scrolling are no longer framerate dependent.
* Fix `OutputPanel.SetMessage` triggering a scrolling animation when editing messages other than the last one.
* Fix word wrapping with two-`char` runes in `RichTextLabel` and `OutputPanel`.
* WebView:
* Multiple clients with WebView can now run at the same time, thanks to better CEF cache management.
### Other
* Audio:
* Improved error logging for invalid file names in `SharedAudioSystem`.
* Configuration:
* Fix crash if more than 255 `REPLICATED` CVars exist. Also increased the max size of the CVar replication message.
* Entities:
* Transform:
* `AnchorEntity` logs instead of using an assert for invalid arguments.
* Containers:
* `SharedContainerSystem.CleanContainer` now uses `PredictedDel()` instead.
* Networking:
* The client now logs an error when attempting to send a network message without server connection. Previously, it would be silently dropped.
* `net.interp` and `net.buffer_size` CVars are now `REPLICATED`.
* Graphics:
* The function used for pointlight attenuation has been modified to be c1 continuous as opposed to simply c0 continuous, resulting in smoother boundary behavior.
* RSI validator no longer allows empty (`""`) state names.
* Packaging:
* Server packaging now excludes all files in the `Audio/` directory.
* Server packaging now excludes engine resources `EngineFonts/` and `Midi/`.
* ACZ explicitly specifies manifest charset as UTF-8.
* Serialization:
* `CurTime`-relative `TimeSpan` values that are `MaxValue` now deserialize without overflow.
* `SpriteSpecifier.Texture` will now fail to validate if the path is inside a `.rsi`. Use RSI sprite specifiers instead.
* Resources:
* `IWritableDirProvider.RootDir` is now null on clients.
* WebView:
* CEF cache is no longer in the content-accessible user data directory.
### Internal
* Added some debug commands for debugging viewport resource management: `vp_clear_all_cached` & `vp_test_finalize`
* `uitest` command now supports command argument for tab selection, like `uitest2`.
* Rewrote `BoxContainer` implementation to make use of new axis system.
* Moved `uitest2` and `devwindow` to use the `OSWindow` control.
* SDL3 binding has been moved to `SpaceWizards.Sdl` NuGet package.
* `dmetamem` command has been moved from `DEBUG` to `TOOLS`.
* Consolidate `AttachToGridOrMap` with `TryGetMapOrGridCoordinates`.
* Secondary window render targets have clear names specified.
* Updated `SpaceWizards.NFluidsynth` to `0.2.2`.
* `Robust.Client.WebView.Cef.Program` is now internal.
* `download_manifest_file.py` script in repo now always decodes as UTF-8 correctly.
* Added a new debug assert to game state processing.
## 267.0.0
### Breaking changes
* When a player disconnects, the relevant callbacks are now fired *after* removing the channel from `INetManager`.
### New features
* Engine builds are now published for ARM64 & FreeBSD.
* CPU model names are now detected on Windows & Linux ARM64.
* Toolshed's `spawn:in` command now works on entities without `Physics` component.
### Bugfixes
* SDL3 windowing backend fixes:
* Avoid macOS freezes with multiple windows.
* Fix macOS rendering breaking when closing secondary windows.
* File dialogs properly associate parent windows.
* Fix IME positions not working with UI scaling properly.
* Properly specify library names for loading native library.
* WinBit threads don't permanently stay stuck when their window closes.
* Checking for the "`null`" literal in serialization is now culture invariant.
### Other
* Compat mode on the client now defaults to on for Windows Snapdragon devices, to work around driver bugs.
* Update various libraries & natives. This enables out-of-the-box ARM64 support on all platforms and is a long-overdue modernization.
* Key name displays now use proper Unicode symbols for macOS ⌥ and ⌘.
* Automated CI for RobustToolbox runs on macOS again.
* Autocompletions for `ProtoId<T>` in Toolshed now use `PrototypeIdsLimited` instead of arbitrarily cutting out if more than 256 of a prototype exists.
## 266.0.0
### Breaking changes

View File

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

View File

@@ -577,3 +577,5 @@ cmd-localization_set_culture-desc = Set DefaultCulture for the client Localizati
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
cmd-localization_set_culture-culture-name = <cultureName>
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })
cmd-addmap-hint-2 = runMapInit [true / false]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,8 +54,14 @@ namespace Robust.Client.Animations
}
else
{
var next = KeyFrames[nextKeyFrame];
// Get us a scale 0 -> 1 here.
var t = playingTime / KeyFrames[nextKeyFrame].KeyTime;
var t = playingTime / next.KeyTime;
// Apply easing to time parameter, if one was specified
if (next.Easing != null)
t = next.Easing(t);
switch (InterpolationMode)
{
@@ -147,10 +153,20 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(object value, float keyTime)
/// <summary>
/// An easing function to apply when interpolating to this keyframe's value.
/// Modifies the time parameter (0..1) of the interpolation between the previous keyframe and this one.
/// </summary>
/// <remarks>
/// See <see cref="Easings"/> for examples of easing functions, or provide your own.
/// </remarks>
public readonly Func<float, float>? Easing;
public KeyFrame(object value, float keyTime, Func<float, float>? easing = null)
{
Value = value;
KeyTime = keyTime;
Easing = easing;
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Client.ResourceManagement;
using Robust.Shared;
@@ -145,7 +144,7 @@ internal sealed partial class AudioManager : IAudioInternal
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0)
EFX.DeleteFilter(handles.filterHandle);
ALC.EFX.DeleteFilter(handles.filterHandle);
}
private void _checkAlcError(ALDevice device,

View File

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

View File

@@ -372,13 +372,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
var parentUid = xform.ParentUid;
Vector2 worldPos;
component.Volume = component.Params.Volume;
// Handle grid audio differently by using grid position.
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
var parentUid = xform.ParentUid;
worldPos = _maps.GetGridPosition(parentUid);
}
else
@@ -412,7 +412,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
else
{
var occlusion = GetOcclusion(listener, delta, distance, entity);
var occlusion = GetOcclusion(listener, delta, distance, parentUid);
component.Occlusion = occlusion;
}
@@ -420,11 +420,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
component.Position = worldPos;
// Make race cars go NYYEEOOOOOMMMMM
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
if (_physicsQuery.TryGetComponent(parentUid, out var physicsComp))
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
var velocity = _physics.GetMapLinearVelocity(parentUid, physicsComp);
component.Velocity = velocity;
}
}
@@ -589,6 +589,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
// Since we're playing the sound immediately in the middle of a tick, we need to force ProcessStream -now-
// to set occlusion/position/velocity etc
// otherwise predicted positional sounds will sound very incorrect in several possible ways (e#5802, e#6175) until the next tick
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
return playing;
}
@@ -632,6 +637,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
// see PlayEntity for why this is necessary
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
return playing;
}
@@ -714,8 +723,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
offset = Math.Clamp(offset, 0f, maxOffset);
source.PlaybackPosition = offset;
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
ApplyAudioParams(comp.Params, comp);
source.StartPlaying();
return (entity, comp);
}

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -77,7 +76,7 @@ internal sealed class AudioSource : BaseAudioSource
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
ALC.EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
Master.RemoveAudioSource(SourceHandle);

View File

@@ -1,7 +1,6 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Sources;
@@ -82,9 +81,9 @@ public abstract class BaseAudioSource : IAudioSource
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
var state = AL.GetSource(SourceHandle, ALGetSourcei.SourceState);
Master._checkAlError();
return state == ALSourceState.Playing;
return state == (int)ALSourceState.Playing;
}
set
{
@@ -362,11 +361,11 @@ public abstract class BaseAudioSource : IAudioSource
if (audio is AuxiliaryAudio impAudio)
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
}
else
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
}
Master._checkAlError();
@@ -376,12 +375,12 @@ public abstract class BaseAudioSource : IAudioSource
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
FilterHandle = ALC.EFX.GenFilter();
ALC.EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ using Robust.Shared.ContentPack;
namespace Robust.Client.Console.Commands
{
#if DEBUG
#if TOOLS
internal sealed class DumpMetadataMembersCommand : LocalizedCommands
{
public override string Command => "dmetamem";

View File

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

View File

@@ -4,8 +4,6 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -44,7 +42,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
var progressBar = new ProgressBar { MaxValue = 10, Value = 5 };
vBox.AddChild(progressBar);
var optionButton = new OptionButton();
var optionButton = new OptionButton
{
ToolTip = "This button has a tooltip. Spooky!"
};
optionButton.AddItem("Honk");
optionButton.AddItem("Foo");
optionButton.AddItem("Bar");
@@ -155,6 +156,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
_sprite = new TabSpriteView();
_tabContainer.AddChild(_sprite);
_tabContainer.AddChild(TabCursorShapes());
_tabContainer.AddChild(new TabWrapContainer { Name = nameof(Tab.WrapContainer) });
}
public void OnClosed()
@@ -275,32 +277,13 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
RichText = 7,
SpriteView = 8,
TabCursorShapes = 9,
WrapContainer = 10,
}
}
internal sealed class UITestCommand : LocalizedCommands
internal abstract class BaseUITestCommand : LocalizedCommands
{
public override string Command => "uitest";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var window = new DefaultWindow { MinSize = new(800, 600) };
var control = new UITestControl();
window.OnClose += control.OnClosed;
window.Contents.AddChild(control);
window.OpenCentered();
}
}
internal sealed class UITest2Command : LocalizedCommands
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiMgr = default!;
public override string Command => "uitest2";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
public sealed override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
@@ -321,18 +304,10 @@ internal sealed class UITest2Command : LocalizedCommands
control.SelectTab(tab);
}
var window = _clyde.CreateWindow(new WindowCreateParameters
{
Title = Loc.GetString("cmd-uitest2-title"),
});
var root = _uiMgr.CreateWindowRoot(window);
window.DisposeOnClose = true;
window.RequestClosed += _ => control.OnClosed();
root.AddChild(control);
CreateWindow(control);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public sealed override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
@@ -343,4 +318,35 @@ internal sealed class UITest2Command : LocalizedCommands
return CompletionResult.Empty;
}
protected abstract void CreateWindow(UITestControl control);
}
internal sealed class UITestCommand : BaseUITestCommand
{
public override string Command => "uitest";
protected override void CreateWindow(UITestControl control)
{
var window = new DefaultWindow { MinSize = new(800, 600) };
window.OnClose += control.OnClosed;
window.Contents.AddChild(control);
window.OpenCentered();
}
}
internal sealed class UITest2Command : BaseUITestCommand
{
public override string Command => "uitest2";
protected override void CreateWindow(UITestControl control)
{
var window = new OSWindow
{
Title = Loc.GetString("cmd-uitest2-title"),
};
window.AddChild(control);
window.Closed += control.OnClosed;
window.Show();
}
}

View File

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

View File

@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SDL3;
namespace Robust.Client
{
@@ -93,6 +94,8 @@ namespace Robust.Client
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
_displayMode = mode;
if (!StartupSystemSplash(options, logHandlerFactory))
{
_logger.Fatal("Failed to start game controller!");

View File

@@ -110,6 +110,8 @@ namespace Robust.Client
private ResourceManifestData? _resourceManifest;
private DisplayMode _displayMode;
public void SetCommandLineArgs(CommandLineArgs args)
{
_commandLineArgs = args;
@@ -273,6 +275,9 @@ namespace Robust.Client
}
};
_configurationManager.OnValueChanged(CVars.DisplayMaxFPS, _ => UpdateVsyncConfig());
_configurationManager.OnValueChanged(CVars.DisplayVSync, _ => UpdateVsyncConfig(), invokeImmediately: true);
_clyde.Ready();
if (_resourceManifest!.AutoConnect &&
@@ -387,7 +392,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
@@ -709,6 +714,30 @@ namespace Robust.Client
}
private void UpdateVsyncConfig()
{
if (_displayMode == DisplayMode.Headless)
return;
var vsync = _configurationManager.GetCVar(CVars.DisplayVSync);
var maxFps = Math.Clamp(_configurationManager.GetCVar(CVars.DisplayMaxFPS), 0, 10_000);
_clyde.VsyncEnabled = vsync;
if (_mainLoop == null)
return;
if (vsync || maxFps == 0)
{
_mainLoop.SleepMode = SleepMode.None;
}
else
{
_mainLoop.SleepMode = SleepMode.Limit;
_mainLoop.LimitMinFrameTime = TimeSpan.FromSeconds(1.0 / maxFps);
}
}
internal enum DisplayMode : byte
{
Headless,

View File

@@ -29,6 +29,9 @@ namespace Robust.Client.GameObjects
internal event Action? AfterStartup;
internal event Action? AfterShutdown;
private readonly Queue<EntityUid> _queuedPredictedDeletions = new();
private readonly HashSet<EntityUid> _queuedPredictedDeletionsSet = new();
public override void Initialize()
{
SetupNetworking();
@@ -213,6 +216,34 @@ namespace Robust.Client.GameObjects
}
}
using (histogram?.WithLabels("PredictedQueueDel").NewTimer())
{
while (_queuedPredictedDeletions.TryDequeue(out var uid))
{
if (!MetaQuery.TryGetComponentInternal(uid, out var meta))
continue;
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
continue;
var xform = TransformQuery.GetComponentInternal(uid);
if (meta.NetEntity.IsClientSide())
{
DeleteEntity(uid, meta, xform);
}
else
{
_xforms.DetachEntity(uid, xform, meta, null);
// base call bypasses IGameTiming.InPrediction check
// This is pretty janky and there should be a way for the client to dirty an entity outside of prediction
// TODO PREDICTION
base.Dirty(uid, xform, meta);
}
}
_queuedPredictedDeletionsSet.Clear();
}
base.TickUpdate(frameTime, noPredictions, histogram);
}
@@ -317,18 +348,23 @@ namespace Robust.Client.GameObjects
}
}
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
public override bool IsQueuedForDeletion(EntityUid uid)
=> QueuedDeletionsSet.Contains(uid) || _queuedPredictedDeletions.Contains(uid);
if (ent.Comp1.NetEntity.IsClientSide())
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?> ent)
{
// Some UIs get disposed after entity-manager has shut down and already deleted all entities.
if (!Started)
return;
if (IsQueuedForDeletion(ent.Owner))
return;
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
if (ent.Comp.NetEntity.IsClientSide())
{
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
// base call to skip that.
@@ -337,7 +373,10 @@ namespace Robust.Client.GameObjects
}
else
{
_xforms.DetachEntity(ent.Owner, ent.Comp2);
if (!_queuedPredictedDeletionsSet.Add(ent.Owner))
return;
_queuedPredictedDeletions.Enqueue(ent.Owner);
}
}
}

View File

@@ -294,6 +294,16 @@ namespace Robust.Client.GameObjects
LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale);
}
/// <summary>
/// If false, this will prevent any of this sprite's animated layers from looping their animation.
/// This will set <see cref="Layer.AutoAnimated"/> whenever any layer's animation finishes.
/// </summary>
/// <remarks>
/// If this is false, this effectively overrides each layer's own <see cref="Layer.Loop"/>.
/// </remarks>
[DataField]
public bool Loop = true;
/// <summary>
/// Update this sprite component to visibly match the current state of other at the time
/// this is called. Does not keep them perpetually in sync.
@@ -601,6 +611,7 @@ namespace Robust.Client.GameObjects
layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy;
layer.Cycle = layerDatum.Cycle;
layer.Loop = layerDatum.Loop;
layer.Color = layerDatum.Color ?? layer.Color;
layer._rotation = layerDatum.Rotation ?? layer._rotation;
@@ -1157,6 +1168,15 @@ namespace Robust.Client.GameObjects
/// </remarks>
[ViewVariables] public bool Cycle;
/// <summary>
/// If false, this will prevent the layer's animation from looping.
/// This will set <see cref="AutoAnimated"/> to false once the animation finishes.
/// </summary>
/// <remarks>
/// This may be overriden by the parent's loop property.
/// </remarks>
[ViewVariables] public bool Loop = true;
// TODO SPRITE ACCESS
internal RSI.State? _actualState;
[ViewVariables] public RSI.State? ActualState => _actualState;
@@ -1336,6 +1356,8 @@ namespace Robust.Client.GameObjects
DirOffset = toClone.DirOffset;
_autoAnimated = toClone._autoAnimated;
RenderingStrategy = toClone.RenderingStrategy;
Cycle = toClone.Cycle;
Loop = toClone.Loop;
if (toClone.CopyToShaderParameters is { } copyToShaderParameters)
CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters);
}
@@ -1663,17 +1685,25 @@ 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)
{
if (Reversed)
{
AnimationFrame -= 1;
// Animation finished, do we cycle back to positive or reset.
if (AnimationFrame < 0)
{
if (!Loop || !_parent.Loop)
{
// stop at first frame
AnimationFrame = 0;
AnimationTimeLeft = 0;
AutoAnimated = false;
return;
}
if (Cycle)
{
AnimationFrame = 1;
@@ -1691,9 +1721,17 @@ namespace Robust.Client.GameObjects
{
AnimationFrame += 1;
// Animation finished, do we reverse or reset.
if (AnimationFrame >= delayCount)
{
if (!Loop || !_parent.Loop)
{
// stop at last frame
AnimationFrame = delayCount - 1;
AnimationTimeLeft = 0;
AutoAnimated = false;
return;
}
if (Cycle)
{
AnimationFrame = delayCount - 2;
@@ -1711,6 +1749,7 @@ namespace Robust.Client.GameObjects
AnimationTimeLeft += state.GetDelay(AnimationFrame);
}
}
}
/// <summary>

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.Contracts;
using Robust.Client.Graphics;
using Robust.Client.Map;
using Robust.Client.ResourceManagement;
@@ -9,13 +10,14 @@ namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
protected override MapId GetNextMapId()
[Pure]
internal override MapId GetNextMapId()
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
var id = new MapId(--LastMapId);
var id = new MapId(LastMapId - 1);
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(--LastMapId);
id = new MapId(id.Value - 1);
}
return id;
}

View File

@@ -29,6 +29,8 @@ namespace Robust.Client.GameObjects
component.Enabled = state.Enabled;
component.Offset = state.Offset;
component.Softness = state.Softness;
component.Falloff = state.Falloff;
component.CurveFactor = state.CurveFactor;
component.CastShadows = state.CastShadows;
component.Energy = state.Energy;
component.Radius = state.Radius;

View File

@@ -88,6 +88,7 @@ public sealed partial class SpriteSystem
target.Comp.RenderOrder = source.Comp.RenderOrder;
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
target.Comp.Loop = source.Comp.Loop;
DirtyBounds(target!);
_tree.QueueTreeUpdate(target!);

View File

@@ -220,24 +220,34 @@ Had full state: {LastFullState != null}"
{
var compState = change.State;
if (compState is IComponentDeltaState delta
&& compData.TryGetValue(change.NetID, out var old)) // May fail if relying on implicit data
if (compState is not IComponentDeltaState delta)
{
DebugTools.Assert(old is not IComponentDeltaState, "last state is not a full state");
if (cloneDelta)
{
compState = delta.CreateNewFullState(old!);
}
else
{
delta.ApplyToFullState(old!);
compState = old;
}
DebugTools.Assert(compState is not IComponentDeltaState, "newly constructed state is not a full state");
compData[change.NetID] = compState;
continue;
}
compData[change.NetID] = compState;
if (!compData.TryGetValue(change.NetID, out var old))
{
// Either the server needs to ensure that the initial state it sends to a client is a full
// state, or the client needs to be able to construct an implicit full state (i.e., get-state
// code needs to be in shared code).
//
// Without this, the client won't be able to reset predicted changes made to this component.
DebugTools.Assert("Received delta state without having received or constructed an implicit full state");
continue;
}
DebugTools.Assert(old is not IComponentDeltaState, "last state is not a full state");
if (!cloneDelta)
{
delta.ApplyToFullState(old!);
continue;
}
var newFull = delta.CreateNewFullState(old!);
compData[change.NetID] = newFull;
DebugTools.Assert(newFull is not IComponentDeltaState, "constructed state is not a full state");
}
if (entityState.NetComponents == null)

View File

@@ -1,6 +1,10 @@
using System;
using System.Runtime.InteropServices;
using Robust.Shared;
using Robust.Shared.Log;
#if WINDOWS
using TerraFX.Interop.Windows;
using TerraFX.Interop.DirectX;
#endif
namespace Robust.Client.Graphics.Clyde
{
@@ -13,6 +17,8 @@ namespace Robust.Client.Graphics.Clyde
private void InitGLContextManager()
{
CheckForceCompatMode();
// Advanced GL contexts currently disabled due to lack of testing etc.
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngle))
{
@@ -55,6 +61,74 @@ namespace Robust.Client.Graphics.Clyde
_glContext = new GLContextWindow(this);
}
private void CheckForceCompatMode()
{
#if WINDOWS
// Qualcomm (Snapdragon/Adreno) devices have broken OpenGL drivers on Windows.
if (CheckIsQualcommDevice())
{
_sawmillOgl.Info("We appear to be on a Qualcomm device. Enabling compat mode due to broken OpenGL driver");
_cfg.OverrideDefault(CVars.DisplayCompat, true);
}
#endif
}
#if WINDOWS
private static unsafe bool CheckIsQualcommDevice()
{
// Ideally we would check the OpenGL driver instead... but OpenGL is terrible so that's impossible.
// Let's just check with DXGI instead.
IDXGIFactory1* dxgiFactory;
ThrowIfFailed(
nameof(DirectX.CreateDXGIFactory1),
DirectX.CreateDXGIFactory1(Windows.__uuidof<IDXGIFactory1>(), (void**) &dxgiFactory));
try
{
uint idx = 0;
IDXGIAdapter* adapter;
while (dxgiFactory->EnumAdapters(idx, &adapter) != DXGI.DXGI_ERROR_NOT_FOUND)
{
try
{
DXGI_ADAPTER_DESC desc;
ThrowIfFailed("GetDesc", adapter->GetDesc(&desc));
var descString = ((ReadOnlySpan<char>)desc.Description).TrimEnd('\0');
if (descString.Contains("qualcomm", StringComparison.OrdinalIgnoreCase) ||
descString.Contains("snapdragon", StringComparison.OrdinalIgnoreCase) ||
descString.Contains("adreno", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
finally
{
adapter->Release();
}
idx += 1;
}
}
finally
{
dxgiFactory->Release();
}
return false;
}
private static void ThrowIfFailed(string methodName, HRESULT hr)
{
if (Windows.FAILED(hr))
{
Marshal.ThrowExceptionForHR(hr);
}
}
#endif
private struct GLContextSpec
{
public int Major;

View File

@@ -121,6 +121,19 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback)
{
ClearRenderState();
_renderHandle.RenderInRenderTarget(
renderTarget,
() =>
{
callback(_renderHandle);
},
null);
}
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
// Check that entity manager has started.

View File

@@ -451,6 +451,8 @@ namespace Robust.Client.Graphics.Clyde
var lastPower = float.NaN;
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
var lastSoftness = float.NaN;
var lastFalloff = float.NaN;
var lastCurveFactor = float.NaN;
Texture? lastMask = null;
using (_prof.Group("Draw Lights"))
@@ -504,6 +506,18 @@ namespace Robust.Client.Graphics.Clyde
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
}
if (!MathHelper.CloseToPercent(lastFalloff, component.Falloff))
{
lastFalloff = component.Falloff;
lightShader.SetUniformMaybe("lightFalloff", lastFalloff);
}
if (!MathHelper.CloseToPercent(lastCurveFactor, component.CurveFactor))
{
lastCurveFactor = component.CurveFactor;
lightShader.SetUniformMaybe("lightCurveFactor", lastCurveFactor);
}
lightShader.SetUniformMaybe("lightCenter", lightPos);
lightShader.SetUniformMaybe("lightIndex",
component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);

View File

@@ -209,6 +209,7 @@ namespace Robust.Client.Graphics.Clyde
var pressure = estPixSize * size.X * size.Y;
var handle = AllocRid();
var renderTarget = new RenderTexture(size, textureObject, this, handle);
var data = new LoadedRenderTarget
{
IsWindow = false,
@@ -220,10 +221,11 @@ namespace Robust.Client.Graphics.Clyde
MemoryPressure = pressure,
ColorFormat = format.ColorFormat,
SampleParameters = sampleParameters,
Instance = new WeakReference<RenderTargetBase>(renderTarget),
Name = name,
};
//GC.AddMemoryPressure(pressure);
var renderTarget = new RenderTexture(size, textureObject, this, handle);
_renderTargets.Add(handle, data);
return renderTarget;
}
@@ -301,10 +303,22 @@ namespace Robust.Client.Graphics.Clyde
}
}
private sealed class LoadedRenderTarget
public IEnumerable<(RenderTargetBase, LoadedRenderTarget)> GetLoadedRenderTextures()
{
foreach (var loaded in _renderTargets.Values)
{
if (!loaded.Instance.TryGetTarget(out var instance))
continue;
yield return (instance, loaded);
}
}
internal sealed class LoadedRenderTarget
{
public bool IsWindow;
public WindowId WindowId;
public string? Name;
public Vector2i Size;
public bool IsSrgb;
@@ -325,9 +339,11 @@ namespace Robust.Client.Graphics.Clyde
public long MemoryPressure;
public TextureSampleParameters? SampleParameters;
public required WeakReference<RenderTargetBase> Instance;
}
private abstract class RenderTargetBase : IRenderTarget
internal abstract class RenderTargetBase : IRenderTarget
{
protected readonly Clyde Clyde;
private bool _disposed;
@@ -389,7 +405,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private sealed class RenderTexture : RenderTargetBase, IRenderTexture
internal sealed class RenderTexture : RenderTargetBase, IRenderTexture
{
public RenderTexture(Vector2i size, ClydeTexture texture, Clyde clyde, ClydeHandle handle)
: base(clyde, handle)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
@@ -15,10 +16,14 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
new();
private long _nextViewportId = 1;
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
{
var handle = AllocRid();
var viewport = new Viewport(handle, name, this)
var viewport = new Viewport(_nextViewportId++, handle, name, this)
{
Size = size,
RenderTarget = CreateRenderTarget(size,
@@ -59,28 +64,43 @@ namespace Robust.Client.Graphics.Clyde
private void FlushViewportDispose()
{
// Free of allocations unless a dead viewport is found.
List<ClydeHandle>? toRemove = null;
foreach (var (handle, viewportRef) in _viewports)
while (_viewportDisposeQueue.TryDequeue(out var data))
{
if (!viewportRef.TryGetTarget(out _))
{
toRemove ??= new List<ClydeHandle>();
toRemove.Add(handle);
}
}
if (toRemove == null)
{
return;
}
foreach (var remove in toRemove)
{
_viewports.Remove(remove);
DisposeViewport(data);
}
}
private void DisposeViewport(ViewportDisposeData disposeData)
{
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
_viewports.Remove(disposeData.Handle);
if (disposeData.ClearEvent is not { } clearEvent)
return;
try
{
clearEvent(disposeData.ClearEventData);
}
catch (Exception ex)
{
_clydeSawmill.Error($"Caught exception while disposing viewport: {ex}");
}
}
#if TOOLS
public void ViewportsClearAllCached()
{
foreach (var vpRef in _viewports.Values)
{
if (!vpRef.TryGetTarget(out var vp))
continue;
vp.FireClear();
}
}
#endif // TOOLS
private sealed class Viewport : IClydeViewport
{
private readonly ClydeHandle _handle;
@@ -106,17 +126,20 @@ namespace Robust.Client.Graphics.Clyde
public string? Name { get; }
public Viewport(ClydeHandle handle, string? name, Clyde clyde)
public Viewport(long id, ClydeHandle handle, string? name, Clyde clyde)
{
Name = name;
_handle = handle;
_clyde = clyde;
Id = id;
}
public Vector2i Size { get; set; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public Vector2 RenderScale { get; set; } = Vector2.One;
public bool AutomaticRender { get; set; }
public long Id { get; }
void IClydeViewport.Render()
{
@@ -186,20 +209,56 @@ namespace Robust.Client.Graphics.Clyde
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
}
~Viewport()
{
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
}
public void Dispose()
{
GC.SuppressFinalize(this);
RenderTarget.Dispose();
LightRenderTarget.Dispose();
WallMaskRenderTarget.Dispose();
WallBleedIntermediateRenderTarget1.Dispose();
WallBleedIntermediateRenderTarget2.Dispose();
_clyde._viewports.Remove(_handle);
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
}
private ViewportDisposeData DisposeData(bool referenceSelf)
{
return new ViewportDisposeData
{
Handle = _handle,
Id = Id,
ClearEvent = ClearCachedResources,
ClearEventData = MakeClearEvent(referenceSelf)
};
}
private ClearCachedViewportResourcesEvent MakeClearEvent(bool referenceSelf)
{
return new ClearCachedViewportResourcesEvent(Id, referenceSelf ? this : null);
}
public void FireClear()
{
ClearCachedResources?.Invoke(MakeClearEvent(referenceSelf: true));
}
IRenderTexture IClydeViewport.RenderTarget => RenderTarget;
IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget;
public IEye? Eye { get; set; }
}
private sealed class ViewportDisposeData
{
public ClydeHandle Handle;
public long Id;
public Action<ClearCachedViewportResourcesEvent>? ClearEvent;
public ClearCachedViewportResourcesEvent ClearEventData;
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Robust.Client.Input;
@@ -101,6 +102,10 @@ namespace Robust.Client.Graphics.Clyde
_windowingThread = Thread.CurrentThread;
// Default to SDL3 on ARM64. GLFW is not feature complete there (lacking file dialog implementation)
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
_cfg.SetCVar(CVars.DisplayWindowingApi, "sdl3");
var windowingApi = _cfg.GetCVar(CVars.DisplayWindowingApi);
IWindowingImpl winImpl;
@@ -349,15 +354,17 @@ namespace Robust.Client.Graphics.Clyde
_windowHandles.Add(reg.Handle);
var rtId = AllocRid();
var renderTarget = new RenderWindow(this, rtId);
_renderTargets.Add(rtId, new LoadedRenderTarget
{
Size = reg.FramebufferSize,
IsWindow = true,
WindowId = reg.Id,
IsSrgb = true
IsSrgb = true,
Instance = new WeakReference<RenderTargetBase>(renderTarget),
});
reg.RenderTarget = new RenderWindow(this, rtId);
reg.RenderTarget = renderTarget;
_glContext!.WindowCreated(glSpec, reg);
}
@@ -374,6 +381,8 @@ namespace Robust.Client.Graphics.Clyde
if (reg.IsDisposed)
return;
_sawmillWin.Debug($"Destroying window {reg.Id}");
reg.IsDisposed = true;
_glContext!.WindowDestroyed(reg);
@@ -398,10 +407,17 @@ namespace Robust.Client.Graphics.Clyde
_glContext?.SwapAllBuffers();
}
private void VSyncChanged(bool newValue)
public bool VsyncEnabled
{
_vSync = newValue;
_glContext?.UpdateVSync();
get => _vSync;
set
{
if (_vSync == value)
return;
_vSync = value;
_glContext?.UpdateVSync();
}
}
private void WindowModeChanged(int mode)

View File

@@ -114,7 +114,6 @@ namespace Robust.Client.Graphics.Clyde
_proto.PrototypesReloaded += OnProtoReload;
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
_cfg.OnValueChanged(CVars.LightResolutionScale, LightResolutionScaleChanged, true);
_cfg.OnValueChanged(CVars.MaxShadowcastingLights, MaxShadowcastingLightsChanged, true);
@@ -128,7 +127,11 @@ namespace Robust.Client.Graphics.Clyde
// macOS cannot.
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
_cfg.OverrideDefault(CVars.DisplayThreadWindowApi, true);
#if MACOS
// Trust macOS to not need threaded window blitting.
// (threaded window blitting is a workaround to avoid having to frequently MakeCurrent() on Windows, as it is broken).
_cfg.OverrideDefault(CVars.DisplayThreadWindowBlit, false);
#endif
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
_threadWindowApi = _cfg.GetCVar(CVars.DisplayThreadWindowApi);

View File

@@ -34,6 +34,7 @@ namespace Robust.Client.Graphics.Clyde
public bool IsFocused => true;
private readonly List<IClydeWindow> _windows = new();
private int _nextWindowId = 2;
private long _nextViewportId = 1;
public ShaderInstance InstanceShader(ShaderSourceResource handle, bool? light = null, ShaderBlendMode? blend = null)
{
@@ -75,6 +76,11 @@ namespace Robust.Client.Graphics.Clyde
return [];
}
public IEnumerable<(Clyde.RenderTargetBase, Clyde.LoadedRenderTarget)> GetLoadedRenderTextures()
{
return [];
}
public ClydeDebugLayers DebugLayers { get; set; }
public string GetKeyName(Keyboard.Key key) => string.Empty;
@@ -240,7 +246,7 @@ namespace Robust.Client.Graphics.Clyde
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
string? name = null)
{
return new Viewport(size);
return new Viewport(_nextViewportId++, size);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
@@ -307,6 +313,19 @@ namespace Robust.Client.Graphics.Clyde
public IFileDialogManagerImplementation? FileDialogImpl => null;
public bool VsyncEnabled { get; set; }
#if TOOLS
public void ViewportsClearAllCached()
{
throw new NotImplementedException();
}
#endif // TOOLS
public void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback)
{
}
private sealed class DummyCursor : ICursor
{
public void Dispose()
@@ -482,15 +501,19 @@ namespace Robust.Client.Graphics.Clyde
private sealed class Viewport : IClydeViewport
{
public Viewport(Vector2i size)
public Viewport(long id, Vector2i size)
{
Size = size;
Id = id;
}
public void Dispose()
{
ClearCachedResources?.Invoke(new ClearCachedViewportResourcesEvent(Id, null));
}
public long Id { get; }
public IRenderTexture RenderTarget { get; } =
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
@@ -499,6 +522,7 @@ namespace Robust.Client.Graphics.Clyde
public IEye? Eye { get; set; }
public Vector2i Size { get; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public Vector2 RenderScale { get; set; }
public bool AutomaticRender { get; set; }

View File

@@ -102,6 +102,8 @@ namespace Robust.Client.Graphics.Clyde
{
var data = _windowData[reg.Id];
data.BlitDoneEvent?.Set();
// Set events so blit thread properly wakes up and notices it needs to shut down.
data.BlitStartEvent?.Set();
_windowData.Remove(reg.Id);
}
@@ -326,11 +328,14 @@ namespace Robust.Client.Graphics.Clyde
{
reg.RenderTexture?.Dispose();
reg.RenderTexture = Clyde.CreateRenderTarget(reg.Reg.FramebufferSize, new RenderTargetFormatParameters
{
ColorFormat = RenderTargetColorFormat.Rgba8Srgb,
HasDepthStencil = true
});
reg.RenderTexture = Clyde.CreateRenderTarget(
reg.Reg.FramebufferSize,
new RenderTargetFormatParameters
{
ColorFormat = RenderTargetColorFormat.Rgba8Srgb,
HasDepthStencil = true
},
name: $"{reg.Reg.Id}-RenderTexture");
// Necessary to correctly sync multi-context blitting.
reg.RenderTexture.MakeGLFence = true;
}

View File

@@ -1,25 +0,0 @@
/* SDL3-CS - C# Bindings for SDL3
*
* Copyright (c) 2024 Colin Jackson
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Colin "cryy22" Jackson <c@cryy22.art>
*
*/

View File

@@ -1,56 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SDL3;
public static partial class SDL
{
// Extensions to SDL3-CS that aren't part of the main library.
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
[LibraryImport(nativeLibName, EntryPoint = "SDL_WaitEvent")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial SDLBool SDL_WaitEventRef(ref SDL_Event @event);
public const byte SDL_BUTTON_LEFT = 1;
public const byte SDL_BUTTON_MIDDLE = 2;
public const byte SDL_BUTTON_RIGHT = 3;
public const byte SDL_BUTTON_X1 = 4;
public const byte SDL_BUTTON_X2 = 5;
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
public const int SDL_FILEDIALOG_OPENFILE = 0;
public const int SDL_FILEDIALOG_SAVEFILE = 1;
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
}

File diff suppressed because it is too large Load Diff

View File

@@ -81,6 +81,11 @@ internal partial class Clyde
case EventQuit:
ProcessEventQuit();
break;
#if MACOS
case EventWindowDestroyed:
ProcessEventWindowDestroyed();
break;
#endif
default:
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
break;
@@ -255,5 +260,15 @@ internal partial class Clyde
{
_clyde.SendInputModeChanged();
}
#if MACOS
private void ProcessEventWindowDestroyed()
{
// For some reason, on macOS, closing a secondary window
// causes the GL context on the primary thread to crap itself.
// Rebinding it seems to fix it.
GLMakeContextCurrent(_clyde._mainWindow);
}
#endif
}
}

View File

@@ -46,6 +46,10 @@ internal partial class Clyde
}
}
// NOTE: Giving a parent window is required to avoid the file dialog being blocking on macOS.
var mainWindow = (Sdl3WindowReg)_clyde._mainWindow!;
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_WINDOW_POINTER, mainWindow.Sdl3Window);
var task = ShowFileDialogWithProperties(type, props);
SDL.SDL_DestroyProperties(props);

View File

@@ -278,5 +278,9 @@ internal partial class Clyde
private sealed class EventKeyMapChanged : EventBase;
private sealed class EventQuit : EventBase;
#if MACOS
private sealed class EventWindowDestroyed : EventBase;
#endif
}
}

View File

@@ -7,8 +7,10 @@ using Robust.Shared.Maths;
using SDL3;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
#if WINDOWS
using BOOL = TerraFX.Interop.Windows.BOOL;
using Windows = TerraFX.Interop.Windows.Windows;
#endif
using GLAttr = SDL3.SDL.SDL_GLAttr;
using X11Window = TerraFX.Interop.Xlib.Window;
@@ -142,9 +144,12 @@ internal partial class Clyde
});
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
private void WinThreadWinDestroy(CmdWinDestroy cmd)
{
SDL.SDL_DestroyWindow(cmd.Window);
#if MACOS
SendEvent(new EventWindowDestroyed());
#endif
}
private (nint window, nint context) CreateSdl3WindowForRenderer(
@@ -461,6 +466,7 @@ internal partial class Clyde
var reg = (Sdl3WindowReg)window;
var windowPtr = WinPtr(reg);
#if WINDOWS
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
@@ -473,7 +479,7 @@ internal partial class Clyde
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
if (!reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
@@ -492,9 +498,12 @@ internal partial class Clyde
swapInterval = reg.SwapInterval;
}
}
#endif
//_sawmill.Debug($"Swapping: {window.Id} @ {_clyde._gameTiming.CurFrame}");
SDL.SDL_GL_SwapWindow(windowPtr);
#if WINDOWS
if (dwmFlush)
{
var i = swapInterval;
@@ -505,6 +514,7 @@ internal partial class Clyde
SDL.SDL_GL_SetSwapInterval(swapInterval);
}
#endif
}
public uint? WindowGetX11Id(WindowReg window)
@@ -547,17 +557,18 @@ internal partial class Clyde
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
var ratio = ((Sdl3WindowReg)reg).PixelRatio;
SendCmd(new CmdTextInputSetRect
{
Window = WinPtr(reg),
Rect = new SDL.SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
x = (int)(rect.Left / ratio.X),
y = (int)(rect.Top / ratio.Y),
w = (int)(rect.Width / ratio.X),
h = (int)(rect.Height / ratio.Y)
},
Cursor = cursor
Cursor = (int)(cursor / ratio.X)
});
}

View File

@@ -61,6 +61,10 @@ internal partial class Clyde
// https://github.com/libsdl-org/SDL/issues/11813
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
#if MACOS
SDL.SDL_SetHint(SDL.SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1");
#endif
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
if (!res)
{

View File

@@ -55,6 +55,7 @@ namespace Robust.Client.Graphics
Texture GetStockTexture(ClydeStockTexture stockTexture);
IEnumerable<(Clyde.Clyde.ClydeTexture, Clyde.Clyde.LoadedTexture)> GetLoadedTextures();
IEnumerable<(Clyde.Clyde.RenderTargetBase, Clyde.Clyde.LoadedRenderTarget)> GetLoadedRenderTextures();
ClydeDebugLayers DebugLayers { get; set; }
@@ -72,5 +73,20 @@ namespace Robust.Client.Graphics
void RunOnWindowThread(Action action);
IFileDialogManagerImplementation? FileDialogImpl { get; }
bool VsyncEnabled { get; set; }
// Viewports
#if TOOLS
/// <summary>
/// Fires <see cref="IClydeViewport.ClearCachedResources"/> on all viewports. For debugging.
/// </summary>
void ViewportsClearAllCached();
#endif // TOOLS
void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback);
}
}

View File

@@ -13,6 +13,11 @@ namespace Robust.Client.Graphics
/// </summary>
public interface IClydeViewport : IDisposable
{
/// <summary>
/// A unique ID for this viewport. No other viewport with this ID can ever exist in the app lifetime.
/// </summary>
long Id { get; }
/// <summary>
/// The render target that is rendered to when rendering this viewport.
/// </summary>
@@ -22,6 +27,16 @@ namespace Robust.Client.Graphics
IEye? Eye { get; set; }
Vector2i Size { get; }
/// <summary>
/// Raised when the viewport indicates that any cached rendering resources (e.g. render targets)
/// should be purged.
/// </summary>
/// <remarks>
/// This event is raised if the viewport is disposed (manually or via finalization).
/// However, code should expect this event to be raised at any time, even if the viewport is not disposed fully.
/// </remarks>
event Action<ClearCachedViewportResourcesEvent> ClearCachedResources;
/// <summary>
/// Color to clear the render target to before rendering. If null, no clearing will happen.
/// </summary>
@@ -85,4 +100,23 @@ namespace Robust.Client.Graphics
IViewportControl control,
in UIBox2i viewportBounds);
}
public struct ClearCachedViewportResourcesEvent
{
/// <summary>
/// The <see cref="IClydeViewport.Id"/> of the viewport.
/// </summary>
public readonly long ViewportId;
/// <summary>
/// The viewport itself. This is not available if the viewport was disposed.
/// </summary>
public readonly IClydeViewport? Viewport;
internal ClearCachedViewportResourcesEvent(long viewportId, IClydeViewport? viewport)
{
ViewportId = viewportId;
Viewport = viewport;
}
}
}

View File

@@ -197,6 +197,13 @@ namespace Robust.Client.Input
locId += "-linux";
}
#if MACOS
if (key == Key.Alt)
{
locId += "-mac";
}
#endif
if (loc.TryGetString(locId, out var name))
return name;

View File

@@ -562,7 +562,7 @@ namespace Robust.Client.Placement
}
coordinates = InputManager.MouseScreenPosition;
return true;
return coordinates.IsValid;
}
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)

View File

@@ -11,7 +11,6 @@ namespace Robust.Client.Replays.Commands;
public abstract class BaseReplayCommand : LocalizedCommands
{
[Dependency] protected readonly IReplayPlaybackManager PlaybackManager = default!;
protected ILocalizationManager Loc => LocalizationManager;
public override string Description => Loc.GetString($"cmd-{Command.Replace('_','-')}-desc");

View File

@@ -18,8 +18,9 @@
<PackageReference Include="SpaceWizards.NFluidsynth" PrivateAssets="compile" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="OpenToolkit.Graphics" PrivateAssets="compile" />
<PackageReference Include="OpenTK.OpenAL" PrivateAssets="compile" />
<PackageReference Include="OpenTK.Audio.OpenAL" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.SharpFont" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sdl" PrivateAssets="compile" />
<PackageReference Include="Robust.Natives" />
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
@@ -63,6 +64,8 @@
<RobustLinkAssemblies Include="TerraFX.Interop.Windows" />
<RobustLinkAssemblies Include="TerraFX.Interop.Xlib" />
<RobustLinkAssemblies Include="OpenToolkit.Graphics" />
<RobustLinkAssemblies Include="SpaceWizards.Sdl" />
<RobustLinkAssemblies Include="SpaceWizards.SharpFont" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets" />

View File

@@ -0,0 +1,192 @@
using System.Numerics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
/// <summary>
/// Defines an axis that certain controls can be laid out along.
/// </summary>
/// <seealso cref="IAxisImplementation"/>
public enum Axis : byte
{
/// <summary>
/// Items are laid out left to right.
/// </summary>
Horizontal,
/// <summary>
/// Items are laid out right to left.
/// </summary>
HorizontalReverse,
/// <summary>
/// Items are laid out top to bottom.
/// </summary>
Vertical,
/// <summary>
/// Items are laid out bottom to top.
/// </summary>
VerticalReverse,
}
/// <summary>
/// Interface that implements the rules of an <see cref="Axis"/>.
/// </summary>
/// <remarks>
/// <para>
/// To make it easier to write code that supports all 4 layout axis, layout code is advised to use generics over this
/// type and its implementors.
/// </para>
/// <para>
/// An axis has a "main" and a "cross" axis. For example,
/// <see cref="HorizontalAxis"/> has the main axis go left to right, and the cross axis go top to bottom.
/// </para>
/// <para>
/// The functions in this interface primarily allow converting between "UI space" (the normal UI coordinate system) and
/// "axis space" (same as UI space for <see cref="HorizontalAxis"/>). This allows you to write all code as if you're
/// doing only horizontal layout, but automatically have it work on all axis.
/// </para>
/// </remarks>
/// <seealso cref="HorizontalAxis"/>
/// <seealso cref="HorizontalReverseAxis"/>
/// <seealso cref="VerticalAxis"/>
/// <seealso cref="VerticalReverseAxis"/>
public interface IAxisImplementation
{
//
// To/from axis space conversions
//
/// <summary>
/// Convert a size value (e.g. from <see cref="Control.DesiredSize"/>) from UI space to axis space.
/// </summary>
static abstract Vector2 SizeToAxis(Vector2 size);
/// <summary>
/// Convert a size value (e.g. for <see cref="Control.Measure"/>) from axis space to UI space.
/// </summary>
static abstract Vector2 SizeFromAxis(Vector2 size);
/// <summary>
/// Convert a box (e.g. for <see cref="Control.Arrange"/>) from axis space to UI space.
/// </summary>
/// <param name="box">The box to convert, in axis space.</param>
/// <param name="spaceSize">The amount of space, in UI space, that the layout is happening relative to.</param>
static abstract UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize);
//
// Control
//
/// <summary>
/// Gets the "expand flag" (<see cref="Control.HorizontalExpand"/> or <see cref="Control.VerticalExpand"/>) for a
/// control that is appropriate for the main axis.
/// </summary>
static abstract bool GetMainExpandFlag(Control control);
}
/// <summary>
/// Axis implementation for <see cref="Axis.Horizontal"/>.
/// </summary>
public struct HorizontalAxis : IAxisImplementation
{
public static Vector2 SizeToAxis(Vector2 size)
{
return size;
}
public static Vector2 SizeFromAxis(Vector2 size)
{
return size;
}
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
{
return box;
}
public static bool GetMainExpandFlag(Control control)
{
return control.HorizontalExpand;
}
}
/// <summary>
/// Axis implementation for <see cref="Axis.HorizontalReverse"/>.
/// </summary>
public struct HorizontalReverseAxis : IAxisImplementation
{
public static Vector2 SizeToAxis(Vector2 size)
{
return size;
}
public static Vector2 SizeFromAxis(Vector2 size)
{
return size;
}
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
{
return new UIBox2(spaceSize.X - box.Right, box.Top, spaceSize.X - box.Left, box.Bottom);
}
public static bool GetMainExpandFlag(Control control)
{
return control.HorizontalExpand;
}
}
/// <summary>
/// Axis implementation for <see cref="Axis.Vertical"/>.
/// </summary>
public struct VerticalAxis : IAxisImplementation
{
public static Vector2 SizeToAxis(Vector2 size)
{
return new Vector2(size.Y, size.X);
}
public static Vector2 SizeFromAxis(Vector2 size)
{
return new Vector2(size.Y, size.X);
}
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
{
return new UIBox2(box.Top, box.Left, box.Bottom, box.Right);
}
public static bool GetMainExpandFlag(Control control)
{
return control.VerticalExpand;
}
}
/// <summary>
/// Axis implementation for <see cref="Axis.VerticalReverse"/>.
/// </summary>
public struct VerticalReverseAxis : IAxisImplementation
{
public static Vector2 SizeToAxis(Vector2 size)
{
return new Vector2(size.Y, size.X);
}
public static Vector2 SizeFromAxis(Vector2 size)
{
return new Vector2(size.Y, size.X);
}
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
{
return new UIBox2(box.Top, spaceSize.Y - box.Right, box.Bottom, spaceSize.Y - box.Left);
}
public static bool GetMainExpandFlag(Control control)
{
return control.VerticalExpand;
}
}

View File

@@ -1049,7 +1049,7 @@ namespace Robust.Client.UserInterface
Ignore = 2,
}
public sealed class OrderedChildCollection : ICollection<Control>, IReadOnlyCollection<Control>
public sealed class OrderedChildCollection : ICollection<Control>, IReadOnlyList<Control>
{
private readonly Control Owner;
@@ -1101,6 +1101,7 @@ namespace Robust.Client.UserInterface
int ICollection<Control>.Count => Owner.ChildCount;
int IReadOnlyCollection<Control>.Count => Owner.ChildCount;
public Control this[int index] => Owner._orderedChildren[index];
public bool IsReadOnly => false;

View File

@@ -27,8 +27,6 @@ namespace Robust.Client.UserInterface.Controls
/// </remarks>
public AlignMode Align { get; set; }
private bool Vertical => Orientation == LayoutOrientation.Vertical;
public LayoutOrientation Orientation
{
get => _orientation;
@@ -56,19 +54,24 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// Account for separation.
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
var desiredSize = Vector2.Zero;
if (Vertical)
if (Orientation == LayoutOrientation.Vertical)
{
desiredSize.Y += separation;
availableSize.Y = Math.Max(0, availableSize.Y - separation);
return MeasureItems<VerticalAxis>(availableSize);
}
else
{
desiredSize.X += separation;
availableSize.X = Math.Max(0, availableSize.X - separation);
return MeasureItems<HorizontalAxis>(availableSize);
}
}
private Vector2 MeasureItems<TAxis>(Vector2 availableSize) where TAxis : IAxisImplementation
{
availableSize = TAxis.SizeToAxis(availableSize);
// Account for separation.
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
var desiredSize = new Vector2(separation, 0);
availableSize.X = Math.Max(0, availableSize.X) - separation;
// First, we measure non-stretching children.
foreach (var child in Children)
@@ -76,46 +79,74 @@ namespace Robust.Client.UserInterface.Controls
if (!child.Visible)
continue;
child.Measure(availableSize);
child.Measure(TAxis.SizeFromAxis(availableSize));
var childDesired = TAxis.SizeToAxis(child.DesiredSize);
if (Vertical)
{
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
availableSize.Y = Math.Max(0, availableSize.Y - child.DesiredSize.Y);
}
else
{
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
availableSize.X = Math.Max(0, availableSize.X - child.DesiredSize.X);
}
desiredSize.X += childDesired.X;
desiredSize.Y = Math.Max(desiredSize.Y, childDesired.Y);
availableSize.X = Math.Max(0, availableSize.X - childDesired.X);
}
return desiredSize;
return TAxis.SizeFromAxis(desiredSize);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var separation = ActualSeparation;
var visibleChildCount = Children.Where(c => c.Visible).Count();
var stretchAvail = Vertical ? finalSize.Y : finalSize.X;
if (Orientation == LayoutOrientation.Vertical)
{
LayOutItems<VerticalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
}
else
{
LayOutItems<HorizontalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
}
return finalSize;
}
internal static void LayOutItems<TAxis>(
Vector2 baseOffset,
Vector2 finalSize,
AlignMode align,
OrderedChildCollection children,
int start,
int end,
float separation,
Vector2? fixedSize = null)
where TAxis : IAxisImplementation
{
var realFinalSize = finalSize;
finalSize = TAxis.SizeToAxis(finalSize);
fixedSize = fixedSize == null ? null : TAxis.SizeToAxis(fixedSize.Value);
var visibleChildCount = 0;
for (var i = start; i < end; i++)
{
if (children[i].Visible)
visibleChildCount += 1;
}
var stretchAvail = finalSize.X;
stretchAvail -= separation * (visibleChildCount - 1);
stretchAvail = Math.Max(0, stretchAvail);
// Step one: figure out the sizes of all our children and whether they want to stretch.
var sizeList = new List<(Control control, float size, bool stretch)>(visibleChildCount);
var totalStretchRatio = 0f;
foreach (var child in Children)
for (var i = start; i < end; i++)
{
var child = children[i];
if (!child.Visible)
continue;
bool stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
bool stretch = TAxis.GetMainExpandFlag(child);
if (!stretch)
{
var size = Vertical ? child.DesiredSize.Y : child.DesiredSize.X;
var measuredSize = fixedSize ?? TAxis.SizeToAxis(child.DesiredSize);
var size = measuredSize.X;
size = Math.Clamp(size, 0, stretchAvail);
stretchAvail -= size;
sizeList.Add((child, size, false));
@@ -146,7 +177,8 @@ namespace Robust.Client.UserInterface.Controls
continue;
var share = stretchAvail * control.SizeFlagsStretchRatio / totalStretchRatio;
var desired = Vertical ? control.DesiredSize.Y : control.DesiredSize.X;
var measuredSize = fixedSize ?? TAxis.SizeToAxis(control.DesiredSize);
var desired = measuredSize.X;
if (share >= desired)
{
sizeList[i] = (control, share, true);
@@ -164,7 +196,7 @@ namespace Robust.Client.UserInterface.Controls
else
{
// No stretching children -> offset the children based on the alignment.
switch (Align)
switch (align)
{
case AlignMode.Begin:
break;
@@ -190,22 +222,14 @@ namespace Robust.Client.UserInterface.Controls
first = false;
UIBox2 targetBox;
if (Vertical)
{
targetBox = new UIBox2(0, offset, finalSize.X, offset + size);
}
else
{
targetBox = new UIBox2(offset, 0, offset + size, finalSize.Y);
}
var targetBox = TAxis.BoxFromAxis(new UIBox2(offset, 0, offset + size, finalSize.Y), realFinalSize);
targetBox = targetBox.Translated(baseOffset);
control.Arrange(targetBox);
offset += size;
}
return finalSize;
}
public enum AlignMode : byte

View File

@@ -121,11 +121,14 @@ namespace Robust.Client.UserInterface.Controls
{
if (show)
{
if (Root == null)
throw new InvalidOperationException("No UI root! We can't pop up!");
var globalPos = GlobalPosition;
_popupVBox.Measure(Vector2Helpers.Infinity);
var (minX, minY) = _popupVBox.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
Root.ModalRoot.AddChild(_popup);
_popup.Open(box);
}
else
@@ -136,7 +139,7 @@ namespace Robust.Client.UserInterface.Controls
private void OnPopupHide()
{
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
_popup.Orphan();
}

View File

@@ -135,7 +135,13 @@ namespace Robust.Client.UserInterface.Controls
ClydeWindow.Resized += OnWindowResized;
_root = UserInterfaceManager.CreateWindowRoot(ClydeWindow);
_root.CreateRootControls();
// Add ourselves *after* creating the root.
// This way root controls are valid in EnteredTree().
// We have to re-organize the controls after, of course.
_root.AddChild(this);
SetPositionFirst();
// Resize the window by our UIScale
ClydeWindow.Size = new((int)(ClydeWindow.Size.X * UIScale), (int)(ClydeWindow.Size.Y * UIScale));

View File

@@ -153,13 +153,16 @@ namespace Robust.Client.UserInterface.Controls
{
if (show)
{
if (Root == null)
throw new InvalidOperationException("No UI root! We can't pop up!");
var globalPos = GlobalPosition;
globalPos.Y += Size.Y + 1; // Place it below us, with a safety margin.
globalPos.Y -= Margin.SumVertical;
OptionsScroll.Measure(Window?.Size ?? Vector2Helpers.Infinity);
var (minX, minY) = OptionsScroll.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
Root.ModalRoot.AddChild(_popup);
_popup.Open(box);
}
else
@@ -170,7 +173,7 @@ namespace Robust.Client.UserInterface.Controls
private void OnPopupHide()
{
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
_popup.Orphan();
}
private void ButtonOnPressed(ButtonEventArgs obj)

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
@@ -154,6 +155,7 @@ namespace Robust.Client.UserInterface.Controls
public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
var atBottom = !_scrollDownButton.Visible;
var oldEntry = _entries[index];
var font = _getFont();
_totalContentHeight -= oldEntry.Height + font.GetLineSeparation(UIScale);
@@ -164,6 +166,10 @@ namespace Robust.Client.UserInterface.Controls
_entries[index] = entry;
AddNewItemHeight(font, in entry);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (atBottom)
_scrollBar.Value = _scrollBar.MaxValue;
}
private void AddNewItemHeight(Font font, in RichTextEntry entry)
@@ -278,7 +284,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
[System.Diagnostics.Contracts.Pure]
[Pure]
private Font _getFont()
{
if (TryGetStyleProperty<Font>("font", out var font))
@@ -289,7 +295,7 @@ namespace Robust.Client.UserInterface.Controls
return UserInterfaceManager.ThemeDefaults.DefaultFont;
}
[System.Diagnostics.Contracts.Pure]
[Pure]
private StyleBox? _getStyleBox()
{
if (StyleBoxOverride != null)
@@ -301,14 +307,14 @@ namespace Robust.Client.UserInterface.Controls
return box;
}
[System.Diagnostics.Contracts.Pure]
[Pure]
private float _getScrollSpeed()
{
// The scroll speed depends on the UI scale because the scroll bar is working with physical pixels.
return GetScrollSpeed(_getFont(), UIScale);
}
[System.Diagnostics.Contracts.Pure]
[Pure]
private UIBox2 _getContentBox()
{
var style = _getStyleBox();

View File

@@ -30,7 +30,7 @@ namespace Robust.Client.UserInterface.Controls
public bool CloseOnEscape { get; set; } = true;
public virtual void Open(UIBox2? box = null, Vector2? altPos = null)
public virtual void Open(UIBox2? box = null, Vector2? altPos = null, Vector2? altPosUp = null)
{
if (Visible)
{
@@ -44,10 +44,12 @@ namespace Robust.Client.UserInterface.Controls
if (box != null &&
(_desiredSize != box.Value.Size ||
PopupContainer.GetPopupOrigin(this) != box.Value.TopLeft ||
PopupContainer.GetAltOrigin(this) != altPos))
PopupContainer.GetAltOrigin(this) != altPos ||
PopupContainer.GetAltOriginUp(this) != altPosUp))
{
PopupContainer.SetPopupOrigin(this, box.Value.TopLeft);
PopupContainer.SetAltOrigin(this, altPos);
PopupContainer.SetAltOriginUp(this, altPosUp);
_desiredSize = box.Value.Size;
InvalidateMeasure();

View File

@@ -27,6 +27,13 @@ namespace Robust.Client.UserInterface.Controls
public static readonly AttachedProperty AltOriginProperty = AttachedProperty.Create("AltOrigin",
typeof(PopupContainer), typeof(Vector2?), changed: PopupOriginChangedCallback);
/// <summary>
/// Alternative position to bottom-left-align the popup if <see cref="PopupOriginProperty"/>
/// would put it off-screen vertically.
/// </summary>
public static readonly AttachedProperty AltOriginUpProperty = AttachedProperty.Create("AltOriginUp",
typeof(PopupContainer), typeof(Vector2?), changed: PopupOriginChangedCallback);
public PopupContainer()
{
RectClipContent = true;
@@ -47,11 +54,21 @@ namespace Robust.Client.UserInterface.Controls
return control.GetValue<Vector2?>(AltOriginProperty);
}
public static Vector2? GetAltOriginUp(Control control)
{
return control.GetValue<Vector2?>(AltOriginUpProperty);
}
public static void SetAltOrigin(Control control, Vector2? origin)
{
control.SetValue(AltOriginProperty, origin);
}
public static void SetAltOriginUp(Control control, Vector2? origin)
{
control.SetValue(AltOriginUpProperty, origin);
}
private static void PopupOriginChangedCallback(Control owner, AttachedPropertyChangedEventArgs eventArgs)
{
if (owner.Parent is PopupContainer container)
@@ -67,47 +84,58 @@ namespace Robust.Client.UserInterface.Controls
var size = child.DesiredSize;
var offset = child.GetValue<Vector2>(PopupOriginProperty);
var altPos = child.GetValue<Vector2?>(AltOriginProperty);
var altPosUp = child.GetValue<Vector2?>(AltOriginUpProperty);
var (r, b) = size + offset; // bottom right corner.
var box = UIBox2.FromDimensions(offset, size);
var isAltPos = false;
var isAltPosUp = false;
// Clamp the right edge.
if (r > Width)
if (box.Right > Width)
{
// Try to position at alt pos.
if (altPos != null && altPos.Value.X - size.X > 0)
{
// There is horizontal room at the alt pos so there we go.
isAltPos = true;
offset = new Vector2(altPos.Value.X - size.X, altPos.Value.Y);
(_, b) = size + offset;
box = UIBox2.FromDimensions(new Vector2(altPos.Value.X - size.X, altPos.Value.Y), size);
}
else
{
offset -= new Vector2(r - Width, 0);
box = box.Translated(new Vector2(-(box.Right - Width), 0));
}
}
// Clamp the bottom edge.
if (b > Height)
if (box.Bottom > Height)
{
offset -= new Vector2(0, b - Height);
// Try to position at alt pos.
if (altPosUp != null && altPosUp.Value.Y - size.Y > 0)
{
// There is vertical room at the alt pos so there we go.
isAltPosUp = true;
box = UIBox2.FromDimensions(new Vector2(altPosUp.Value.X, altPosUp.Value.Y - size.Y), size);
}
else
{
box = box.Translated(new Vector2(0, -(box.Bottom - Height)));
}
}
// Try to clamp the left edge.
if (offset.X < 0 && !isAltPos)
if (box.Left < 0 && !isAltPos)
{
offset -= new Vector2(offset.X, 0);
box = box.Translated(new Vector2(-offset.X, 0));
}
// Try to clamp the top edge.
if (offset.Y < 0)
if (box.Top < 0 && !isAltPosUp)
{
offset -= new Vector2(0, offset.Y);
box = box.Translated(new Vector2(0, -offset.Y));
}
child.Arrange(UIBox2.FromDimensions(offset, size));
child.Arrange(box);
}
return finalSize;

View File

@@ -74,7 +74,7 @@ namespace Robust.Client.UserInterface.Controls
else
{
_updating = true;
Value = MathHelper.Lerp(Value, ValueTarget, Math.Min(args.DeltaSeconds * 15, 1));
Value = UIAnimations.LerpAnimate(Value, ValueTarget, args.DeltaSeconds, 15);
_updating = false;
}
}

View File

@@ -0,0 +1,270 @@
using System;
using System.Numerics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls;
internal sealed class TableContainer : Container
{
private int _columns = 1;
/// <summary>
/// The absolute minimum width a column can be forced to.
/// </summary>
/// <remarks>
/// <para>
/// If a column *asks* for less width than this (small contents), it can still be smaller.
/// But if it asks for more it cannot go below this width.
/// </para>
/// </remarks>
public float MinForcedColumnWidth { get; set; } = 50;
// Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
private ColumnData[] _columnDataCache = [];
private RowData[] _rowDataCache = [];
/// <summary>
/// How many columns should be displayed.
/// </summary>
public int Columns
{
get => _columns;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
_columns = value;
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
ResetCachedArrays();
// Do a first pass measuring all child controls as if they're given infinite space.
// This gives us a maximum width the columns want, which we use to proportion them later.
var columnIdx = 0;
foreach (var child in Children)
{
ref var column = ref _columnDataCache[columnIdx];
child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
columnIdx += 1;
if (columnIdx == _columns)
columnIdx = 0;
}
// Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
var totalMinWidth = 0f;
var totalMaxWidth = 0f;
var totalSlack = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
column.Slack = column.MaxWidth - column.MinWidth;
totalMinWidth += column.MinWidth;
totalMaxWidth += column.MaxWidth;
totalSlack += column.Slack;
}
if (totalMaxWidth <= availableSize.X)
{
// We want less horizontal space than we're given. Huh, that's convenient.
// Just set assigned width to be however much they asked for.
// We could probably skip the second measure pass in this scenario,
// but that's just an optimization, so I don't care right now.
//
// There's probably a very clever way to make this behavior work with the else block of logic,
// just by fiddling with the math.
// I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
column.AssignedWidth = column.MaxWidth;
}
}
else
{
// We don't have enough horizontal space,
// at least without causing *some* sort of word wrapping (assuming text contents).
//
// Assign horizontal space proportional to the wanted maximum size of the columns.
var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var slackRatio = column.Slack / totalSlack;
column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
}
}
// Go over controls for a second measuring pass, this time giving them their assigned measure width.
// This will give us a height to slot into per-row data.
// We still measure assuming infinite vertical space.
// This control can't properly handle being constrained on the Y axis.
columnIdx = 0;
var rowIdx = 0;
foreach (var child in Children)
{
ref var column = ref _columnDataCache[columnIdx];
ref var row = ref _rowDataCache[rowIdx];
child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
columnIdx += 1;
if (columnIdx == _columns)
{
columnIdx = 0;
rowIdx += 1;
}
}
// Sum up height of all rows to get final measured table height.
var totalHeight = 0f;
for (var r = 0; r < _rowDataCache.Length; r++)
{
ref var row = ref _rowDataCache[r];
totalHeight += row.MeasuredHeight;
}
return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
// TODO: Expand to fit given vertical space.
// Calculate MinWidth and Slack sums again from column data.
// We could've cached these from measure but whatever.
var totalMinWidth = 0f;
var totalSlack = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
totalMinWidth += column.MinWidth;
totalSlack += column.Slack;
}
// Calculate new width based on final given size, also assign horizontal positions of all columns.
var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
var xPos = 0f;
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var slackRatio = column.Slack / totalSlack;
column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
column.ArrangedX = xPos;
xPos += column.ArrangedWidth;
}
// Do actual arrangement row-by-row.
var arrangeY = 0f;
for (var r = 0; r < _rowDataCache.Length; r++)
{
ref var row = ref _rowDataCache[r];
for (var c = 0; c < _columns; c++)
{
ref var column = ref _columnDataCache[c];
var index = c + r * _columns;
if (index >= ChildCount) // Quit early if we don't actually fill out the row.
break;
var child = GetChild(c + r * _columns);
child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
}
arrangeY += row.MeasuredHeight;
}
return finalSize with { Y = arrangeY };
}
/// <summary>
/// Ensure cached array space is allocated to correct size and is reset to a clean slate.
/// </summary>
private void ResetCachedArrays()
{
// 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
if (_columnDataCache.Length != _columns)
_columnDataCache = new ColumnData[_columns];
Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
var rowCount = ChildCount / _columns;
if (ChildCount % _columns != 0)
rowCount += 1;
if (rowCount != _rowDataCache.Length)
_rowDataCache = new RowData[rowCount];
Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
}
/// <summary>
/// Per-column data used during layout.
/// </summary>
private struct ColumnData
{
// Measure data.
/// <summary>
/// The maximum width any control in this column wants, if given infinite space.
/// Maximum of all controls on the column.
/// </summary>
public float MaxWidth;
/// <summary>
/// The minimum width this column may be given.
/// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
/// </summary>
public float MinWidth;
/// <summary>
/// Difference between max and min width; how much this column can expand from its minimum.
/// </summary>
public float Slack;
/// <summary>
/// How much horizontal space this column was assigned at measure time.
/// </summary>
public float AssignedWidth;
// Arrange data.
/// <summary>
/// How much horizontal space this column was assigned at arrange time.
/// </summary>
public float ArrangedWidth;
/// <summary>
/// The horizontal position this column was assigned at arrange time.
/// </summary>
public float ArrangedX;
}
private struct RowData
{
// Measure data.
/// <summary>
/// How much height the tallest control on this row was measured at,
/// measuring for infinite vertical space but assigned column width.
/// </summary>
public float MeasuredHeight;
}
}

View File

@@ -18,6 +18,9 @@ namespace Robust.Client.UserInterface.Controls
public Color? BackgroundColor { get; set; }
public virtual LayoutContainer PopupRoot => throw new NotSupportedException();
public virtual PopupContainer ModalRoot => throw new NotSupportedException();
private Color _styleBgColor;
internal Color ActualBgColor => BackgroundColor ?? _styleBgColor;

View File

@@ -1,12 +1,14 @@
using Robust.Client.Graphics;
using System;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
public sealed class WindowRoot : UIRoot
{
private PopupContainer? _modalRoot;
private LayoutContainer? _popupRoot;
internal WindowRoot(IClydeWindow window)
{
Window = window;
@@ -32,5 +34,39 @@ namespace Robust.Client.UserInterface.Controls
/// </remarks>
/// <seealso cref="CVars.ResAutoScaleEnabled"/>
public bool DisableAutoScaling { get; set; } = true;
public override PopupContainer ModalRoot => _modalRoot ?? throw new InvalidOperationException(
$"Tried to access root controls without calling {nameof(CreateRootControls)}!");
public override LayoutContainer PopupRoot => _popupRoot ?? throw new InvalidOperationException(
$"Tried to access root controls without calling {nameof(CreateRootControls)}!");
/// <summary>
/// Creates root controls (e.g. <see cref="UIRoot.ModalRoot"/>) that are necessary for the UI system to
/// fully function.
/// </summary>
/// <remarks>
/// This should be called *after* inserting the main content into this instance,
/// so that the created root controls (e.g. popups) correctly stay on top.
/// </remarks>
public void CreateRootControls()
{
if (_modalRoot != null)
throw new InvalidOperationException("We've already created root controls!");
_modalRoot = new PopupContainer
{
Name = nameof(ModalRoot),
MouseFilter = MouseFilterMode.Ignore,
};
AddChild(_modalRoot);
_popupRoot = new LayoutContainer
{
Name = nameof(PopupRoot),
MouseFilter = MouseFilterMode.Ignore
};
AddChild(_popupRoot);
}
}
}

View File

@@ -0,0 +1,330 @@
using System;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls;
/// <summary>
/// Lays of children sequentially, "wrapping" them onto another row/column if necessary.
/// </summary>
public sealed class WrapContainer : Container
{
/// <summary>
/// Specifies the amount of space between two children, on the main axis.
/// </summary>
public const string StylePropertySeparation = "separation";
/// <summary>
/// Specifies the amount of space between two children, on the cross axis.
/// </summary>
public const string StylePropertyCrossSeparation = "cross-separation";
// Parameters.
private Axis _layoutAxis;
private ItemJustification _justification;
private bool _equalSize;
private bool _reverse;
private int? _separationOverride;
private int? _crossSeparationOverride;
// Cached layout data.
private ValueList<(int endIndex, float cross)> _rowIndices;
private float _lastMeasureCross;
/// <summary>
/// Specifies the amount of space between two children, on the main axis.
/// </summary>
/// <remarks>
/// This property overrides <see cref="StylePropertySeparation"/>, if set.
/// </remarks>
public int? SeparationOverride
{
get => _separationOverride;
set
{
_separationOverride = value;
InvalidateMeasure();
}
}
/// <summary>
/// Specifies the amount of space between two children, on the cross axis.
/// </summary>
/// <remarks>
/// This property overrides <see cref="StylePropertyCrossSeparation"/>, if set.
/// </remarks>
public int? CrossSeparationOverride
{
get => _crossSeparationOverride;
set
{
_crossSeparationOverride = value;
InvalidateMeasure();
}
}
/// <summary>
/// The <see cref="Axis"/> along which to lay out children.
/// </summary>
public Axis LayoutAxis
{
get => _layoutAxis;
set
{
_layoutAxis = value;
InvalidateMeasure();
}
}
/// <summary>
/// If true, all children will be laid out with the size of the largest child.
/// </summary>
public bool EqualSize
{
get => _equalSize;
set
{
_equalSize = value;
InvalidateMeasure();
}
}
/// <summary>
/// Determines where items will be laid out on an individual row/column.
/// </summary>
public ItemJustification Justification
{
get => _justification;
set
{
_justification = value;
InvalidateArrange();
}
}
/// <summary>
/// If true, reverses the order on which wrapping rows/columns are laid out.
/// </summary>
/// <remarks>
/// <para>
/// On horizontal axis, the first children are on the top row, and new rows are added <i>downwards</i>.
/// With <see cref="Reverse"/> set to true,
/// the first children are instead on the bottom row, with new rows growing upwards.
/// </para>
/// </remarks>
public bool Reverse
{
get => _reverse;
set
{
_reverse = value;
InvalidateArrange();
}
}
private int ActualSeparation
{
get
{
if (TryGetStyleProperty(StylePropertySeparation, out int separation))
{
return separation;
}
return SeparationOverride ?? 0;
}
}
private int ActualCrossSeparation
{
get
{
if (TryGetStyleProperty(StylePropertyCrossSeparation, out int separation))
{
return separation;
}
return CrossSeparationOverride ?? 0;
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var axis = LayoutAxis;
return axis switch
{
Axis.Horizontal => MeasureImplementation<HorizontalAxis>(availableSize),
Axis.HorizontalReverse => MeasureImplementation<HorizontalReverseAxis>(availableSize),
Axis.Vertical => MeasureImplementation<VerticalAxis>(availableSize),
Axis.VerticalReverse => MeasureImplementation<VerticalReverseAxis>(availableSize),
_ => throw new ArgumentOutOfRangeException()
};
}
private Vector2 MeasureImplementation<TAxis>(Vector2 availableSize) where TAxis : IAxisImplementation
{
_rowIndices.Clear();
var realAvailableSize = availableSize;
availableSize = TAxis.SizeToAxis(availableSize);
// TODO: Round to pixels properly.
var separation = ActualSeparation;
var crossSeparation = ActualCrossSeparation;
var curMainSize = 0f;
var curCrossSize = 0f;
var totalCrossSize = 0f;
var maxMainSize = 0f;
var firstOnRow = true;
var equalDesiredSize = _equalSize ? TAxis.SizeToAxis(GetMaxMeasure(realAvailableSize)) : default;
var countLaidOut = 0;
foreach (var control in Children)
{
Vector2 controlDesiredSize;
if (_equalSize)
{
controlDesiredSize = equalDesiredSize;
}
else
{
control.Measure(realAvailableSize);
controlDesiredSize = TAxis.SizeToAxis(control.DesiredSize);
}
var controlMainSize = controlDesiredSize.X;
var spaceTaken = controlMainSize + (firstOnRow ? 0 : separation);
if (curMainSize + spaceTaken > availableSize.X)
{
// We've wrapped.
RowEnd(lastRow: false);
curMainSize = controlMainSize;
}
else
{
curMainSize += spaceTaken;
}
curCrossSize = Math.Max(curCrossSize, controlDesiredSize.Y);
firstOnRow = false;
countLaidOut += 1;
}
RowEnd(lastRow: true);
_lastMeasureCross = totalCrossSize;
return TAxis.SizeFromAxis(new Vector2(maxMainSize, totalCrossSize));
void RowEnd(bool lastRow)
{
maxMainSize = Math.Max(maxMainSize, curMainSize);
totalCrossSize += curCrossSize;
if (!lastRow)
totalCrossSize += crossSeparation;
_rowIndices.Add((countLaidOut, curCrossSize));
curCrossSize = 0;
}
}
private Vector2 GetMaxDesired()
{
var vec = Vector2.Zero;
foreach (var child in Children)
{
vec = Vector2.Max(vec, child.DesiredSize);
}
return vec;
}
private Vector2 GetMaxMeasure(Vector2 availableSize)
{
var vec = Vector2.Zero;
foreach (var child in Children)
{
child.Measure(availableSize);
vec = Vector2.Max(vec, child.DesiredSize);
}
return vec;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var axis = LayoutAxis;
return axis switch
{
Axis.Horizontal => ArrangeImplementation<HorizontalAxis>(finalSize),
Axis.HorizontalReverse => ArrangeImplementation<HorizontalReverseAxis>(finalSize),
Axis.Vertical => ArrangeImplementation<VerticalAxis>(finalSize),
Axis.VerticalReverse => ArrangeImplementation<VerticalReverseAxis>(finalSize),
_ => throw new ArgumentOutOfRangeException()
};
}
private Vector2 ArrangeImplementation<TAxis>(Vector2 finalSize) where TAxis : IAxisImplementation
{
var realFinalSize = finalSize;
finalSize = TAxis.SizeToAxis(finalSize);
var separation = ActualSeparation;
var crossSeparation = ActualCrossSeparation;
var baseOffset = Reverse ? new Vector2(0, _lastMeasureCross) : Vector2.Zero;
var fixedSize = _equalSize ? (Vector2?)GetMaxDesired() : null;
var start = 0;
for (var i = 0; i < _rowIndices.Count; i++)
{
var (endIndex, cross) = _rowIndices[i];
if (Reverse)
{
baseOffset.Y -= cross;
}
var box = TAxis.BoxFromAxis(UIBox2.FromDimensions(baseOffset, finalSize with { Y = cross }), realFinalSize);
BoxContainer.LayOutItems<TAxis>(
box.TopLeft,
box.Size,
(BoxContainer.AlignMode)_justification,
Children,
start,
endIndex,
separation,
fixedSize);
start = endIndex;
if (Reverse)
{
baseOffset.Y -= crossSeparation;
}
else
{
baseOffset.Y = baseOffset.Y + cross + crossSeparation;
}
}
return realFinalSize;
}
/// <summary>
/// Justification values for <see cref="WrapContainer.Justification"/>.
/// </summary>
public enum ItemJustification : byte
{
// These MUST match the values in BoxContainer.
Begin = BoxContainer.AlignMode.Begin,
Center = BoxContainer.AlignMode.Center,
End = BoxContainer.AlignMode.End
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
@@ -224,10 +225,13 @@ public sealed partial class DebugConsole
if (_compPopup.Contents.ChildCount != 0)
{
_compPopup.Open(
UIBox2.FromDimensions(
offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
5, 5));
var box = UIBox2.FromDimensions(
offset - _compPopup.Contents.Margin.Left,
CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
5,
5);
var altPosUp = new Vector2(offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y);
_compPopup.Open(box, altPosUp: altPosUp);
}
}

View File

@@ -83,7 +83,7 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.ClearText += OnClearText;
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
Root!.ModalRoot.AddChild(_compPopup);
}
protected override void ExitedTree()
@@ -95,7 +95,7 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.ClearText -= OnClearText;
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
_compPopup.Orphan();
}
private void MaxEntriesChanged(int value)

View File

@@ -1,42 +1,39 @@
using System;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.Profiling;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
{
internal sealed class DebugMonitors : BoxContainer, IDebugMonitors
{
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientGameStateManager _state = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientNetManager _net = default!;
private readonly Control[] _monitors = new Control[Enum.GetNames<DebugMonitor>().Length];
//TODO: Think about a factory for this
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
IMapManager mapManager)
public void Init()
{
Visible = false;
SeparationOverride = 2;
Orientation = LayoutOrientation.Vertical;
Add(DebugMonitor.Fps, new FpsCounter(gameTiming));
Add(DebugMonitor.Fps, new FpsCounter(_timing));
Add(DebugMonitor.Coords, new DebugCoordsPanel());
Add(DebugMonitor.Net, new DebugNetPanel(netManager, gameTiming));
Add(DebugMonitor.Bandwidth, new DebugNetBandwidthPanel(netManager, gameTiming));
Add(DebugMonitor.Time, new DebugTimePanel(gameTiming, IoCManager.Resolve<IClientGameStateManager>()));
Add(DebugMonitor.Frames, new FrameGraph(gameTiming, IoCManager.Resolve<IConfigurationManager>()));
Add(DebugMonitor.Net, new DebugNetPanel(_net, _timing));
Add(DebugMonitor.Bandwidth, new DebugNetBandwidthPanel(_net, _timing));
Add(DebugMonitor.Time, new DebugTimePanel(_timing, _state));
Add(DebugMonitor.Frames, new FrameGraph(_timing, _cfg));
Add(DebugMonitor.Memory, new DebugMemoryPanel());
Add(DebugMonitor.Clyde, new DebugClydePanel { HorizontalAlignment = HAlignment.Left });
Add(DebugMonitor.System, new DebugSystemPanel { HorizontalAlignment = HAlignment.Left });
Add(DebugMonitor.Version, new DebugVersionPanel(_cfg) {HorizontalAlignment = HAlignment.Left});
Add(DebugMonitor.Input, new DebugInputPanel { HorizontalAlignment = HAlignment.Left });
Add(DebugMonitor.Prof, new LiveProfileViewControl());

View File

@@ -0,0 +1,39 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
{
internal sealed class DebugVersionPanel : PanelContainer
{
public DebugVersionPanel(IConfigurationManager cfg)
{
var contents = new Label
{
FontColorShadowOverride = Color.Black,
};
AddChild(contents);
PanelOverride = new StyleBoxFlat
{
BackgroundColor = new Color(35, 134, 37, 138),
ContentMarginLeftOverride = 5,
ContentMarginRightOverride = 5,
ContentMarginTopOverride = 5,
ContentMarginBottomOverride = 5,
};
MouseFilter = contents.MouseFilter = MouseFilterMode.Ignore;
// Set visible explicitly
Visible = true;
HorizontalAlignment = HAlignment.Left;
VerticalAlignment = VAlignment.Top;
var buildInfo = GameBuildInformation.GetBuildInfoFromConfig(cfg);
contents.Text = $"Fork ID: {buildInfo.ForkId}\nFork Version: {buildInfo.Version}";
}
}
}

View File

@@ -71,7 +71,7 @@ namespace Robust.Client.UserInterface.CustomControls
}
else
{
_curAnchorOffset = MathHelper.Lerp(_curAnchorOffset, targetOffset, args.DeltaSeconds * 20);
_curAnchorOffset = UIAnimations.LerpAnimate(_curAnchorOffset, targetOffset, args.DeltaSeconds, 20);
}
UpdateAnchorOffset();

View File

@@ -6,5 +6,6 @@
<DevWindowTabUI Name="UI" />
<DevWindowTabPerf Name="Perf" />
<DevWindowTabTextures Name="Textures" />
<DevWindowTabRenderTargets Name="RenderTargets" />
</TabContainer>
</Control>

View File

@@ -28,6 +28,7 @@ namespace Robust.Client.UserInterface
TabContainer.SetTabTitle(UI, "User Interface");
TabContainer.SetTabTitle(Perf, "Profiling");
TabContainer.SetTabTitle(Textures, Loc.GetString("dev-window-tab-textures-title"));
TabContainer.SetTabTitle(RenderTargets, Loc.GetString("dev-window-tab-render-targets-title"));
Stylesheet =
new DefaultStylesheet(IoCManager.Resolve<IResourceCache>(), IoCManager.Resolve<IUserInterfaceManager>()).Stylesheet;
@@ -41,26 +42,14 @@ namespace Robust.Client.UserInterface
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var monitor = clyde.EnumerateMonitors().First();
if (args.Length > 0)
var window = new OSWindow
{
var id = int.Parse(args[0]);
monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
}
var window = clyde.CreateWindow(new WindowCreateParameters
{
//Maximized = true,
Title = "Robust Debug Window",
//Monitor = monitor,
});
var root = IoCManager.Resolve<IUserInterfaceManager>().CreateWindowRoot(window);
window.DisposeOnClose = true;
};
var control = new DevWindow();
window.AddChild(control);
root.AddChild(control);
window.Show();
}
}
}

View File

@@ -0,0 +1,26 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.DevWindowTabRenderTargets">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Button Text="{Loc 'dev-window-tab-render-targets-reload'}" Name="ReloadButton" />
</BoxContainer>
<LineEdit Name="FilterEdit" PlaceHolder="{Loc 'dev-window-tab-render-targets-filter'}" />
<ScrollContainer VerticalExpand="True" VScrollEnabled="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical">
<TableContainer Name="Table" Margin="2" Columns="6">
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-id'}" />
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-name'}" />
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-size'}" />
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-type'}" />
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-vram'}" />
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-thumbnail'}" />
</TableContainer>
</BoxContainer>
</ScrollContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="TotalLabel" Margin="4" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using RTCF = Robust.Client.Graphics.RenderTargetColorFormat;
namespace Robust.Client.UserInterface;
[GenerateTypedNameReferences]
internal sealed partial class DevWindowTabRenderTargets : Control
{
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private readonly Control[] _gridHeader;
private readonly StyleBoxFlat _styleAltRow = new() { BackgroundColor = Color.FromHex("#222") };
#if TOOLS
private readonly HashSet<IRenderTarget> _copyTextures = new();
#endif // TOOLS
public DevWindowTabRenderTargets()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_gridHeader = Table.Children.ToArray();
ReloadButton.OnPressed += _ => Reload();
FilterEdit.OnTextChanged += _ => Reload();
}
protected override void VisibilityChanged(bool newVisible)
{
base.VisibilityChanged(newVisible);
if (newVisible)
{
Reload();
}
else
{
// Clear to release memory when tab not visible.
Clear();
}
}
protected override void ExitedTree()
{
base.ExitedTree();
Clear();
}
private void Clear()
{
Table.RemoveAllChildren();
#if TOOLS
foreach (var copiedTexture in _copyTextures)
{
copiedTexture.Dispose();
}
_copyTextures.Clear();
#endif
}
private void Reload()
{
Table.RemoveAllChildren();
foreach (var header in _gridHeader)
{
Table.AddChild(header);
}
var totalVram = 0L;
var even = true;
var rts = _clyde.GetLoadedRenderTextures().OrderBy(x => x.Item1.Handle.Value).ToArray();
foreach (var (instance, loaded) in rts)
{
#if TOOLS
if (_copyTextures.Contains(instance))
continue;
#endif // TOOLS
if (!string.IsNullOrWhiteSpace(FilterEdit.Text))
{
if (loaded.Name is not { } name)
continue;
if (!name.Contains(FilterEdit.Text, StringComparison.CurrentCultureIgnoreCase))
continue;
}
AddColumnText(instance.Handle.Value.ToString());
if (loaded.Name != null)
AddColumnText(loaded.Name);
else if (loaded.WindowId != WindowId.Invalid)
AddColumnText(loaded.WindowId.ToString());
else
AddColumnText(_loc.GetString("dev-window-tab-render-targets-value-null"));
AddColumnText(loaded.Size.ToString());
var type = loaded.ColorFormat.ToString();
if (loaded.DepthStencilHandle != default)
type += "+DS";
AddColumnText(type);
AddColumnText(ByteHelpers.FormatBytes(loaded.MemoryPressure));
// Disable texture thumbnails outside TOOLS.
// Avoid people cheating by using devwindow to see through walls. Barely.
#if TOOLS
if (instance is IRenderTexture renderTexture)
{
var clone = CloneTexture(renderTexture.Texture, loaded.ColorFormat);
_copyTextures.Add(clone);
AddColumn(new TextureRect
{
Texture = clone.Texture,
Stretch = TextureRect.StretchMode.KeepAspect
});
}
else
#endif // TOOLS
{
AddColumnText(_loc.GetString("dev-window-tab-render-targets-value-not-available"));
}
totalVram += loaded.MemoryPressure;
even = !even;
}
TotalLabel.Text = Loc.GetString(
"dev-window-tab-render-targets-summary",
("vram", ByteHelpers.FormatBytes(totalVram)));
return;
void AddColumnText(string text)
{
var richTextLabel = new RichTextLabel { Margin = new Thickness(4) };
richTextLabel.SetMessage(text, defaultColor: Color.White);
AddColumn(richTextLabel);
}
void AddColumn(Control control)
{
control.VerticalAlignment = VAlignment.Center;
if (even)
{
control = new PanelContainer
{
PanelOverride = _styleAltRow,
Children = { control },
};
}
else
{
// Wrapping control so we can use SetHeight.
control = new Control
{
Children = { control },
};
}
control.SetHeight = 50;
Table.AddChild(control);
}
}
#if TOOLS
private IRenderTexture CloneTexture(Texture texture, RTCF colorFormat)
{
var thumbnailSize = GetThumbnailSize(texture.Size);
var rt = _clyde.CreateRenderTarget(
thumbnailSize,
new RenderTargetFormatParameters
{
ColorFormat = colorFormat,
HasDepthStencil = false,
},
new TextureSampleParameters
{
Filter = true,
},
name: $"{nameof(DevWindowTabRenderTargets)}-clone");
_clyde.RenderNow(rt,
handle =>
{
handle.DrawingHandleScreen.DrawTextureRect(texture, UIBox2.FromDimensions(Vector2.Zero, thumbnailSize));
});
return rt;
}
private static Vector2i GetThumbnailSize(Vector2i textureSize)
{
const int maxHeight = 50;
const int maxWidth = 100;
var (w, h) = (Vector2)textureSize;
if (h > maxHeight)
{
w /= h / maxHeight;
h = maxHeight;
}
if (w > maxWidth)
{
h /= w / maxWidth;
w = maxWidth;
}
return new Vector2i((int)w, (int)h);
}
#endif // TOOLS
}

View File

@@ -36,5 +36,6 @@ public enum DebugMonitor
Input,
Bandwidth,
Prof,
System
System,
Version
}

View File

@@ -21,7 +21,7 @@ namespace Robust.Client.UserInterface
/// <param name="tooltip">control to position (current size will be used to determine bounds)</param>
public static void PositionTooltip(Control tooltip)
{
PositionTooltip(tooltip.UserInterfaceManager.RootControl.Size,
PositionTooltip(tooltip.Root!.Size,
tooltip.UserInterfaceManager.MousePositionScaled.Position,
tooltip);
}

View File

@@ -0,0 +1,13 @@
using System;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
internal static class UIAnimations
{
// From https://blog.pkh.me/p/41-fixing-the-iterative-damping-interpolation-in-video-games.html
public static float LerpAnimate(float a, float b, float dt, float rate)
{
return MathHelper.Lerp(a, b, 1f - MathF.Exp(-dt * rate));
}
}

View File

@@ -333,7 +333,7 @@ internal partial class UserInterfaceManager
if (_suppliedTooltip != null)
{
PopupRoot.RemoveChild(_suppliedTooltip);
_suppliedTooltip.Orphan();
_suppliedTooltip = null;
}
@@ -538,7 +538,7 @@ internal partial class UserInterfaceManager
if (_showingTooltip) return;
_showingTooltip = true;
var hovered = CurrentlyHovered;
if (hovered == null)
if (hovered == null || hovered.Root == null)
{
return;
}
@@ -563,7 +563,7 @@ internal partial class UserInterfaceManager
if (_suppliedTooltip == null)
return;
PopupRoot.AddChild(_suppliedTooltip);
hovered.Root.PopupRoot.AddChild(_suppliedTooltip);
Tooltips.PositionTooltip(_suppliedTooltip);
hovered.PerformShowTooltip();
}

View File

@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.CustomControls.DebugMonitorControls;
@@ -20,9 +18,7 @@ using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
@@ -37,13 +33,9 @@ namespace Robust.Client.UserInterface
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
@@ -86,10 +78,10 @@ namespace Robust.Client.UserInterface
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
[ViewVariables] public PopupContainer ModalRoot { get; private set; } = default!;
[ViewVariables] public PopupContainer ModalRoot => RootControl.ModalRoot;
[ViewVariables] public WindowRoot RootControl { get; private set; } = default!;
[ViewVariables] public LayoutContainer WindowRoot { get; private set; } = default!;
[ViewVariables] public LayoutContainer PopupRoot { get; private set; } = default!;
[ViewVariables] public LayoutContainer PopupRoot => RootControl.PopupRoot;
[ViewVariables] public DropDownDebugConsole DebugConsole { get; private set; } = default!;
[ViewVariables] public IDebugMonitors DebugMonitors => _debugMonitors;
private DebugMonitors _debugMonitors = default!;
@@ -117,10 +109,11 @@ namespace Robust.Client.UserInterface
DebugConsole = new DropDownDebugConsole();
RootControl.AddChild(DebugConsole);
DebugConsole.SetPositionInParent(ModalRoot.GetPositionInParent());
DebugConsole.SetPositionInParent(RootControl.ModalRoot.GetPositionInParent());
_debugMonitors = new DebugMonitors(_gameTiming, _playerManager, _eyeManager, _inputManager, _stateManager,
_clyde, _netManager, _mapManager);
_debugMonitors = new DebugMonitors();
_rootDependencies.InjectDependencies(_debugMonitors);
_debugMonitors.Init();
DebugConsole.BelowConsole.AddChild(_debugMonitors);
_inputManager.SetInputCommand(EngineKeyFunctions.ShowDebugConsole,
@@ -177,19 +170,7 @@ namespace Robust.Client.UserInterface
};
RootControl.AddChild(WindowRoot);
ModalRoot = new PopupContainer
{
Name = "ModalRoot",
MouseFilter = Control.MouseFilterMode.Ignore,
};
RootControl.AddChild(ModalRoot);
PopupRoot = new LayoutContainer
{
Name = "PopupRoot",
MouseFilter = Control.MouseFilterMode.Ignore
};
RootControl.AddChild(PopupRoot);
RootControl.CreateRootControls();
}
public void InitializeTesting()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics.Contracts;
using System.Text;
using Robust.Client.Graphics;
@@ -36,10 +36,18 @@ internal struct WordWrap
LastRune = new Rune('A');
}
/// <summary>
/// Add <paramref name="rune" /> as the next glyph in the sequence and update internal line break state.
/// </summary>
/// <param name="breakLine">If non-null, indicates that a line break needs to be added at this rune
/// index within the string. This index will refer to the offset of a previously-entered rune</param>
/// <param name="breakNewLine">If non-null, indicates the rune index of an entered newline</param>
/// <param name="skip">If true, indicates that the rune should occupy zero space. Currently only used
/// for newlines.</param>
public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip)
{
BreakIndexCounter = NextBreakIndexCounter;
NextBreakIndexCounter += rune.Utf16SequenceLength;
NextBreakIndexCounter += 1;
breakLine = null;
breakNewLine = null;

View File

@@ -3,7 +3,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -18,24 +20,25 @@ namespace Robust.Client.UserInterface.XAML.Proxy;
/// </remarks>
internal sealed class XamlHotReloadManager : IXamlHotReloadManager
{
private const string MarkerFileName = "SpaceStation14.sln";
[Dependency] ILogManager _logManager = null!;
[Dependency] private readonly IResourceManager _resources = null!;
[Dependency] private readonly IConfigurationManager _cfg = null!;
[Dependency] private readonly ILogManager _logManager = null!;
[Dependency] private readonly IResourceManagerInternal _resources = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
private ISawmill _sawmill = null!;
private FileSystemWatcher? _watcher;
private string _markerFileName = null!;
public void Initialize()
{
_markerFileName = _cfg.GetCVar(CVars.XamlHotReloadMarkerName);
_sawmill = _logManager.GetSawmill("xamlhotreload");
var codeLocation = InferCodeLocation();
if (codeLocation == null)
{
_sawmill.Warning($"could not find code -- where is {MarkerFileName}?");
_sawmill.Warning($"could not find code -- where is {_markerFileName}?");
return;
}
@@ -117,9 +120,9 @@ internal sealed class XamlHotReloadManager : IXamlHotReloadManager
private string? InferCodeLocation()
{
// ascend upwards from each content root until the solution file is found
foreach (var contentRoot in _resources.GetContentRoots())
foreach (var baseSystemPath in _resources.GetContentRoots())
{
var systemPath = contentRoot.ToRelativeSystemPath();
var systemPath = baseSystemPath;
while (true)
{
var files = Array.Empty<string>();
@@ -129,7 +132,7 @@ internal sealed class XamlHotReloadManager : IXamlHotReloadManager
}
catch (IOException) { } // this is allowed to fail, and if so we just keep going up
if (files.Any(f => Path.GetFileName(f).Equals(MarkerFileName, StringComparison.InvariantCultureIgnoreCase)))
if (files.Any(f => Path.GetFileName(f).Equals(_markerFileName, StringComparison.InvariantCultureIgnoreCase)))
{
return systemPath;
}

View File

@@ -1,6 +1,9 @@
using System;
#if !WINDOWS
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenTK.Audio.OpenAL;
using SDL3;
namespace Robust.Client.Utility
{
@@ -9,32 +12,31 @@ namespace Robust.Client.Utility
[ModuleInitializer]
internal static void Initialize()
{
if (OperatingSystem.IsWindows())
return;
NativeLibrary.SetDllImportResolver(typeof(ClientDllMap).Assembly, (name, assembly, path) =>
{
if (name == "swnfd.dll")
{
if (OperatingSystem.IsLinux())
return NativeLibrary.Load("libswnfd.so", assembly, path);
if (OperatingSystem.IsMacOS())
return NativeLibrary.Load("libswnfd.dylib", assembly, path);
return IntPtr.Zero;
#if LINUX || FREEBSD
return NativeLibrary.Load("libswnfd.so", assembly, path);
#elif MACOS
return NativeLibrary.Load("libswnfd.dylib", assembly, path);
#endif
}
if (name == "libEGL.dll")
{
if (OperatingSystem.IsLinux())
return NativeLibrary.Load("libEGL.so", assembly, path);
return IntPtr.Zero;
#if LINUX || FREEBSD
return NativeLibrary.Load("libEGL.so", assembly, path);
#endif
}
return IntPtr.Zero;
});
#if MACOS
OpenALLibraryNameContainer.OverridePath = "libopenal.1.dylib";
#endif
}
}
}
#endif

View File

@@ -19,7 +19,7 @@ internal sealed class ReloadManager : IReloadManager
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly IResourceManagerInternal _res = default!;
#pragma warning disable CS0414
[Dependency] private readonly ITaskManager _tasks = default!;
#pragma warning restore CS0414
@@ -72,7 +72,7 @@ internal sealed class ReloadManager : IReloadManager
public void Register(ResPath directory, string filter)
{
Register(directory.ToString(), filter);
Register(directory.ToRelativeSystemPath(), filter);
}
public void Register(string directory, string filter)
@@ -83,7 +83,7 @@ internal sealed class ReloadManager : IReloadManager
#if TOOLS
foreach (var root in _res.GetContentRoots())
{
var path = root + directory;
var path = Path.Join(root, directory);
if (!Directory.Exists(path))
{
@@ -128,17 +128,17 @@ internal sealed class ReloadManager : IReloadManager
_tasks.RunOnMainThread(() =>
{
var fullPath = args.FullPath.Replace(Path.DirectorySeparatorChar, '/');
var file = new ResPath(fullPath);
foreach (var rootIter in _res.GetContentRoots())
{
if (!file.TryRelativeTo(rootIter, out var relative))
var relPath = Path.GetRelativePath(rootIter, args.FullPath);
if (relPath == args.FullPath)
{
// Not relative.
continue;
}
_reloadQueue.Add(relative.Value);
var file = ResPath.FromRelativeSystemPath(relPath);
_reloadQueue.Add(file);
}
});
}

View File

@@ -24,8 +24,9 @@ namespace Robust.Packaging;
/// PresetPassesCore --2--> Output
/// InputResources --> PresetPassesResources
/// PresetPassesResources --1--> AudioMetadata
/// PresetPassesResources --2--> NormalizeTextResources
/// PresetPassesResources --3--> PrefixResources
/// PresetPassesResources --2--> DropAudioFiles
/// PresetPassesResources --3--> NormalizeTextResources
/// PresetPassesResources --4--> PrefixResources
/// AudioMetadata --> PrefixResources
/// NormalizeTextResources --> PrefixResources
/// PrefixResources --> Output
@@ -59,6 +60,17 @@ public sealed class RobustServerAssetGraph
public AssetPassPipe PresetPassesResources { get; }
public AssetPassAudioMetadata AudioMetadata { get; }
/// <summary>
/// Used to drop all files in the <c>Audio/</c> directory.
/// </summary>
/// <remarks>
/// <para>
/// Most audio files are actually removed by <see cref="AudioMetadata"/>.
/// This pass cleans up stuff like attribution files and soundfonts in <c>Audio/MidiCustom</c>.
/// </para>
/// </remarks>
public AssetPassFilterDrop DropAudioFiles { get; }
/// <summary>
/// Normalizes text files in resources.
/// </summary>
@@ -103,10 +115,12 @@ public sealed class RobustServerAssetGraph
PresetPassesResources = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesResources" };
NormalizeTextResources = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextResources" };
AudioMetadata = new AssetPassAudioMetadata { Name = "RobustServerAssetGraphAudioMetadata" };
DropAudioFiles = new AssetPassFilterDrop(p => p.Path.StartsWith("Audio/")) { Name = "RobustServerAssetGraphDropAudioFiles" };
PrefixResources = new AssetPassPrefix("Resources/") { Name = "RobustServerAssetGraphPrefixResources" };
PresetPassesResources.AddDependency(InputResources);
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(DropAudioFiles);
DropAudioFiles.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
NormalizeTextResources.AddDependency(PresetPassesResources).AddBefore(PrefixResources);
PrefixResources.AddDependency(PresetPassesResources);
PrefixResources.AddDependency(AudioMetadata);
@@ -124,6 +138,7 @@ public sealed class RobustServerAssetGraph
NormalizeTextResources,
AudioMetadata,
PrefixResources,
DropAudioFiles
};
}
}

View File

@@ -8,7 +8,9 @@ public sealed class RobustServerPackaging
{
"Textures",
"Fonts",
"EngineFonts",
"Shaders",
"Midi"
};
public static async Task WriteServerResources(

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -102,8 +102,8 @@ namespace Robust.Server.Console.Commands
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (!sys.MapExists(mapId))
{
shell.WriteError("Target map does not exist.");
return;
shell.WriteError($"MapID {intMapId} did not exist, creating without map init");
sys.CreateMap(mapId, false); // doesnt runmapinit to be conservative.
}
Vector2 offset = default;

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.Contracts;
using System.Linq;
using Robust.Server.GameStates;
using Robust.Shared;
@@ -18,13 +19,15 @@ namespace Robust.Server.GameObjects
private bool _deleteEmptyGrids;
protected override MapId GetNextMapId()
[Pure]
internal override MapId GetNextMapId()
{
var id = new MapId(++LastMapId);
var id = new MapId(LastMapId + 1);
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(++LastMapId);
id = new MapId(id.Value + 1);
}
return id;
}

View File

@@ -28,7 +28,8 @@
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sodium" PrivateAssets="compile" />
<PackageReference Include="SharpZstd.Interop" PrivateAssets="compile" />
<PackageReference Include="SharpZstd.Interop" PrivateAssets="compile" ExcludeAssets="native" />
<PackageReference Include="Robust.Natives.Zstd" />
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />

View File

@@ -92,7 +92,7 @@ namespace Robust.Server.ServerStatus
{
context.ResponseHeaders.Add("Content-Encoding", "zstd");
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK, "text/plain; charset=utf-8");
}
else
{
@@ -110,7 +110,7 @@ namespace Robust.Server.ServerStatus
}
else
{
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK, "text/plain; charset=utf-8");
}
}

View File

@@ -25,11 +25,11 @@ public static class Matrix3Helpers
return a.EqualsApprox(b, (float) tolerance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("Use TransformBox")]
// This method was previously broken, and now just returns an bounding box pretending to be a Box2Rotated
public static Box2Rotated TransformBounds(this Matrix3x2 refFromBox, Box2Rotated box)
{
var matty = Matrix3x2.Multiply(refFromBox, box.Transform);
return new Box2Rotated(Vector2.Transform(box.BottomLeft, matty), Vector2.Transform(box.TopRight, matty));
return new Box2Rotated(TransformBox(refFromBox, box));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -437,7 +437,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
protected TimeSpan GetAudioLength(string filename)
{
if (!filename.StartsWith('/'))
throw new ArgumentException("Path must be rooted");
throw new ArgumentException($"Path must be rooted. Path: {filename}");
return GetAudioLengthImpl(filename);
}

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