Compare commits

..

6 Commits

Author SHA1 Message Date
PJB3005
93c887eec5 Version: 261.2.3 2025-09-26 13:40:43 +02:00
PJB3005
45a2895e4b 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:43 +02:00
PJB3005
7d6def6adf Version: 261.2.2 2025-09-19 09:17:27 +02:00
Skye
f733a9efa5 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:27 +02:00
PJB3005
8a68dce987 Version: 261.2.1 2025-09-14 14:55:51 +02:00
PJB3005
b8d840437e Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:51 +02:00
399 changed files with 15706 additions and 10709 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,10 +16,7 @@
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false">
<SetConfiguration Condition="'$(Configuration)' == 'DebugOpt'">Configuration=Debug</SetConfiguration>
<SetConfiguration Condition="'$(Configuration)' == 'Tools'">Configuration=Release</SetConfiguration>
</ProjectReference>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false"/>
</ItemGroup>
<!-- XamlIL does not make use of special Robust configurations like DebugOpt. Convert these down. -->

View File

@@ -54,392 +54,13 @@ END TEMPLATE-->
*None yet*
## 267.3.0
## 261.2.3
### New features
* Sandbox:
* Added `System.DateOnly` and `System.TimeOnly`.
* `MapId`, `MapCoordinates`, and `EntityCoordinates` are now yaml serialisable
* The base component tree lookup system has new methods including several new `QueryAabb()` overloads that take in a collection and various new `IntersectRay()` overloads that should replace `IntersectRayWithPredicate`.
* Added `OccluderSystem.InRangeUnoccluded()` for checking for occluders that lie between two points.
* `LocalizedCommands` now pass the command name as an argument to the localized help text.
## 261.2.2
### Bugfixes
* Fixed `MapLoaderSystem.SerializeEntitiesRecursive()` not properly serialising when given multiple root entities (e.g., multiple maps)
* Fixed yaml hot reloading throwing invalid path exceptions.
* The `EntityManager.CreateEntityUninitialized` overload that uses MapCoordinates now actually attaches entities to a grid if one is present at those coordinates, as was stated in it's documentation.
* Fixed physics joint relays not being properly updated when an entity is removed from a container.
### Other
* Updated natives again to attempt to fix issues caused by the previous update.
## 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
* When a player disconnects, the relevant callbacks are now fired *after* removing the channel from `INetManager`.
### New features
* Engine builds are now published for ARM64 & FreeBSD.
* CPU model names are now detected on Windows & Linux ARM64.
* Toolshed's `spawn:in` command now works on entities without `Physics` component.
### Bugfixes
* SDL3 windowing backend fixes:
* Avoid macOS freezes with multiple windows.
* Fix macOS rendering breaking when closing secondary windows.
* File dialogs properly associate parent windows.
* Fix IME positions not working with UI scaling properly.
* Properly specify library names for loading native library.
* WinBit threads don't permanently stay stuck when their window closes.
* Checking for the "`null`" literal in serialization is now culture invariant.
### Other
* Compat mode on the client now defaults to on for Windows Snapdragon devices, to work around driver bugs.
* Update various libraries & natives. This enables out-of-the-box ARM64 support on all platforms and is a long-overdue modernization.
* Key name displays now use proper Unicode symbols for macOS ⌥ and ⌘.
* Automated CI for RobustToolbox runs on macOS again.
* Autocompletions for `ProtoId<T>` in Toolshed now use `PrototypeIdsLimited` instead of arbitrarily cutting out if more than 256 of a prototype exists.
## 266.0.0
### Breaking changes
* A new analyzer has been added that will error if you attempt to subscribe to `AfterAutoHandleStateEvent` on a
component that doesn't have the `AutoGenerateComponentState` attribute, or doesn't have the first argument of that
attribute set to `true`. In most cases you will want to set said argument to `true`.
* The fields on `AutoGenerateComponentStateAttribute` are now `readonly`. Setting these directly (instead of using the constructor arguments) never worked in the first place, so this change only catches existing programming errors.
* When a player disconnects, `ISharedPlayerManager.PlayerStatusChanged` is now fired *after* removing the session from the `Sessions` list.
* `.rsi` files are now compacted into individual `.rsic` files on packaging. This should significantly reduce file count & improve performance all over release builds, but breaks the ability to access `.png` files into RSIs directly. To avoid this, `"rsic": false` can be specified in the RSI's JSON metadata.
* The `scale` command has been removed, with the intent of it being moved to content instead.
### New features
* ViewVariables editors for `ProtoId` fields now have a Select button which opens a window listing all available prototypes of the appropriate type.
* added **IConfigurationManager**.*SubscribeMultiple* ext. method to provide simpler way to unsubscribe from multiple cvar at once
* Added `SharedMapSystem.QueueDeleteMap`, which deletes a map with the specified MapId in the next tick.
* Added generic version of `ComponentRegistry.TryGetComponent`.
* `AttributeHelper.HasAttribute` has had an overload's type signature loosened from `INamedTypeSymbol` to `ITypeSymbol`.
* Errors are now logged when sending messages to disconnected `INetChannel`s.
* Warnings are now logged if sending a message via Lidgren failed for some reason.
* `.yml` and `.ftl` files in the same directory are now concatenated onto each other, to reduce file count in packaged builds. This is done through the new `AssetPassMergeTextDirectories` pass.
* Added `System.Linq.ImmutableArrayExtensions` to sandbox.
* `ImmutableDictionary<TKey, TValue>` and `ImmutableHashSet<T>` can now be network serialized.
* `[AutoPausedField]` now works on fields of type `Dictionary<TKey, TimeSpan>`.
* `[NotYamlSerializable]` analyzer now detects nullable fields of the not-serializable type.
* `ItemList` items can now have a scale applied for the icon.
* Added new OS mouse cursor shapes for the SDL3 backend. These are not available on the GLFW backend.
* Added `IMidiRenderer.MinVolume` to scale the volume of MIDI notes.
* Added `SharedPhysicsSystem.ScaleFixtures`, to apply the physics-only changes of the prior `scale` command.
### Bugfixes
* `LayoutContainer.SetMarginsPreset` and `SetAnchorAndMarginPreset` now correctly use the provided control's top anchor when calculating the margins for its presets; it previously used the bottom anchor instead. This may result in a few UI differences, by a few pixels at most.
* `IConfigurationManager` no longer logs a warning when saving configuration in an integration test.
* Fixed impossible-to-source `ChannelClosedException`s when sending some net messages to disconnected `INetChannel`s.
* Fixed an edge case causing some color values to throw an error in `ColorNaming`.
* Fresh builds from specific projects should no longer cause errors related to `Robust.Client.Injectors` not being found.
* Stopped errors getting logged about `NoteOff` and `NoteOn` operations failing in MIDI.
* Fixed MIDI players not resuming properly when re-entering PVS range.
### Other
* Updated ImageSharp to 3.1.11 to stop the warning about a DoS vulnerability.
* Prototype YAML documents that are completely empty are now skipped by the prototype loader. Previously they would cause a load error for the whole file.
* `TileSpawnWindow` can now be localized.
* `BaseWindow` uses the new mouse cursor shapes for diagonal resizing.
* `NFluidsynth` has been updated to 0.2.0
### Internal
* Added `uitest` tab for standard mouse cursor shapes.
## 265.0.0
### Breaking changes
* More members in `IntegrationInstance` now enforce that the instance is idle before accessing it.
* `Prototype.ValidateDirectory` now requires that prototype IDs have no spaces or periods in them.
* `IPrototypeManager.TryIndex` no longer logs errors unless using the overload with an optional parameter. Use `Resolve()` instead if error logging is desired.
* `LocalizedCommands` now has a `Loc` property that refers to `LocalizationManager`. This can cause compile failures if you have static methods in child types that referenced static `Loc`.
* `[AutoGenerateComponentState]` now works on parent members for inherited classes. This can cause compile failures in certain formerly silently broken cases with overriden properties.
* `Vector3`, `Vector4`, `Quaternion`, and `Matrix4` have been removed from `Robust.Shared.Maths`. Use the `System.Numerics` types instead.
### New features
* `RobustClientPackaging.WriteClientResources()` and `RobustServerPackaging.WriteServerResources()` now have an overload taking in a set of things to ignore in the content resources directory.
* Added `IPrototypeManager.Resolve()`, which logs an error if the resolved prototype does not exist. This is effectively the previous (but not original) default behavior of `IPrototypeManager.TryIndex`.
* There's now a ViewVariables property editor for tuples.
* Added `ColorNaming` helper functions for getting textual descriptions of color values.
* Added Oklab/Oklch conversion functions for `Color`.
* `ColorSelectorSliders` now displays textual descriptions of color values.
* Added `TimeSpanExt.TryTimeSpan` to parse `TimeSpan`s with the `1.5h` format available in YAML.
* Added `ITestContextLike` and related classes to allow controlling pooled integration instances better.
* `EntProtoId` VV prop editors now don't allow setting invalid prototype IDs, inline with `ProtoId<T>`.
* Custom VV controls can now be registered using `IViewVariableControlFactory`.
* The entity spawn window now shows all placement modes registered with `IPlacementManager`.
* Added `VectorHelpers.InterpolateCubic` for `System.Numerics` `Vector3` and `Vector4`.
* Added deconstruct helpers for `System.Numerics` `Vector3` and `Vector4`.
### Bugfixes
* Pooled integration instances returned by `RobustIntegrationTest` are now treated as non-idle, for consistency with non-pooled startups.
* `SharedAudioSystem.SetState` no longer calls `DirtyField` on `PlaybackPosition`, an unnetworked field.
* Fix loading texture files from the root directory.
* Fix integration test pooling leaking non-reusable instances.
* Fix multiple bugs where VV displayed the wrong property editor for remote values.
* VV displays group headings again in member list.
* Fix a stack overflow that could occur with `ColorSelectorSliders`.
* `MidiRenderer` now properly handles `NoteOn` events with 0 velocity (which should actually be treated as `NoteOff` events).
### Other
* The debug assert for `RobustRandom.Next(TimeSpan, TimeSpan)` now allows for the two arguments to be equal.
* The configuration system will now report an error instead of warning if it fails to load the config file.
* Members in `IntegrationInstance` that enforce the instance is idle now always allow access from the instance's thread (e.g. from a callback).
* `IPrototypeManager` methods now have `[ForbidLiteral]` where appropriate.
* Performance improvements to physics system.
* `[ValidatePrototypeIdAttribute]` has been marked as obsolete.
* `ParallelManager` no longer cuts out exception information for caught job exceptions.
* Improved logging for PVS uninitialized/deleted entity errors.
### Internal
* General code & warning cleanup.
* Fix `VisibilityTest` being unreliable.
* `ColorSelectorSliders` has been internally refactored.
* Added CI workflows that test all RT build configurations.
## 264.0.0
### Breaking changes
* `IPrototypeManager.Index(Type kind, string id)` now throws `UnknownPrototypeException` instead of `KeyNotFoundException`, for consistency with `IPrototypeManager.Index<T>`.
### New features
* Types can now implement the new interface `IRobustCloneable<T>` to be cloned by the component state source generator.
* Added extra Roslyn Analyzers to detect some misuse of prototypes:
* Network serializing prototypes (tagging them with `[Serializable, NetSerializable]`).
* Constructing new instances of prototypes directly.
* Add `PrototypeManagerExt.Index` helper function that takes a nullable `ProtoId<T>`, returning null if the ID is null.
* Added an `AlwaysActive` field to `WebViewControl` to make a browser window active even when not in the UI tree.
* Made some common dependencies accessible through `IPlacementManager`.
* Added a new `GENITIVE()` localization helper function, which is useful for certain languages.
### Bugfixes
* Sprite scale is now correctly applied to sprite boundaries in `SpriteSystem.GetLocalBounds`.
* Fixed documentation for `IPrototypeManager.Index<T>` stating that `KeyNotFoundException` gets thrown, when in actuality `UnknownPrototypeException` gets thrown.
### Other
* More tiny optimizations to `DataDefinitionAnalyzer`.
* NetSerializer has been updated. On debug, it will now report *where* a type that can't be serialized is referenced from.
### Internal
* Minor internal code cleanup.
## 263.0.0
### Breaking changes
* Fully removed some non-`Entity<T>` container methods.
### New features
* `IMidiRenderer.LoadSoundfont` has been split into `LoadSoundfontResource` and `LoadSoundfontUser`, the original now being deprecated.
* Client command execution now properly catches errors instead of letting them bubble up through the input stack.
* Added `CompletionHelper.PrototypeIdsLimited` API to allow commands to autocomplete entity prototype IDs.
* Added `spawn:in` Toolshed command.
* Added `MapLoaderSystem.TryLoadGeneric` overload to load from a `Stream`.
* Added `OutputPanel.GetMessage()` and `OutputPanel.SetMessage()` to allow replacing individual messages.
### Bugfixes
* Fixed debug asserts when using MIDI on Windows.
* Fixed an error getting logged on startup on macOS related to window icons.
* `CC-BY-NC-ND-4.0` is now a valid license for the RGA validator.
* Fixed `TabContainer.CurrentTab` clamping against the wrong value.
* Fix culture-based parsing in `TimespanSerializer`.
* Fixed grid rendering blowing up on tile IDs that aren't registered.
* Fixed debug assert when loading MIDI soundfonts on Windows.
* Make `ColorSelectorSliders` properly update the dropdown when changing `SelectorType`.
* Fixed `tpto` allowing teleports to oneself, thereby causing them to be deleted.
* Fix OpenAL extensions being requested incorrectly, causing an error on macOS.
* Fixed horizontal measuring of markup controls in rich text.
### Other
* Improved logging for some audio entity errors.
* Avoided more server stutters when using `csci`.
* Improved physics performance.
* Made various localization functions like `GENDER()` not throw if passed a string instead of an `EntityUid`.
* The generic clause on `EntitySystem.AddComp<T>` has been changed to `IComponent` (from `Component`) for consistency with `IEntityManager.AddComponent<T>`.
* `DataDefinitionAnalyzer` has been optimized somewhat.
* Improved assert logging error message when static data fields are encountered.
### Internal
* Warning cleanup.
* Added more tests for `DataDefinitionAnalyzer`.
* Consistently use `EntitySystem` proxy methods in engine.
## 262.0.0
### Breaking changes
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
### New features
* `ToolshedManager.TryParse` now also supports nullable value types.
* Add an ignoredComponents arg to IsDefault.
### Bugfixes
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
* Fix windows not saving their positions correctly when their x position is <= 0.
* Fix transform state handling overriding PVS detachment.
## 261.2.1
## 261.2.0

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,6 +1,8 @@
## EntitySpawnWindow
entity-spawn-window-title = Entity Spawn Panel
entity-spawn-window-search-bar-placeholder = search
entity-spawn-window-clear-button = Clear
entity-spawn-window-replace-button-text = Replace
entity-spawn-window-override-menu-tooltip = Override placement
@@ -20,5 +22,3 @@ output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode
window-search-bar-placeholder = Search
window-clear-button = Clear

View File

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

View File

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

View File

@@ -195,8 +195,6 @@ command-description-spawn-at =
Spawns an entity at the given coordinates.
command-description-spawn-on =
Spawns an entity on the given entity, at it's coordinates.
command-description-spawn-in =
Spawns an entity in the given container on the given entity, dropping it at its coordinates if it doesn't fit
command-description-spawn-attached =
Spawns an entity attached to the given entity, at (0 0) relative to it.
command-description-mappos =
@@ -428,7 +426,3 @@ command-description-cmd-info =
On its own, this means it'll print the command's help message.
command-description-comp-rm =
Removes the given component from the entity.
command-description-overlay-toggle = Toggle an overlay on or off
command-description-overlay-add = Add an overlay (if it does not already exist)
command-description-overlay-remove = Remove an overlay

View File

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

View File

@@ -14,8 +14,6 @@ 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;
@@ -49,15 +47,8 @@ void fragment()
discard;
}
// 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);
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);
val *= lightPower;
val *= mask;

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ public sealed class DataDefinitionAnalyzerTest
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute(string? tag = null) : DataFieldBaseAttribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
public sealed class NotYamlSerializableAttribute : Attribute;
}
@@ -117,61 +117,6 @@ public sealed class DataDefinitionAnalyzerTest
);
}
[Test]
public async Task PartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed class Foo { }
""";
await Verifier(code,
// /0/Test0.cs(4,15): error RA0017: Type Foo is a DataDefinition but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataDefinitionPartialRule).WithSpan(4, 15, 4, 20).WithArguments("Foo")
);
}
[Test]
public async Task NestedPartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
public sealed class Foo
{
[DataDefinition]
public sealed partial class Nested { }
}
""";
await Verifier(code,
// /0/Test0.cs(3,15): error RA0018: Type Foo contains nested data definition Nested but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.NestedDataDefinitionPartialRule).WithSpan(3, 15, 3, 20).WithArguments("Foo", "Nested")
);
}
[Test]
public async Task RedundantDataFieldTagTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField("someValue")]
public int SomeValue;
}
""";
await Verifier(code,
// /0/Test0.cs(6,6): info RA0027: Data field SomeValue in data definition Foo has an explicitly set tag that matches autogenerated tag
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldRedundantTagRule).WithSpan(6, 6, 6, 28).WithArguments("SomeValue", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{
@@ -203,8 +148,6 @@ public sealed class DataDefinitionAnalyzerTest
[NotYamlSerializable]
public sealed class NotSerializableClass { }
[NotYamlSerializable]
public readonly struct NotSerializableStruct { }
[DataDefinition]
public sealed partial class Foo
@@ -215,21 +158,6 @@ public sealed class DataDefinitionAnalyzerTest
[DataField]
public NotSerializableClass BadProperty { get; set; }
[DataField]
public NotSerializableClass? BadNullableField;
[DataField]
public NotSerializableStruct BadStructField;
[DataField]
public NotSerializableStruct BadStructProperty { get; set; }
[DataField]
public NotSerializableStruct? BadNullableStructField;
[DataField]
public NotSerializableStruct? BadNullableStructProperty { get; set; }
public NotSerializableClass GoodField; // Not a DataField, not a problem
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
@@ -237,20 +165,10 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(12,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(12, 12, 12, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(15,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(15, 12, 15, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass"),
// /0/Test0.cs(18,12): error RA0036: Data field BadNullableField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(18, 12, 18, 33).WithArguments("BadNullableField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(21,12): error RA0036: Data field BadStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(21, 12, 21, 33).WithArguments("BadStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(24,12): error RA0036: Data field BadStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(24, 12, 24, 33).WithArguments("BadStructProperty", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(27,12): error RA0036: Data field BadNullableStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(27, 12, 27, 34).WithArguments("BadNullableStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(30,12): error RA0036: Data field BadNullableStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(30, 12, 30, 34).WithArguments("BadNullableStructProperty", "Foo", "NotSerializableStruct")
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private const string DataFieldAttributeName = "DataField";
private const string ViewVariablesAttributeName = "ViewVariables";
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
Diagnostics.IdDataDefinitionPartial,
"Type must be partial",
"Type {0} is a DataDefinition but is not partial",
@@ -32,7 +32,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type that is a data definition as partial."
);
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
Diagnostics.IdNestedDataDefinitionPartial,
"Type must be partial",
"Type {0} contains nested data definition {1} but is not partial",
@@ -62,7 +62,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to add a setter."
);
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
Diagnostics.IdDataFieldRedundantTag,
"Data field has redundant tag specified",
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
@@ -102,31 +102,23 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolStartAction(symbolContext =>
{
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
return;
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
if (!IsDataDefinition(typeSymbol))
return;
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}, SymbolKind.NamedType);
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}
private static void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
{
if (context.Node is not TypeDeclarationSyntax declaration)
return;
if (context.ContainingSymbol is not INamedTypeSymbol type)
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
if (!IsDataDefinition(type))
return;
if (!IsPartial(declaration))
@@ -137,7 +129,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
var containingType = type.ContainingType;
while (containingType != null)
{
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
var containingTypeDeclaration = (TypeDeclarationSyntax) containingType.DeclaringSyntaxReferences[0].GetSyntax();
if (!IsPartial(containingTypeDeclaration))
{
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
@@ -147,31 +139,32 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}
}
private static void AnalyzeDataField(SyntaxNodeAnalysisContext context)
private void AnalyzeDataField(SyntaxNodeAnalysisContext context)
{
if (context.Node is not FieldDeclarationSyntax field)
return;
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type))
return;
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
if (fieldSymbol == null)
continue;
if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute))
continue;
if (IsReadOnlyDataField(type, fieldSymbol))
{
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
if (HasRedundantTag(fieldSymbol))
{
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
@@ -186,8 +179,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
continue;
fieldTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(fieldTypeSymbol);
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
@@ -200,33 +191,30 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}
}
private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
private void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
{
if (context.Node is not PropertyDeclarationSyntax property)
return;
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
return;
if (type.IsRecord || type.IsValueType)
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type) || type.IsRecord || type.IsValueType)
return;
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(property);
if (propertySymbol == null)
return;
if (!IsDataField(propertySymbol, out _, out var datafieldAttribute))
return;
if (IsReadOnlyDataField(type, propertySymbol))
{
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol, datafieldAttribute))
if (HasRedundantTag(propertySymbol))
{
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
@@ -241,8 +229,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
return;
propertyTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(propertyTypeSymbol);
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
@@ -256,6 +242,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
{
if (!IsDataField(field, out _, out _))
return false;
return IsReadOnlyMember(type, field);
}
@@ -380,14 +369,17 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
private static bool HasRedundantTag(ISymbol symbol)
{
if (!IsDataField(symbol, out var _, out var attribute))
return false;
// No args, no problem
if (datafieldAttribute.ConstructorArguments.Length == 0)
if (attribute.ConstructorArguments.Length == 0)
return false;
// If a tag is explicitly specified, it will be the first argument...
var tagArgument = datafieldAttribute.ConstructorArguments[0];
var tagArgument = attribute.ConstructorArguments[0];
// ...but the first arg could also something else, since tag is optional
// so we make sure that it's a string
if (tagArgument.Value is not string explicitName)
@@ -402,6 +394,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool HasVVReadWrite(ISymbol symbol)
{
if (!IsDataField(symbol, out _, out _))
return false;
// Make sure it has ViewVariablesAttribute
AttributeData? viewVariablesAttribute = null;
foreach (var attr in symbol.GetAttributes())
@@ -427,6 +422,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
{
if (!IsDataField(field, out _, out _))
return false;
return HasAttribute(type, NotYamlSerializableName);
}

View File

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

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
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,9 +61,12 @@ namespace Robust.Client.WebView.Cef
if (cefResourcesPath == null)
throw new InvalidOperationException("Unable to locate cef_resources directory!");
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
var cachePath = FindAndLockCacheDirectory();
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{
@@ -73,7 +76,7 @@ namespace Robust.Client.WebView.Cef
BrowserSubprocessPath = subProcessPath,
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
ResourcesDirPath = cefResourcesPath,
RemoteDebuggingPort = remoteDebugPort,
RemoteDebuggingPort = 9222,
CookieableSchemesList = "usr,res",
CachePath = cachePath,
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Animations;
using Robust.Shared.Maths;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Animations
{
@@ -54,14 +56,8 @@ namespace Robust.Client.Animations
}
else
{
var next = KeyFrames[nextKeyFrame];
// Get us a scale 0 -> 1 here.
var t = playingTime / next.KeyTime;
// Apply easing to time parameter, if one was specified
if (next.Easing != null)
t = next.Easing(t);
var t = playingTime / KeyFrames[nextKeyFrame].KeyTime;
switch (InterpolationMode)
{
@@ -126,9 +122,9 @@ namespace Robust.Client.Animations
case Vector2 vector2:
return Vector2Helpers.InterpolateCubic((Vector2) preA, vector2, (Vector2) b, (Vector2) postB, t);
case Vector3 vector3:
return VectorHelpers.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
return Vector3.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
case Vector4 vector4:
return VectorHelpers.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
return Vector4.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
case float f:
return MathHelper.InterpolateCubic((float) preA, f, (float) b, (float) postB, t);
case double d:
@@ -153,20 +149,10 @@ namespace Robust.Client.Animations
/// </summary>
public readonly 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)
public KeyFrame(object value, float keyTime)
{
Value = value;
KeyTime = keyTime;
Easing = easing;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Client.ResourceManagement;
using Robust.Shared;
@@ -56,8 +57,8 @@ internal sealed partial class AudioManager : IAudioInternal
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
@@ -144,7 +145,7 @@ internal sealed partial class AudioManager : IAudioInternal
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0)
ALC.EFX.DeleteFilter(handles.filterHandle);
EFX.DeleteFilter(handles.filterHandle);
}
private void _checkAlcError(ALDevice device,

View File

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

View File

@@ -372,13 +372,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
var parentUid = xform.ParentUid;
Vector2 worldPos;
component.Volume = component.Params.Volume;
// Handle grid audio differently by using grid position.
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
var parentUid = xform.ParentUid;
worldPos = _maps.GetGridPosition(parentUid);
}
else
@@ -412,7 +412,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
else
{
var occlusion = GetOcclusion(listener, delta, distance, parentUid);
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
}
@@ -420,11 +420,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
component.Position = worldPos;
// Make race cars go NYYEEOOOOOMMMMM
if (_physicsQuery.TryGetComponent(parentUid, out var physicsComp))
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(parentUid, physicsComp);
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
component.Velocity = velocity;
}
}
@@ -582,18 +582,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(entity))
{
LogAudioPlaybackOnInvalidEntity(specifier, entity);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
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;
}
@@ -631,16 +626,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
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;
}
@@ -723,6 +714,8 @@ 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);
}
@@ -760,12 +753,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
{
var soundInfo = specifier?.ToString() ?? "unknown sound";
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
}
#region Jobs
private record struct UpdateAudioJob : IParallelRobustJob

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiRenderer : IMidiRenderer
internal sealed class MidiRenderer : IMidiRenderer
{
private readonly IMidiManager _midiManager;
private readonly ITaskManager _taskManager;
@@ -214,11 +214,6 @@ internal sealed partial class MidiRenderer : IMidiRenderer
[ViewVariables]
public BitArray FilteredChannels { get; } = new(RobustMidiEvent.MaxChannels);
[ViewVariables]
public byte MinVolume { get => _minVolume; set => _minVolume = value; }
private byte _minVolume;
[ViewVariables(VVAccess.ReadWrite)]
public byte? VelocityOverride { get; set; } = null;
@@ -440,6 +435,15 @@ internal sealed partial class MidiRenderer : IMidiRenderer
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
}
public void LoadSoundfont(string filename, bool resetPresets = true)
{
lock (_playerStateLock)
{
_synth.LoadSoundFont(filename, resetPresets);
MidiSoundfont = 1;
}
}
void IMidiRenderer.Render()
{
Render();
@@ -544,7 +548,14 @@ internal sealed partial class MidiRenderer : IMidiRenderer
if (velocity <= 0)
continue;
_synth.TryNoteOn(channel, key, velocity);
try
{
_synth.NoteOn(channel, key, velocity);
}
catch (FluidSynthInteropException e)
{
_midiSawmill.Error($"CH:{channel} KEY:{key} VEL:{velocity} {e.ToStringBetter()}");
}
}
}
@@ -572,32 +583,19 @@ internal sealed partial class MidiRenderer : IMidiRenderer
{
case RobustMidiCommand.NoteOff:
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.TryNoteOff(midiEvent.Channel, midiEvent.Key);
_synth.NoteOff(midiEvent.Channel, midiEvent.Key);
break;
case RobustMidiCommand.NoteOn:
// Velocity 0 *can* represent a NoteOff event.
var velocity = midiEvent.Velocity;
if (velocity == 0)
{
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
}
if (FilteredChannels[midiEvent.Channel])
break;
if (MinVolume > 0)
velocity = (byte)Math.Floor(MathHelper.Lerp(MinVolume, 127, (float)velocity / 127));
velocity = VelocityOverride ?? velocity;
var velocity = VelocityOverride ?? midiEvent.Velocity;
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
case RobustMidiCommand.AfterTouch:
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = midiEvent.Value;
_synth.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);

View File

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

View File

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

View File

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

View File

@@ -144,7 +144,6 @@ namespace Robust.Client
deps.Register<IViewVariablesManager, ClientViewVariablesManager>();
deps.Register<IClientViewVariablesManager, ClientViewVariablesManager>();
deps.Register<IClientViewVariablesManagerInternal, ClientViewVariablesManager>();
deps.Register<IViewVariableControlFactory, ViewVariableControlFactory>();
deps.Register<IClientConGroupController, ClientConGroupController>();
deps.Register<IScriptClient, ScriptClient>();
deps.Register<IRobustSerializer, ClientRobustSerializer>();

View File

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

View File

@@ -21,7 +21,7 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}

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 || !NetManager.IsConnected)
if ((flags & CVar.REPLICATED) == 0)
return;
var msg = new MsgConVars();

View File

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

View File

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

View File

@@ -1,124 +0,0 @@
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,6 +4,8 @@ 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;
@@ -42,10 +44,7 @@ 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
{
ToolTip = "This button has a tooltip. Spooky!"
};
var optionButton = new OptionButton();
optionButton.AddItem("Honk");
optionButton.AddItem("Foo");
optionButton.AddItem("Bar");
@@ -155,8 +154,6 @@ 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()
@@ -213,53 +210,6 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
return label;
}
private Control TabCursorShapes()
{
var box = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
};
var styleBox = new StyleBoxFlat
{
BackgroundColor = Color.Black
};
foreach (var cursorName in Enum.GetNames<CursorShape>())
{
// Go over names due to duplicate definitions in the enum.
var cursor = Enum.Parse<CursorShape>(cursorName);
// Wow was I bad at API design.
if (cursor == CursorShape.Custom)
continue;
var panel = new PanelContainer
{
PanelOverride = styleBox,
DefaultCursorShape = cursor,
MouseFilter = MouseFilterMode.Stop,
MinHeight = 30,
Children =
{
new Label
{
Text = cursorName,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(4)
}
}
};
box.AddChild(panel);
}
return new ScrollContainer
{
Children = { box },
VScrollEnabled = true,
HScrollEnabled = false,
Name = nameof(Tab.TabCursorShapes),
};
}
public void SelectTab(Tab tab)
{
_tabContainer.CurrentTab = (int)tab;
@@ -276,14 +226,32 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
TextEdit = 6,
RichText = 7,
SpriteView = 8,
TabCursorShapes = 9,
WrapContainer = 10,
}
}
internal abstract class BaseUITestCommand : LocalizedCommands
internal sealed class UITestCommand : LocalizedCommands
{
public sealed override void Execute(IConsoleShell shell, string argStr, string[] args)
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)
{
if (args.Length > 1)
{
@@ -304,10 +272,18 @@ internal abstract class BaseUITestCommand : LocalizedCommands
control.SelectTab(tab);
}
CreateWindow(control);
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);
}
public sealed override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
@@ -318,35 +294,4 @@ internal abstract class BaseUITestCommand : LocalizedCommands
return CompletionResult.Empty;
}
protected abstract void CreateWindow(UITestControl control);
}
internal sealed class UITestCommand : BaseUITestCommand
{
public override string Command => "uitest";
protected override void CreateWindow(UITestControl control)
{
var window = new DefaultWindow { MinSize = new(800, 600) };
window.OnClose += control.OnClosed;
window.Contents.AddChild(control);
window.OpenCentered();
}
}
internal sealed class UITest2Command : BaseUITestCommand
{
public override string Command => "uitest2";
protected override void CreateWindow(UITestControl control)
{
var window = new OSWindow
{
Title = Loc.GetString("cmd-uitest2-title"),
};
window.AddChild(control);
window.Closed += control.OnClosed;
window.Show();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SDL3;
namespace Robust.Client
{
@@ -94,8 +93,6 @@ 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,8 +110,6 @@ namespace Robust.Client
private ResourceManifestData? _resourceManifest;
private DisplayMode _displayMode;
public void SetCommandLineArgs(CommandLineArgs args)
{
_commandLineArgs = args;
@@ -275,9 +273,6 @@ namespace Robust.Client
}
};
_configurationManager.OnValueChanged(CVars.DisplayMaxFPS, _ => UpdateVsyncConfig());
_configurationManager.OnValueChanged(CVars.DisplayVSync, _ => UpdateVsyncConfig(), invokeImmediately: true);
_clyde.Ready();
if (_resourceManifest!.AutoConnect &&
@@ -714,30 +709,6 @@ 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,9 +29,6 @@ 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();
@@ -216,34 +213,6 @@ 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);
}
@@ -348,23 +317,18 @@ namespace Robust.Client.GameObjects
}
}
public override bool IsQueuedForDeletion(EntityUid uid)
=> QueuedDeletionsSet.Contains(uid) || _queuedPredictedDeletions.Contains(uid);
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?> ent)
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
// Some UIs get disposed after entity-manager has shut down and already deleted all entities.
if (!Started)
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
if (IsQueuedForDeletion(ent.Owner))
return;
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
if (ent.Comp.NetEntity.IsClientSide())
if (ent.Comp1.NetEntity.IsClientSide())
{
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
// base call to skip that.
@@ -373,10 +337,7 @@ namespace Robust.Client.GameObjects
}
else
{
if (!_queuedPredictedDeletionsSet.Add(ent.Owner))
return;
_queuedPredictedDeletions.Enqueue(ent.Owner);
_xforms.DetachEntity(ent.Owner, ent.Comp2);
}
}
}

View File

@@ -30,6 +30,8 @@ using Robust.Shared.ViewVariables;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
using Vector4 = Robust.Shared.Maths.Vector4;
using SysVec4 = System.Numerics.Vector4;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Robust.Client.GameObjects
@@ -294,16 +296,6 @@ 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.
@@ -611,7 +603,6 @@ 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;
@@ -1168,15 +1159,6 @@ 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;
@@ -1242,8 +1224,6 @@ namespace Robust.Client.GameObjects
return;
_visible = value;
Owner.Comp.BoundsDirty = true;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (_parent.Owner != EntityUid.Invalid)
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
@@ -1356,8 +1336,6 @@ 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);
}
@@ -1685,25 +1663,17 @@ 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;
@@ -1721,17 +1691,9 @@ 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;
@@ -1749,7 +1711,6 @@ namespace Robust.Client.GameObjects
AnimationTimeLeft += state.GetDelay(AnimationFrame);
}
}
}
/// <summary>
@@ -1830,15 +1791,76 @@ namespace Robust.Client.GameObjects
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeTextures(prototype, out noRot);
var results = new List<IDirectionalTextureProvider>();
noRot = false;
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
results.Add(sys.GetIcon(icon));
return results;
}
if (!prototype.Components.TryGetValue("Sprite", out _))
{
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible) continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid) continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null ||
!rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
entityManager.DeleteEntity(dummy);
if (results.Count == 0)
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeIcon(prototype);
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
return sys.GetIcon(icon);
if (!prototype.Components.ContainsKey("Sprite"))
return sys.GetFallbackState();
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? sys.GetFallbackState();
entityManager.DeleteEntity(dummy);
return result;
}
}
}

View File

@@ -97,7 +97,7 @@ namespace Robust.Client.GameObjects
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EnsureComp<AnimationPlayerComponent>(uid);
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
@@ -158,7 +158,7 @@ namespace Robust.Client.GameObjects
public bool HasRunningAnimation(EntityUid uid, string key)
{
return TryComp(uid, out AnimationPlayerComponent? component) &&
return EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? component) &&
component.PlayingAnimations.ContainsKey(key);
}

View File

@@ -223,12 +223,12 @@ namespace Robust.Client.GameObjects
private void SetEntityContextActive(IInputManager inputMan, EntityUid entity)
{
if(entity == default || !Exists(entity))
if(entity == default || !EntityManager.EntityExists(entity))
throw new ArgumentNullException(nameof(entity));
if (!TryComp(entity, out InputComponent? inputComp))
if (!EntityManager.TryGetComponent(entity, out InputComponent? inputComp))
{
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
return;
}
@@ -239,7 +239,7 @@ namespace Robust.Client.GameObjects
}
else
{
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
}
}

View File

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

View File

@@ -29,8 +29,6 @@ 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

@@ -0,0 +1,25 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
public sealed class ScaleVisualsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(EntityUid uid, ScaleVisualsComponent component, ref AppearanceChangeEvent ev)
{
if (!ev.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
ev.Sprite == null) return;
var vecScale = (Vector2)scale;
// Set it directly because prediction may call this multiple times.
ev.Sprite.Scale = vecScale;
}
}

View File

@@ -51,7 +51,7 @@ public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
var player = _playerManager.LocalEntity;
if (player == null || !TryComp(player.Value, out PhysicsComponent? body))
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;

View File

@@ -32,7 +32,7 @@ public sealed partial class SpriteSystem
bounds = bounds.Union(GetLocalBounds(layer));
}
sprite.Comp._bounds = bounds.Scale(sprite.Comp.Scale);
sprite.Comp._bounds = bounds;
sprite.Comp.BoundsDirty = false;
return sprite.Comp._bounds;
}

View File

@@ -88,7 +88,6 @@ 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

@@ -56,6 +56,10 @@ public sealed partial class SpriteSystem
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
@@ -63,7 +67,11 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
return GetPrototypeIcon(entityPrototype);
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
@@ -71,19 +79,13 @@ public sealed partial class SpriteSystem
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
return cachedResult;
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
}
private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
@@ -100,63 +102,6 @@ public sealed partial class SpriteSystem
return result;
}
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
GetPrototypeTextures(proto, out _);
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
if (proto.TryGetComponent(out IconComponent? icon, _factory))
{
results.Add(GetIcon(icon));
return results;
}
if (!proto.Components.ContainsKey("Sprite"))
{
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
// TODO SPRITE is this needed?
// And if it is, shouldn't GetPrototypeIconInternal also use this?
_appearance.OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible)
continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid)
continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
Del(dummy);
if (results.Count == 0)
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[Pure]
public RSI.State GetFallbackState()
{

View File

@@ -7,6 +7,8 @@ using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
using Vector4 = Robust.Shared.Maths.Vector4;
using SysVec4 = System.Numerics.Vector4;
namespace Robust.Client.GameObjects;
@@ -155,7 +157,7 @@ public sealed partial class SpriteSystem
// Negative color modulation values are by the default shader to disable light shading.
// Specifically we set colour = - 1 - colour
// This is good enough to ensure that non-negative values become negative & is trivially invertible.
layerColor = new(new Vector4(-1) - layerColor.RGBA);
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
}
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);

View File

@@ -34,12 +34,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();

View File

@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
if (compState != null)
{
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);

View File

@@ -220,34 +220,24 @@ Had full state: {LastFullState != null}"
{
var compState = change.State;
if (compState is not IComponentDeltaState delta)
if (compState is IComponentDeltaState delta
&& compData.TryGetValue(change.NetID, out var old)) // May fail if relying on implicit data
{
compData[change.NetID] = compState;
continue;
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");
}
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");
compData[change.NetID] = compState;
}
if (entityState.NetComponents == null)

View File

@@ -13,8 +13,6 @@ namespace Robust.Client.GameStates
{
internal sealed class NetInterpOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -34,7 +32,7 @@ namespace Robust.Client.GameStates
{
IoCManager.InjectDependencies(this);
_lookup = lookup;
_shader = _prototypeManager.Index(UnshadedShader).Instance();
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
_container = _entityManager.System<SharedContainerSystem>();
_xform = _entityManager.System<SharedTransformSystem>();
}

View File

@@ -1,5 +1,6 @@
using System;
using Robust.Shared;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.Clyde
{

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
@@ -255,9 +254,9 @@ namespace Robust.Client.Graphics.Clyde
region = regionMaybe[tile.Variant];
}
var rotationMirroring = (_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef) && tileDef.AllowRotationMirror) ?
tile.RotationMirroring
: 0;
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
i += 1;

View File

@@ -121,19 +121,6 @@ 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

@@ -4,6 +4,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using Robust.Shared.Maths;
using Vector3 = Robust.Shared.Maths.Vector3;
namespace Robust.Client.Graphics.Clyde
{

View File

@@ -18,6 +18,7 @@ using Robust.Shared.Graphics;
using static Robust.Shared.GameObjects.OccluderComponent;
using Robust.Shared.Utility;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
{
@@ -451,8 +452,6 @@ 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"))
@@ -506,18 +505,6 @@ 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,7 +209,6 @@ 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,
@@ -221,11 +220,10 @@ 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;
}
@@ -303,22 +301,10 @@ namespace Robust.Client.Graphics.Clyde
}
}
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
private sealed class LoadedRenderTarget
{
public bool IsWindow;
public WindowId WindowId;
public string? Name;
public Vector2i Size;
public bool IsSrgb;
@@ -339,11 +325,9 @@ namespace Robust.Client.Graphics.Clyde
public long MemoryPressure;
public TextureSampleParameters? SampleParameters;
public required WeakReference<RenderTargetBase> Instance;
}
internal abstract class RenderTargetBase : IRenderTarget
private abstract class RenderTargetBase : IRenderTarget
{
protected readonly Clyde Clyde;
private bool _disposed;
@@ -405,7 +389,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
internal sealed class RenderTexture : RenderTargetBase, IRenderTexture
private sealed class RenderTexture : RenderTargetBase, IRenderTexture
{
public RenderTexture(Vector2i size, ClydeTexture texture, Clyde clyde, ClydeHandle handle)
: base(clyde, handle)

View File

@@ -11,6 +11,8 @@ using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
{
@@ -539,7 +541,7 @@ namespace Robust.Client.Graphics.Clyde
case Matrix3x2 matrix3:
program.SetUniform(name, matrix3);
break;
case Matrix4x4 matrix4:
case Matrix4 matrix4:
program.SetUniform(name, matrix4);
break;
case ClydeTexture clydeTexture:

View File

@@ -10,6 +10,8 @@ using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
{
@@ -526,7 +528,7 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, in Matrix4x4 value)
private protected override void SetParameterImpl(string name, in Matrix4 value)
{
var data = Parent._shaderInstances[Handle];
data.ParametersDirty = true;

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
@@ -16,14 +15,10 @@ 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(_nextViewportId++, handle, name, this)
var viewport = new Viewport(handle, name, this)
{
Size = size,
RenderTarget = CreateRenderTarget(size,
@@ -64,43 +59,28 @@ namespace Robust.Client.Graphics.Clyde
private void FlushViewportDispose()
{
while (_viewportDisposeQueue.TryDequeue(out var data))
// Free of allocations unless a dead viewport is found.
List<ClydeHandle>? toRemove = null;
foreach (var (handle, viewportRef) in _viewports)
{
DisposeViewport(data);
if (!viewportRef.TryGetTarget(out _))
{
toRemove ??= new List<ClydeHandle>();
toRemove.Add(handle);
}
}
}
private void DisposeViewport(ViewportDisposeData disposeData)
{
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
_viewports.Remove(disposeData.Handle);
if (disposeData.ClearEvent is not { } clearEvent)
if (toRemove == null)
{
return;
try
{
clearEvent(disposeData.ClearEventData);
}
catch (Exception ex)
foreach (var remove in toRemove)
{
_clydeSawmill.Error($"Caught exception while disposing viewport: {ex}");
_viewports.Remove(remove);
}
}
#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;
@@ -126,20 +106,17 @@ namespace Robust.Client.Graphics.Clyde
public string? Name { get; }
public Viewport(long id, ClydeHandle handle, string? name, Clyde clyde)
public Viewport(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()
{
@@ -209,56 +186,20 @@ 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.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));
_clyde._viewports.Remove(_handle);
}
IRenderTexture IClydeViewport.RenderTarget => RenderTarget;
IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget;
public IEye? Eye { get; set; }
}
private sealed class ViewportDisposeData
{
public ClydeHandle Handle;
public long Id;
public Action<ClearCachedViewportResourcesEvent>? ClearEvent;
public ClearCachedViewportResourcesEvent ClearEventData;
}
}
}

View File

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

View File

@@ -114,6 +114,7 @@ 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);
@@ -127,11 +128,7 @@ namespace Robust.Client.Graphics.Clyde
// macOS cannot.
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
_cfg.OverrideDefault(CVars.DisplayThreadWindowApi, true);
#if MACOS
// Trust macOS to not need threaded window blitting.
// (threaded window blitting is a workaround to avoid having to frequently MakeCurrent() on Windows, as it is broken).
_cfg.OverrideDefault(CVars.DisplayThreadWindowBlit, false);
#endif
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
_threadWindowApi = _cfg.GetCVar(CVars.DisplayThreadWindowApi);

View File

@@ -17,6 +17,8 @@ using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
{
@@ -34,7 +36,6 @@ 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)
{
@@ -76,11 +77,6 @@ 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;
@@ -246,7 +242,7 @@ namespace Robust.Client.Graphics.Clyde
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
string? name = null)
{
return new Viewport(_nextViewportId++, size);
return new Viewport(size);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
@@ -313,19 +309,6 @@ 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()
@@ -415,7 +398,7 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, in Matrix4x4 value)
private protected override void SetParameterImpl(string name, in Matrix4 value)
{
}
@@ -501,19 +484,15 @@ namespace Robust.Client.Graphics.Clyde
private sealed class Viewport : IClydeViewport
{
public Viewport(long id, Vector2i size)
public Viewport(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));
@@ -522,7 +501,6 @@ namespace Robust.Client.Graphics.Clyde
public IEye? Eye { get; set; }
public Vector2i Size { get; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public Vector2 RenderScale { get; set; }
public bool AutomaticRender { get; set; }

View File

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

View File

@@ -5,6 +5,8 @@ using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Vector3 = Robust.Shared.Maths.Vector3;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
{
@@ -275,20 +277,20 @@ namespace Robust.Client.Graphics.Clyde
_clyde.CheckGlError();
}
public void SetUniform(string uniformName, in Matrix4x4 matrix, bool transpose=true)
public void SetUniform(string uniformName, in Matrix4 matrix, bool transpose=true)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix, transpose);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void SetUniformDirect(int uniformId, in Matrix4x4 value, bool transpose=true)
private unsafe void SetUniformDirect(int uniformId, in Matrix4 value, bool transpose=true)
{
Matrix4x4 tmpTranspose = value;
Matrix4 tmpTranspose = value;
if (transpose)
{
// transposition not supported on GLES2, & no access to _hasGLES
tmpTranspose = Matrix4x4.Transpose(value);
tmpTranspose.Transpose();
}
GL.UniformMatrix4(uniformId, 1, false, (float*) &tmpTranspose);
_clyde.CheckGlError();
@@ -549,7 +551,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniformMaybe(string uniformName, in Matrix4x4 value, bool transpose=true)
public void SetUniformMaybe(string uniformName, in Matrix4 value, bool transpose=true)
{
if (TryGetUniform(uniformName, out var slot))
{

View File

@@ -128,19 +128,6 @@ namespace Robust.Client.Graphics.Clyde
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.Progress, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.NWSEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NESWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.Move, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NotAllowed, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.NWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.NEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.EResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.SEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.SResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.SWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.WResize, CursorShape.HResize);
}
private sealed class CursorImpl : ICursor

View File

@@ -662,10 +662,6 @@ namespace Robust.Client.Graphics.Clyde
{
var icons = _clyde.LoadWindowIcons().ToArray();
// Done if no icon (e.g., macOS)
if (icons.Length == 0)
return;
// Turn each image into a byte[] so we can actually pin their contents.
// Wish I knew a clean way to do this without allocations.
var images = icons

View File

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

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

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