Compare commits

..

70 Commits

Author SHA1 Message Date
PJB3005
6018f2fb12 Version: 267.2.2 2025-12-01 16:05:20 +01:00
PJB3005
52c69fc293 Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
2025-12-01 16:05:19 +01:00
PJB3005
4dc17f3aca Version: 267.2.1 2025-09-26 13:40:39 +02:00
PJB3005
d22280f177 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

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

* release notes

* fixes

* I love ambiguous implicit casts

* avoid deferral issues

---------

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

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

* Revert "Version: 162.2.1"

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

* Fix CollectionExtensions.TryGetValue erroring for indexes under 0

* Revert "Version: 162.2.1"

This reverts commit 9b7f4d48cf.

* release notes

---------

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

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

* Removed goobstation comments

* Addressed changes

* Add layer field, set auto animated

* release notes

---------

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

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

* Add comment

---------

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

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

* support for changing attenuation curve type + lots of docs

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

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

* calc is slang for calculator

* fix

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

* shorter

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

Also added some extra helper features.

* Finish

* Fix bug, avoid IocResolves

* grid is grid

* file filename clash

* turn hint into option

* a

---------

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

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

* Prevent remove children of erroring entities

* better logging

* Improve error tolerance

* Even more exception tolerance

* missing !

* Improve handling of category errors

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

* release notes

* Typo fix

---------

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

* even less nesting

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

* dependency injection

* remove VersionInformationPrinter

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

* unfuck cefglue

* no!

---------

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

* remove unused

* meh implementation

* cefextension

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

* Change remote debugging handling

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

Also disabled debugging by default outside dev builds.

* Lower MaxAttempts to 15

100 was way too much and gave me anxiety idk.

* Fix non-TOOLS default of remote debug port

* Rewrite locking implementation.

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

* Undo unnecessary style changes

---------

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

* test

* more tests

* test overkill

* i'm tired

* rip it out

* Keep method but mark it as obsolete

* Release notes

* grammar

---------

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

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

* Add terminating or deleted checks to TryGetGridOrMapCoordinates

* Add comment to check if TerminatingOrDeleted check is necessary

* Reorganize AttachToGridOrMap to match TryGetGridOrMapCoordinates

* Move validation to method

* Replace internals with TryGetGridOrMapCoordinates

* Explicitly set coordinates type

* Format

* Change name back for now

* Don't duplicate `TerminatingOrDeleted()` check

* Don't call `GetInvWorldMatrix` for the map

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

* Fix parenting to terminating grid

* Fix matrix error

---------

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

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

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

* yarr

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

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

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

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

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

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

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

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

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

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

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

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

* Apply suggestions from code review

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-08-20 18:49:45 +02:00
135 changed files with 3070 additions and 8652 deletions

View File

@@ -59,6 +59,7 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<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" />

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,12 +54,134 @@ END TEMPLATE-->
*None yet*
## 267.0.2
## 267.2.2
## 267.0.1
## 267.2.1
## 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

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

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

View File

@@ -0,0 +1,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

@@ -61,12 +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)
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
var cachePath = FindAndLockCacheDirectory();
var settings = new CefSettings()
{
@@ -76,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

@@ -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

@@ -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 &&
@@ -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

@@ -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

@@ -354,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);
}
@@ -405,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);

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

@@ -328,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,57 +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 const string SDL_PROP_FILE_DIALOG_WINDOW_POINTER = "SDL.filedialog.window";
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

@@ -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

@@ -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

@@ -20,6 +20,7 @@
<PackageReference Include="OpenToolkit.Graphics" 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,7 @@
<RobustLinkAssemblies Include="TerraFX.Interop.Windows" />
<RobustLinkAssemblies Include="TerraFX.Interop.Xlib" />
<RobustLinkAssemblies Include="OpenToolkit.Graphics" />
<RobustLinkAssemblies Include="SpaceWizards.Sdl" />
<RobustLinkAssemblies Include="SpaceWizards.SharpFont" />
</ItemGroup>

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

@@ -22,7 +22,7 @@ internal sealed class XamlHotReloadManager : IXamlHotReloadManager
{
[Dependency] private readonly IConfigurationManager _cfg = null!;
[Dependency] private readonly ILogManager _logManager = null!;
[Dependency] private readonly IResourceManager _resources = null!;
[Dependency] private readonly IResourceManagerInternal _resources = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
@@ -120,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>();

View File

@@ -30,15 +30,6 @@ namespace Robust.Client.Utility
#endif
}
if (name == SDL.nativeLibName)
{
#if LINUX || FREEBSD
return NativeLibrary.Load("libSDL3.so.0", assembly, path);
#elif MACOS
return NativeLibrary.Load("libSDL3.0.dylib", assembly, path);
#endif
}
return IntPtr.Zero;
});

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

@@ -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

@@ -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

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

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);
}

View File

@@ -132,13 +132,13 @@ namespace Robust.Shared
/// Whether to interpolate between server game states for render frames on the client.
/// </summary>
public static readonly CVarDef<bool> NetInterp =
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENTONLY);
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENT | CVar.REPLICATED);
/// <summary>
/// The target number of game states to keep buffered up to smooth out network inconsistency.
/// </summary>
public static readonly CVarDef<int> NetBufferSize =
CVarDef.Create("net.buffer_size", 2, CVar.ARCHIVE | CVar.CLIENTONLY);
CVarDef.Create("net.buffer_size", 2, CVar.ARCHIVE | CVar.CLIENT | CVar.REPLICATED);
/// <summary>
/// The maximum size of the game state buffer. If this is exceeded the client will request a full game state.
@@ -1002,6 +1002,15 @@ namespace Robust.Shared
public static readonly CVarDef<bool> DisplayVSync =
CVarDef.Create("display.vsync", true, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Maximum framerate the client should run at. Set to 0 to have no limit.
/// </summary>
/// <remarks>
/// This is ignored if <see cref="DisplayVSync"/> is enabled.
/// </remarks>
public static readonly CVarDef<int> DisplayMaxFPS =
CVarDef.Create("display.max_fps", 0, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Window mode for the main game window. 0 = windowed, 1 = fullscreen.
/// </summary>

View File

@@ -33,9 +33,23 @@ sealed class AddMapCommand : LocalizedEntityCommands
shell.WriteError($"Map with ID {mapId} already exists!");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
var mapId = _mapSystem.GetNextMapId();
return CompletionResult.FromHintOptions([ new CompletionOption($"{mapId}")], LocalizationManager.GetString("generic-mapid"));
case 2:
return CompletionResult.FromHint(LocalizationManager.GetString("cmd-addmap-hint-2"));
default:
return CompletionResult.Empty;
}
}
}
sealed class RemoveMapCommand : LocalizedCommands
sealed class RemoveMapCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _systems = default!;
@@ -62,6 +76,14 @@ sealed class RemoveMapCommand : LocalizedCommands
mapSystem.DeleteMap(mapId);
shell.WriteLine($"Map {mapId.Value} was removed.");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(args[0], entManager: EntityManager), LocalizationManager.GetString("generic-map"));
}
}
sealed class RemoveGridCommand : LocalizedEntityCommands
@@ -88,6 +110,14 @@ sealed class RemoveGridCommand : LocalizedEntityCommands
EntityManager.DeleteEntity(gridId);
shell.WriteLine($"Grid {gridId} was removed.");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
return CompletionResult.FromHintOptions(CompletionHelper.Components<MapGridComponent>(args[0], entManager: EntityManager), LocalizationManager.GetString("generic-grid"));
}
}
internal sealed class RunMapInitCommand : LocalizedEntityCommands
@@ -153,7 +183,8 @@ internal sealed class ListMapsCommand : LocalizedEntityCommands
string.Join(",", _map.GetAllGrids(mapId).Select(grid => grid.Owner)));
}
shell.WriteLine(msg.ToString());
// Trim the newline
shell.WriteLine(msg.ToString()[..^1]);
}
}
@@ -183,6 +214,6 @@ internal sealed class ListGridsCommand : LocalizedEntityCommands
uid, xform.MapID, uid, worldPos.X, worldPos.Y);
}
shell.WriteLine(msg.ToString());
shell.WriteLine(msg.ToString()[..^1]);
}
}

View File

@@ -208,11 +208,16 @@ public static class CompletionHelper
return sorted ? playerOptions.OrderBy(o => o.Value) : playerOptions;
}
public static IEnumerable<CompletionOption> MapIds(string text, IEntityManager entManager)
{
return GetComponents<MapComponent>(text, entManager: entManager).Select(o => new CompletionOption(o.Component.MapId.ToString(), o.EntityName));
}
public static IEnumerable<CompletionOption> MapIds(IEntityManager? entManager = null)
{
IoCManager.Resolve(ref entManager);
return entManager.EntityQuery<MapComponent>(true).Select(o => new CompletionOption(o.MapId.ToString()));
return entManager.EntityQuery<MapComponent, MetaDataComponent>(true).Select(o => new CompletionOption(o.Item1.MapId.ToString(), o.Item2.EntityName));
}
public static IEnumerable<CompletionOption> MapUids(IEntityManager? entManager = null)
@@ -245,23 +250,29 @@ public static class CompletionHelper
}
}
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
private static IEnumerable<(T Component, string NetString, string EntityName)> GetComponents<T>(string text, IEntityManager entManager, int limit = 20)
where T : IComponent
{
if (text != string.Empty && !NetEntity.TryParse(text, out _))
yield break;
IoCManager.Resolve(ref entManager);
var query = entManager.AllEntityQueryEnumerator<T, MetaDataComponent>();
var i = 0;
while (i < limit && query.MoveNext(out _, out var metadata))
while (i < limit && query.MoveNext(out var comp, out var metadata))
{
var netString = metadata.NetEntity.ToString();
if (!netString.StartsWith(text))
continue;
i++;
yield return new CompletionOption(netString, metadata.EntityName);
yield return (comp, netString, metadata.EntityName);
}
}
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
{
IoCManager.Resolve(ref entManager);
return GetComponents<T>(text, entManager, limit).Select(o => new CompletionOption(o.NetString, o.EntityName));
}
}

View File

@@ -631,7 +631,7 @@ namespace Robust.Shared.Containers
continue;
Remove(ent, container, force: true);
Del(ent);
PredictedDel(ent);
}
}

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -139,6 +139,7 @@ namespace Robust.Shared.ContentPack
/// Returns a list of paths to all top-level content directories
/// </summary>
/// <returns></returns>
[Obsolete("This API is no longer content-accessible")]
IEnumerable<ResPath> GetContentRoots();
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.Utility;
@@ -69,5 +70,7 @@ namespace Robust.Shared.ContentPack
/// </para>
/// </remarks>
bool TryGetDiskFilePath(ResPath path, [NotNullWhen(true)] out string? diskPath);
new IEnumerable<string> GetContentRoots();
}
}

View File

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -371,20 +371,19 @@ namespace Robust.Shared.ContentPack
AddRoot(ResPath.Root, loader);
}
public IEnumerable<ResPath> GetContentRoots()
IEnumerable<ResPath> IResourceManager.GetContentRoots()
{
return [];
}
public IEnumerable<string> GetContentRoots()
{
foreach (var (_, root) in _contentRoots)
{
if (root is DirLoader loader)
{
var rootDir = loader.GetPath(new ResPath(@"/"));
if (root is not DirLoader loader)
continue;
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
yield return loader.GetPath(ResPath.Root);
}
}

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.
@@ -529,6 +533,7 @@ Types:
- "string ToTitleCase(string)"
- "string ToTitleCase(string)"
- "string ToUpper(string)"
UnicodeCategory: { } # Enum
System.IO.Compression:
CompressionMode: { } # Enum
CompressionLevel: { } # Enum
@@ -1559,6 +1564,11 @@ Types:
ValueType: { All: True }
Version: { All: True }
Void: { All: True }
WeakReference`1:
Methods:
- "void .ctor(!0)"
- "bool TryGetTarget(ref !0)"
- "void SetTarget(!0)"
YamlDotNet.Core.Events:
MappingStyle: { } # Enum
SequenceStyle: { } # Enum
@@ -1608,3 +1618,7 @@ Types:
SourcePos: { }
Pidgin.Configuration:
IConfiguration`1: { All: True }
SpaceWizards.Sodium:
CryptoGenericHashBlake2B:
Methods:
- "byte[] Hash(int, System.ReadOnlySpan`1<byte>, System.ReadOnlySpan`1<byte>)"

View File

@@ -685,38 +685,38 @@ public sealed class EntityDeserializer :
foreach (var yamlId in MapYamlIds)
{
var uid = UidMap[yamlId];
if (_mapQuery.TryComp(uid, out var map))
if (UidMap.TryGetValue(yamlId, out var uid) && _mapQuery.TryComp(uid, out var map))
{
Result.Maps.Add((uid, map));
EntMan.EnsureComponent<LoadedMapComponent>(uid);
}
else
_log.Error($"Missing map entity: {EntMan.ToPrettyString(uid)}");
_log.Error($"Missing map entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}");
}
foreach (var yamlId in GridYamlIds)
{
var uid = UidMap[yamlId];
if (_gridQuery.TryComp(uid, out var grid))
if (UidMap.TryGetValue(yamlId, out var uid) && _gridQuery.TryComp(uid, out var grid))
Result.Grids.Add((uid, grid));
else
_log.Error($"Missing grid entity: {EntMan.ToPrettyString(uid)}");
_log.Error($"Missing grid entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}");
}
foreach (var yamlId in OrphanYamlIds)
{
var uid = UidMap[yamlId];
if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as an orphan?");
if (!UidMap.TryGetValue(yamlId, out var uid))
_log.Error($"Missing orphan entity with YamlId: {yamlId}");
else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as an orphan? YamlId: {yamlId}");
else
Result.Orphans.Add(uid);
}
foreach (var yamlId in NullspaceYamlIds)
{
var uid = UidMap[yamlId];
if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
if (!UidMap.TryGetValue(yamlId, out var uid))
_log.Error($"Missing nullspace entity with YamlId: {yamlId}");
else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as a null-space entity?");
else
Result.NullspaceEntities.Add(uid);
@@ -1152,6 +1152,7 @@ public sealed class EntityDeserializer :
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<EntityUid>? _)
{
string msg;
if (node.Value == "invalid")
{
if (CurrentComponent == "Transform")
@@ -1160,7 +1161,7 @@ public sealed class EntityDeserializer :
if (!Options.LogInvalidEntities)
return EntityUid.Invalid;
var msg = CurrentReadingEntity is not { } curr
msg = CurrentReadingEntity is not { } curr
? $"Encountered invalid EntityUid reference"
: $"Encountered invalid EntityUid reference wile reading entity {curr.YamlId}, component: {CurrentComponent}";
_log.Error(msg);
@@ -1170,7 +1171,10 @@ public sealed class EntityDeserializer :
if (int.TryParse(node.Value, out var val) && UidMap.TryGetValue(val, out var entity))
return entity;
_log.Error($"Invalid yaml entity id: '{val}'");
msg = CurrentReadingEntity is not { } ent
? "Encountered unknown entity yaml uid"
: $"Encountered unknown entity yaml uid wile reading entity {ent.YamlId}, component: {CurrentComponent}";
_log.Error(msg);
return EntityUid.Invalid;
}

View File

@@ -110,6 +110,11 @@ public sealed class EntitySerializer : ISerializationContext,
/// </summary>
public readonly Dictionary<string, List<int>> Prototypes = new();
/// <summary>
/// Set of entities that have encountered issues during serialization and are now being ignored.
/// </summary>
public HashSet<EntityUid> ErroringEntities = new();
/// <summary>
/// Yaml ids of all serialized map entities.
/// </summary>
@@ -412,7 +417,7 @@ public sealed class EntitySerializer : ISerializationContext,
// It might be possible that something could cause an entity to be included twice.
// E.g., if someone serializes a grid w/o its map, and then tries to separately include the map and all its children.
// In that case, the grid would already have been serialized as a orphan.
// In that case, the grid would already have been serialized as an orphan.
// uhhh.... I guess its fine?
if (EntityData.ContainsKey(saveId))
return;
@@ -489,6 +494,95 @@ public sealed class EntitySerializer : ISerializationContext,
xform._localRotation = 0;
}
try
{
SerializeComponents(uid, cache, components);
}
catch(Exception e)
{
if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.Rethrow)
{
_log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}");
throw;
}
_log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}:\n{e}");
CurrentEntityYamlUid = 0;
CurrentEntity = null;
CurrentComponent = null;
RemoveErroringEntity(uid);
return;
}
CurrentComponent = null;
if (components.Count != 0)
entData.Add("components", components);
// TODO ENTITY SERIALIZATION
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
// want to serialize.
if (meta.EntityPrototype == null)
{
CurrentEntityYamlUid = 0;
CurrentEntity = null;
return;
}
// an entity may have fewer components than the original prototype, so we need to check if any are missing.
SequenceDataNode? missingComponents = null;
foreach (var (name, comp) in meta.EntityPrototype.Components)
{
// try comp instead of has-comp as it checks whether the component is supposed to have been
// deleted.
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
continue;
missingComponents ??= new();
missingComponents.Add(new ValueDataNode(name));
}
if (missingComponents != null)
entData.Add("missingComponents", missingComponents);
CurrentEntityYamlUid = 0;
CurrentEntity = null;
}
/// <summary>
/// Remove an exception throwing entity (and possibly its children) from the serialized data.
/// </summary>
private void RemoveErroringEntity(EntityUid uid)
{
if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.IgnoreEntityAndChildren)
{
foreach (var child in _xformQuery.GetComponent(uid)._children)
{
RemoveErroringEntity(child);
}
}
ErroringEntities.Add(uid);
if (!YamlUidMap.TryGetValue(uid, out var yamlId))
return;
Nullspace.Remove(yamlId);
Orphans.Remove(yamlId);
Maps.Remove(yamlId);
Grids.Remove(yamlId);
EntityData.Remove(yamlId);
if (_metaQuery.TryGetComponent(uid, out var meta)
&& meta.EntityPrototype != null
&& Prototypes.TryGetValue(meta.EntityPrototype.ID, out var proto))
{
proto.Remove(yamlId);
}
}
private void SerializeComponents(EntityUid uid, Dictionary<string, MappingDataNode>? cache, SequenceDataNode components)
{
foreach (var component in EntMan.GetComponentsInternal(uid))
{
var compType = component.GetType();
@@ -523,48 +617,12 @@ public sealed class EntitySerializer : ISerializationContext,
// Don't need to write it if nothing was written! Note that if this entity has no associated
// prototype, we ALWAYS want to write the component, because merely the fact that it exists is
// information that needs to be written.
if (compMapping.Children.Count != 0 || protoMapping == null)
{
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
components.Add(compMapping);
}
}
CurrentComponent = null;
if (components.Count != 0)
entData.Add("components", components);
// TODO ENTITY SERIALIZATION
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
// want to serialize.
if (meta.EntityPrototype == null)
{
CurrentEntityYamlUid = 0;
CurrentEntity = null;
return;
}
// an entity may have less components than the original prototype, so we need to check if any are missing.
SequenceDataNode? missingComponents = null;
foreach (var (name, comp) in meta.EntityPrototype.Components)
{
// try comp instead of has-comp as it checks whether the component is supposed to have been
// deleted.
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
if (compMapping.Children.Count == 0 && protoMapping != null)
continue;
missingComponents ??= new();
missingComponents.Add(new ValueDataNode(name));
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
components.Add(compMapping);
}
if (missingComponents != null)
entData.Add("missingComponents", missingComponents);
CurrentEntityYamlUid = 0;
CurrentEntity = null;
}
private Dictionary<string, MappingDataNode>? GetProtoCache(EntityPrototype? proto)
@@ -656,7 +714,10 @@ public sealed class EntitySerializer : ISerializationContext,
public SequenceDataNode WriteEntitySection()
{
if (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count)
// Check that EntityData contains the expected number of entities.
if (Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntity
&& Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntityAndChildren
&& (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count))
{
// Maybe someone reserved a yaml id with ReserveYamlId() or implicitly with GetId() without actually
// ever serializing the entity, This can lead to references to non-existent entities.
@@ -878,6 +939,7 @@ public sealed class EntitySerializer : ISerializationContext,
if (YamlUidMap.TryGetValue(value, out var yamlId))
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
if (CurrentComponent == _xformName)
{
if (value == EntityUid.Invalid)
@@ -886,12 +948,18 @@ public sealed class EntitySerializer : ISerializationContext,
DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid));
Orphans.Add(CurrentEntityYamlUid);
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate)
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate && !ErroringEntities.Contains(value))
_log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}");
return new ValueDataNode("invalid");
}
if (ErroringEntities.Contains(value))
{
// Referenced entity already logged an error, so we just silently fail.
return new ValueDataNode("invalid");
}
if (value == EntityUid.Invalid)
{
if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore)

View File

@@ -1,5 +1,4 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
@@ -21,7 +20,13 @@ public record struct SerializationOptions
public MissingEntityBehaviour MissingEntityBehaviour = MissingEntityBehaviour.IncludeNullspace;
/// <summary>
/// Whether or not to log an error when serializing an entity without its parent.
/// What to do when an exception is thrown while trying to serialize an entity. The default behaviour is to abort
/// the serialization.
/// </summary>
public EntityExceptionBehaviour EntityExceptionBehaviour = EntityExceptionBehaviour.Rethrow;
/// <summary>
/// Whether to log an error when serializing an entity without its parent.
/// </summary>
public bool ErrorOnOrphan = true;

View File

@@ -86,3 +86,33 @@ public enum MissingEntityBehaviour
/// </summary>
AutoInclude,
}
public enum EntityExceptionBehaviour
{
/// <summary>
/// Re-throw the exception, interrupting the serialization.
/// </summary>
Rethrow,
/// <summary>
/// Continue serializing and simply skip/ignore this entity. May result in broken maps that log errors or simply
/// fail to load.
/// </summary>
IgnoreEntity,
/// <summary>
/// Continue serializing and simply skip/ignore this entity and all of its children.
/// May result in broken maps that log errors or simply fail to load.
/// </summary>
IgnoreEntityAndChildren,
// TODO SERIALIZATION
/*
/// <summary>
/// Continue the serialization while skipping over the component that caused the exception to be thrown. May result
/// in broken maps that log errors or simply fail to load.
/// </summary>
IgnoreComponent,
*/
}

View File

@@ -110,7 +110,7 @@ public sealed partial class MapLoaderSystem
// Using a local deserializer instead of a cached value, both to ensure that we don't accidentally carry over
// data from a previous serializations, and because some entities cause other maps/grids to be loaded during
// during mapinit.
// mapinit.
var deserializer = new EntityDeserializer(
_dependency,
data,
@@ -124,6 +124,17 @@ public sealed partial class MapLoaderSystem
return false;
}
// If the file isn't of the expected category, stop before we ever create any entities.
if (opts.ExpectedCategory is { } expected
&& expected != deserializer.Result.Category
&& deserializer.Result.Category != FileCategory.Unknown)
{
// Did someone try to load a map file as a grid or vice versa?
Log.Error($"Map {fileName} does not contain the expected data. Expected {expected} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}
try
{
deserializer.CreateEntities();
@@ -135,6 +146,8 @@ public sealed partial class MapLoaderSystem
throw;
}
// If the map file was an older version, the category has to be inferred from the file's contents in CreateEntities()
// Hence the category is checked again here.
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
{
// Did someone try to load a map file as a grid or vice versa?

View File

@@ -14,6 +14,10 @@ public sealed class PointLightComponentState : ComponentState
public float Softness;
public float Falloff;
public float CurveFactor;
public bool CastShadows;
public bool Enabled;

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using System.Numerics;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects
{
@@ -33,6 +34,39 @@ namespace Robust.Shared.GameObjects
[DataField("softness"), Animatable]
public float Softness { get; set; } = 1f;
/// <summary>
/// Controls how quickly the light falls off in power in its radius.
/// A higher value means a stronger falloff.
/// </summary>
/// <remarks>
/// The default value of 6.8 might seem suspect, but that's because this is a value which was introduced
/// years after SS14 already standardized its light values using an older attenuation curve, and this was the value
/// which, qualitatively, seemed about equivalent in brightness for the large majority of lights on the station
/// compared to the old function.
///
/// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve
/// for different light radii and curve factors.
/// </remarks>
[DataField, Animatable]
public float Falloff { get; set; } = 6.8f;
/// <summary>
/// Controls the shape of the curve used for point light attenuation.
/// This value may vary between 0 and 1.
/// A value of 0 gives a shape roughly equivalent to 1/1+distance (more or less realistic),
/// while a value of 1 gives a shape roughly equivalent to 1+distance^2 (closer to a sphere-shaped light)
/// </summary>
/// <remarks>
/// This does not directly control the exponent of the denominator, though it might seem that way.
/// Rather, it just lerps between an inverse-shaped curve and an inverse-quadratic-shaped curve.
/// Values below 0 or above 1 are nonsensical.
///
/// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve
/// for different light radii and falloff values.
/// </remarks>
[DataField, Animatable]
public float CurveFactor { get; set; } = 0.0f;
/// <summary>
/// Whether this pointlight should cast shadows
/// </summary>

View File

@@ -49,6 +49,7 @@ public sealed partial class PrototypeLayerData
[DataField] public PrototypeCopyToShaderParameters? CopyToShaderParameters;
[DataField] public bool Cycle;
[DataField] public bool Loop = true;
}
/// <summary>

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