Compare commits

...

50 Commits

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

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

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

Does not include test code to avoid risking merge conflicts.

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

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

* Fixes

* Better docs

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

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

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

* Make it look better!

* I forgor xD

* Fix test fails

* Add comment

* Removed unused import

* Only write to file if the number of sections changed

* Servers can now have their own settings

* Minor optionzation and rare colors

* Remove some of the cvars

* debug only loading messages

* Added a few more steps

* Only one section at a time

* nullable section name

* Lock out functions if finished

* Get rid of saving the ccvar

* Cleanup

* Forgot!

* A few tweaks

* Disable vsync

* remove colors

* remove outdated vsync functions

* Silly me xD

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

* Better seconds display

* Simplify drawing logic + it looks better

* Type does not need to be partial

* Make interface to expose to content

* Use correct define to gate showing debug info

Should be TOOLS instead of DEBUG

* Use appropriate exception type in BeginLoadingSection

* Fix exception when closing window during loading screen

Would try to stop the main loop before it exists.

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

* Add to RELEASE-NOTES.md

* Add UI scaling support

* Make ILoadingScreenManager fully internal

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

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

* Add command to show top load time usage.

* Improve verbosity of debug time tracking

More steps and some steps named better

---------

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

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

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

Initial implementation is only for Windows DirectWrite.

* Load system fonts as memory mapped files if possible.

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

* Use ArrayPool to reduce char array allocations

* Disable verbose logging

* Implement system font support on Linux via Fontconfig

* Implement macOS support

* Add "FREEDESKTOP" define constant

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

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

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

* Allow disabling system font support via CVar

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

* make ComponentNameSerializer ignore ignored components

---------

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

* typo

---------

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

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

Webedit ops

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

* A

* update test

* Revert "update test"

This reverts commit 37f4da67fc.

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

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

* fix

* Fix overlays

* a

* comments

* comments

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

* forgot

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

* also outdated

* reduce branches in QueueTreeUpdate

* remove update hashset

* try fix

* Use Entity<T> and add ray overloads

* Move InRangeUnoccluded to engine

* reduce code duplication

* move _initialized check

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

* a

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

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

* :godmode:

* Fix comment

* fix

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

* Delete NetCoordinatesSerializer.cs

* Make EntityCoordinates and MapCoordinates use DataRecord

* Turn them into actual record structs

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

* Allocate MapIds before deserializing components

* Deserialize preallocated ids

* fix map merge assert

* remove old

* Use TryGetMap

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

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

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

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:38 +02:00
105 changed files with 4760 additions and 794 deletions

View File

@@ -47,8 +47,8 @@
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.9.4" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.3.0" />
<PackageVersion Include="Robust.Natives" Version="0.2.1" />
<PackageVersion Include="Robust.Natives.Zstd" Version="0.1.0-zstd1.5.7" />
<PackageVersion Include="Robust.Natives" Version="0.2.3" />
<PackageVersion Include="Robust.Natives.Zstd" Version="0.1.1-zstd1.5.7" />
<PackageVersion Include="Robust.Natives.Cef" Version="131.3.5" />
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
@@ -62,6 +62,7 @@
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />
<PackageVersion Include="libsodium" Version="1.0.20.1" />
<PackageVersion Include="System.Management" Version="9.0.8" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,76 @@ END TEMPLATE-->
*None yet*
## 267.4.2
## 267.4.1
## 267.4.0
### New features
* Added two new custom yaml serializers `CustomListSerializer` and `CustomArraySerializer`.
* CVars defined in `[CVarDefs]` can now be private or internal.
* Added config rollback system to `IConfigurationManager`. This enables CVars to be snapshot and rolled back, even in the event of client crash.
* `OptionButton` now has a `Filterable` property that gives it a text box to filter options.
* Added `FontTagHijackHolder` to replace fonts resolved by `FontTag`.
* Sandbox:
* Exposed `System.Reflection.Metadata.MetadataUpdateHandlerAttribute`.
* Exposed more overloads on `StringBuilder`.
* The engine can now load system fonts.
* At the moment only available on Windows.
* See `ISystemFontManager` for API.
* The client now display a loading screen during startup.
### Bugfixes
* Fix `Menu` and `NumpadDecimal` key codes on SDL3.
* client-side predicted entity deletion ( `EntityManager.PredictedQueueDeleteEntity`) now behaves more like it does on the server. In particular, entities will be deleted on the same tick after all system have been updated. Previously, it would process deletions at the beginning of the next tick.
* Fix modifying `Label.FontOverride` not causing a layout update.
* Controls created by rich-text tags now get arranged to a proper size.
* Fix `OutputPanel` scrollbar breaking if a style update changes the font size.
### Other
* ComponentNameSerializer will now ignore any components that have been ignored via `IComponentFactory.RegisterIgnore`.
* Add pure to some SharedTransformSystem methods.
* Significantly optimised collision detection in SharedBroadphaseSystem.
* `Control.Stylesheet` does not do any work if assigning the value it already has.
* XAML hot reload now JITs UIs when first opened rather than doing every single one at client startup. This reduces dev startup overhead significantly and probably helps with memory usage too.
### Internal
* The `dmetamem` command now sorts its output, and doesn't output to log anymore to avoid output interleaving.
## 267.3.0
### 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.
### 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
@@ -63,7 +133,7 @@ END TEMPLATE-->
### 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.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

View File

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

View File

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

View File

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

View File

@@ -428,3 +428,7 @@ command-description-cmd-info =
On its own, this means it'll print the command's help message.
command-description-comp-rm =
Removes the given component from the entity.
command-description-overlay-toggle = Toggle an overlay on or off
command-description-overlay-add = Add an overlay (if it does not already exist)
command-description-overlay-remove = Remove an overlay

View File

@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Graphics.FontManagement;
using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Localization;
@@ -67,6 +68,7 @@ namespace Robust.Client
deps.Register<IMapManagerInternal, NetworkedMapManager>();
deps.Register<INetworkedMapManager, NetworkedMapManager>();
deps.Register<IEntityManager, ClientEntityManager>();
deps.Register<FontTagHijackHolder>();
deps.Register<IReflectionManager, ClientReflectionManager>();
deps.Register<IConsoleHost, ClientConsoleHost>();
deps.Register<IClientConsoleHost, ClientConsoleHost>();
@@ -108,6 +110,8 @@ namespace Robust.Client
deps.Register<IReloadManager, ReloadManager>();
deps.Register<ILocalizationManager, ClientLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
deps.Register<LoadingScreenManager>();
deps.Register<ILoadingScreenManager, LoadingScreenManager>();
switch (mode)
{
@@ -120,6 +124,8 @@ namespace Robust.Client
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
deps.Register<ISystemFontManager, SystemFontManagerFallback>();
deps.Register<ISystemFontManagerInternal, SystemFontManagerFallback>();
break;
case GameController.DisplayMode.Clyde:
deps.Register<IClyde, Clyde>();
@@ -130,6 +136,8 @@ namespace Robust.Client
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();
deps.Register<ISystemFontManager, SystemFontManager>();
deps.Register<ISystemFontManagerInternal, SystemFontManager>();
break;
default:
throw new ArgumentOutOfRangeException();

View File

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

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
@@ -19,10 +20,28 @@ namespace Robust.Client.Console.Commands
return;
}
foreach (var sig in AssemblyTypeChecker.DumpMetaMembers(type))
var members = AssemblyTypeChecker.DumpMetaMembers(type)
.GroupBy(x => x.IsField)
.ToDictionary(x => x.Key, x => x.Select(t => t.Value).ToList());
if (members.TryGetValue(true, out var fields))
{
System.Console.WriteLine(@$"- ""{sig}""");
shell.WriteLine(sig);
fields.Sort(StringComparer.Ordinal);
foreach (var member in fields)
{
System.Console.WriteLine(@$"- ""{member}""");
}
}
if (members.TryGetValue(false, out var methods))
{
methods.Sort(StringComparer.Ordinal);
foreach (var member in methods)
{
System.Console.WriteLine(@$"- ""{member}""");
}
}
}

View File

@@ -3,6 +3,7 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Console;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -207,7 +208,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
private Control TabRichText()
{
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkupOrThrow(Lipsum));
var msg = FormattedMessage.FromMarkupOrThrow(Lipsum);
msg.AddMarkupOrThrow("\n\nWAWWAAWAWWAWA [cmdlink=\"DOES IT WORK\" command=\"help\" /] DOES IT WORK");
label.SetMessage(msg, [typeof(CommandLinkTag)]);
TabContainer.SetTabTitle(label, "RichText");
return label;

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ namespace Robust.Client
internal partial class GameController : IPostInjectInit
{
private IGameLoop? _mainLoop;
private bool _dontStart;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
@@ -162,8 +163,11 @@ namespace Robust.Client
return;
}
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
if (!_dontStart)
{
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
}
CleanupGameThread();
}

View File

@@ -96,6 +96,8 @@ namespace Robust.Client
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
[Dependency] private readonly LoadingScreenManager _loadscr = default!;
private IWebViewManagerHook? _webViewHook;
@@ -132,27 +134,39 @@ namespace Robust.Client
return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? "";
}
public bool ShowLoadingBar()
{
return _resourceManifest!.ShowLoadingBar ?? _configurationManager.GetCVar(CVars.LoadingShowBar);
}
internal bool StartupContinue(DisplayMode displayMode)
{
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(GameTitle());
_loadscr.Initialize(42);
_taskManager.Initialize();
_parallelMgr.Initialize();
_loadscr.BeginLoadingSection("Init graphics", dontRender: true);
_clyde.InitializePostWindowing();
_clyde.SetWindowTitle(GameTitle());
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_audio.InitializePostWindowing, "Init audio");
_loadscr.LoadingStep(_taskManager.Initialize, _taskManager);
_loadscr.LoadingStep(_parallelMgr.Initialize, _parallelMgr);
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
// Load optional Robust modules.
LoadOptionalRobustModules(displayMode, _resourceManifest!);
_loadscr.LoadingStep(_systemFontManager.Initialize, "System fonts");
// Load optional Robust modules.
_loadscr.LoadingStep(() => LoadOptionalRobustModules(displayMode, _resourceManifest!), "Robust Modules");
_loadscr.BeginLoadingSection(_modLoader);
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!ContentStart);
var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1";
_modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing);
if (!LoadModules())
return false;
@@ -161,16 +175,23 @@ namespace Robust.Client
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
_serializationManager.Initialize();
_loc.Initialize();
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_serializationManager.Initialize, _serializationManager);
_loadscr.LoadingStep(_loc.Initialize, _loc);
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PreInit), "Content PreInit");
// Finish initialization of WebView if loaded.
_webViewHook?.Initialize();
_loadscr.LoadingStep(() =>
{
// Finish initialization of WebView if loaded.
if (_webViewHook != null)
_loadscr.LoadingStep(_webViewHook.Initialize, _webViewHook);
},
"WebView init");
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.Init), "Content Init");
// Start bad file extensions check after content init,
// in case content screws with the VFS.
@@ -179,42 +200,51 @@ namespace Robust.Client
_configurationManager,
_logManager.GetSawmill("res"));
_resourceCache.PreloadTextures();
_networkManager.Initialize(false);
_configurationManager.SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_loadscr.LoadingStep(_resourceCache.PreloadTextures, "Texture preload");
_loadscr.LoadingStep(() => { _networkManager.Initialize(false); }, _networkManager);
_loadscr.LoadingStep(_configurationManager.SetupNetworking, _configurationManager);
_loadscr.LoadingStep(_serializer.Initialize, _serializer);
_loadscr.LoadingStep(_inputManager.Initialize, _inputManager);
_loadscr.LoadingStep(_console.Initialize, _console);
// Make sure this is done before we try to load prototypes,
// avoid any possibility of race conditions causing the check to not finish
// before prototype load.
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
_loadscr.LoadingStep(
() => ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions),
"Check bad file extensions");
_reload.Initialize();
_reflectionManager.Initialize();
_loadscr.LoadingStep(_reload.Initialize, _reload);
_loadscr.LoadingStep(_reflectionManager.Initialize, _reflectionManager);
_loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager);
_loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager);
_loadscr.BeginLoadingSection(_prototypeManager);
_prototypeManager.Initialize();
_prototypeManager.LoadDefaultPrototypes();
_xamlProxyManager.Initialize();
_xamlHotReloadManager.Initialize();
_userInterfaceManager.Initialize();
_eyeManager.Initialize();
_entityManager.Initialize();
_mapManager.Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_tagManager.Initialize();
_protoLoadMan.Initialize();
_netResMan.Initialize();
_replayLoader.Initialize();
_replayPlayback.Initialize();
_replayRecording.Initialize();
_userInterfaceManager.PostInitialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
_loadscr.EndLoadingSection();
_loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init");
_loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager);
_loadscr.LoadingStep(_entityManager.Initialize, _entityManager);
_loadscr.LoadingStep(_mapManager.Initialize, _mapManager);
_loadscr.LoadingStep(_gameStateManager.Initialize, _gameStateManager);
_loadscr.LoadingStep(_placementManager.Initialize, _placementManager);
_loadscr.LoadingStep(_viewVariablesManager.Initialize, _viewVariablesManager);
_loadscr.LoadingStep(_scriptClient.Initialize, _scriptClient);
_loadscr.LoadingStep(_client.Initialize, _client);
_loadscr.LoadingStep(_discord.Initialize, _discord);
_loadscr.LoadingStep(_tagManager.Initialize, _tagManager);
_loadscr.LoadingStep(_protoLoadMan.Initialize, _protoLoadMan);
_loadscr.LoadingStep(_netResMan.Initialize, _netResMan);
_loadscr.LoadingStep(_replayLoader.Initialize, _replayLoader);
_loadscr.LoadingStep(_replayPlayback.Initialize, _replayPlayback);
_loadscr.LoadingStep(_replayRecording.Initialize, _replayRecording);
_loadscr.LoadingStep(_userInterfaceManager.PostInitialize, "UI postinit");
// Init stuff before this if at all possible.
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PostInit), "Content PostInit");
_loadscr.Finish();
if (_commandLineArgs?.Username != null)
{
@@ -358,9 +388,6 @@ namespace Robust.Client
var userDataDir = GetUserDataDir();
_configurationManager.Initialize(false);
// MUST load cvars before loading from config file so the cfg manager is aware of secure cvars.
// So SECURE CVars are blacklisted from config.
_configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client
_configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared
@@ -424,7 +451,8 @@ namespace Robust.Client
_configurationManager.OverrideConVars(new[]
{
(CVars.DisplayWindowIconSet.Name, WindowIconSet()),
(CVars.DisplaySplashLogo.Name, SplashLogo())
(CVars.DisplaySplashLogo.Name, SplashLogo()),
(CVars.LoadingShowBar.Name, ShowLoadingBar().ToString()),
});
}
@@ -489,10 +517,18 @@ namespace Robust.Client
public void Shutdown(string? reason = null)
{
DebugTools.AssertNotNull(_mainLoop);
if (_mainLoop == null)
{
if (!_dontStart)
{
_logger.Info($"Shutdown called before client init completed: {reason ?? "No reason provided"}");
_dontStart = true;
}
return;
}
// Already got shut down I assume,
if (!_mainLoop!.Running)
if (!_mainLoop.Running)
{
return;
}

View File

@@ -216,35 +216,36 @@ namespace Robust.Client.GameObjects
}
}
using (histogram?.WithLabels("PredictedQueueDel").NewTimer())
base.TickUpdate(frameTime, noPredictions, histogram);
}
internal override void ProcessQueueudDeletions()
{
base.ProcessQueueudDeletions();
while (_queuedPredictedDeletions.TryDequeue(out var uid))
{
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())
{
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);
}
DeleteEntity(uid, meta, xform);
}
else
{
_xforms.DetachEntity(uid, xform, meta, null);
// base call bypasses IGameTiming.InPrediction check
// This is pretty janky and there should be a way for the client to dirty an entity outside of prediction
// TODO PREDICTION Is actually needed after the current predicted deletion fix?
base.Dirty(uid, xform, meta);
}
_queuedPredictedDeletionsSet.Clear();
}
base.TickUpdate(frameTime, noPredictions, histogram);
_queuedPredictedDeletionsSet.Clear();
}
/// <inheritdoc />

View File

@@ -361,6 +361,9 @@ namespace Robust.Client.GameStates
// avoid exception spam from repeatedly trying to reset the same entity.
_entitySystemManager.GetEntitySystem<ClientDirtySystem>().Reset();
_runtimeLog.LogException(e, "ResetPredictedEntities");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
// If we were waiting for a new state, we are now applying it.
@@ -541,6 +544,11 @@ namespace Robust.Client.GameStates
{
((IBroadcastEventBusInternal)_entities.EventBus).ProcessEventQueue();
}
using (_prof.Group("QueueDel"))
{
_entities.ProcessQueueudDeletions();
}
}
_prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(_timing.CurTick.Value));
@@ -949,6 +957,9 @@ namespace Robust.Client.GameStates
{
_sawmill.Error($"Caught exception while deleting entities");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(ApplyEntityStates)}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}

View File

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

View File

@@ -76,9 +76,9 @@ namespace Robust.Client.Graphics.Clyde
}
// Short path to render only the splash.
if (_drawingSplash)
if (_drawingLoadingScreen)
{
DrawSplash(_renderHandle);
DrawLoadingScreen(_renderHandle);
FlushRenderQueue();
SwapAllBuffers();
return;
@@ -430,18 +430,11 @@ namespace Robust.Client.Graphics.Clyde
FlushRenderQueue();
}
private void DrawSplash(IRenderHandle handle)
private void DrawLoadingScreen(IRenderHandle handle)
{
// Clear screen to black for splash.
ClearFramebuffer(Color.Black);
var splashTex = _cfg.GetCVar(CVars.DisplaySplashLogo);
if (string.IsNullOrEmpty(splashTex))
return;
var texture = _resourceCache.GetResource<TextureResource>(splashTex).Texture;
handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
_loadingScreenManager.DrawLoadingScreen(handle, ScreenSize);
}
private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearColor=default)

View File

@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
private long _nextViewportId = 1;
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
private readonly ConcurrentQueue<(string? name, ViewportDisposeData data)> _viewportDisposeQueue = new();
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
{
@@ -66,13 +66,14 @@ namespace Robust.Client.Graphics.Clyde
{
while (_viewportDisposeQueue.TryDequeue(out var data))
{
DisposeViewport(data);
DisposeViewport(data.data, data.name, wasLeaked: true);
}
}
private void DisposeViewport(ViewportDisposeData disposeData)
private void DisposeViewport(ViewportDisposeData disposeData, string? name = null, bool wasLeaked = false)
{
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
if (wasLeaked)
_clydeSawmill.Warning($"Viewport {disposeData.Id} ({name ?? "null"}) got leaked");
_viewports.Remove(disposeData.Handle);
if (disposeData.ClearEvent is not { } clearEvent)
@@ -211,7 +212,7 @@ namespace Robust.Client.Graphics.Clyde
~Viewport()
{
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
_clyde._viewportDisposeQueue.Enqueue((Name, DisposeData(referenceSelf: false)));
}
public void Dispose()
@@ -224,7 +225,7 @@ namespace Robust.Client.Graphics.Clyde
WallBleedIntermediateRenderTarget1.Dispose();
WallBleedIntermediateRenderTarget2.Dispose();
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
_clyde.DisposeViewport(DisposeData(referenceSelf: false), Name);
}
private ViewportDisposeData DisposeData(bool referenceSelf)

View File

@@ -119,8 +119,8 @@ namespace Robust.Client.Graphics.Clyde
break;
default:
_logManager.GetSawmill("clyde.win").Log(
LogLevel.Error, "Unknown windowing API: {name}. Falling back to GLFW.", windowingApi);
goto case "glfw";
LogLevel.Error, "Unknown windowing API: {name}. Falling back to SDL3.", windowingApi);
goto case "sdl3";
}
_windowing = winImpl;

View File

@@ -52,6 +52,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly ClientEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IReloadManager _reloads = default!;
[Dependency] private readonly LoadingScreenManager _loadingScreenManager = default!;
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
@@ -68,7 +69,7 @@ namespace Robust.Client.Graphics.Clyde
// VAO is per-window and not stored (not necessary!)
private GLBuffer WindowVBO = default!;
private bool _drawingSplash = true;
private bool _drawingLoadingScreen = true;
private GLShaderProgram? _currentProgram;
@@ -213,7 +214,7 @@ namespace Robust.Client.Graphics.Clyde
public void Ready()
{
_drawingSplash = false;
_drawingLoadingScreen = false;
InitLighting();
}

View File

@@ -146,7 +146,7 @@ internal partial class Clyde
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
MapKey(SC.SDL_SCANCODE_APPLICATION, Key.Menu);
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
@@ -173,7 +173,7 @@ internal partial class Clyde
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
MapKey(SC.SDL_SCANCODE_KP_PERIOD, Key.NumpadDecimal);
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
MapKey(SC.SDL_SCANCODE_UP, Key.Up);

View File

@@ -104,6 +104,12 @@ namespace Robust.Client.Graphics
Handle = IoCManager.Resolve<IFontManagerInternal>().MakeInstance(res.FontFaceHandle, size);
}
internal VectorFont(IFontInstanceHandle handle, int size)
{
Size = size;
Handle = handle;
}
public override int GetAscent(float scale) => Handle.GetAscent(scale);
public override int GetHeight(float scale) => Handle.GetHeight(scale);
public override int GetDescent(float scale) => Handle.GetDescent(scale);
@@ -222,4 +228,74 @@ namespace Robust.Client.Graphics
return null;
}
}
/// <summary>
/// Possible values for font weights. Larger values have thicker font strokes.
/// </summary>
/// <remarks>
/// <para>
/// These values are based on the <c>usWeightClass</c> property of the OpenType specification:
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace.Weight"/>
public enum FontWeight : ushort
{
Thin = 100,
ExtraLight = 200,
UltraLight = ExtraLight,
Light = 300,
SemiLight = 350,
Normal = 400,
Regular = Normal,
Medium = 500,
SemiBold = 600,
DemiBold = SemiBold,
Bold = 700,
ExtraBold = 800,
UltraBold = ExtraBold,
Black = 900,
Heavy = Black,
ExtraBlack = 950,
UltraBlack = ExtraBlack,
}
/// <summary>
/// Possible slant values for fonts.
/// </summary>
/// <seealso cref="ISystemFontFace.Slant"/>
public enum FontSlant : byte
{
// NOTE: Enum values correspond to DWRITE_FONT_STYLE.
Normal = 0,
Oblique = 1,
// FUN FACT: they're called "italics" because they look like the Leaning Tower of Pisa.
// Don't fact-check that.
Italic = 2
}
/// <summary>
/// Possible values for font widths. Larger values are proportionally wider.
/// </summary>
/// <remarks>
/// <para>
/// These values are based on the <c>usWidthClass</c> property of the OpenType specification:
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace.Width"/>
public enum FontWidth : ushort
{
UltraCondensed = 1,
ExtraCondensed = 2,
Condensed = 3,
SemiCondensed = 4,
Normal = 5,
Medium = Normal,
SemiExpanded = 6,
Expanded = 7,
ExtraExpanded = 8,
UltraExpanded = 9,
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Console;
namespace Robust.Client.Graphics.FontManagement;
internal sealed class SystemFontDebugCommand : IConsoleCommand
{
public string Command => "system_font_debug";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
new SystemFontDebugWindow().OpenCentered();
}
}

View File

@@ -0,0 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="System font debug">
<SplitContainer Orientation="Horizontal" MinSize="800 600">
<ScrollContainer HScrollEnabled="False">
<BoxContainer Name="SelectorContainer" Orientation="Vertical" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False">
<BoxContainer Orientation="Vertical">
<Label Name="FamilyLabel" />
<BoxContainer Orientation="Vertical" Name="FaceContainer" />
</BoxContainer>
</ScrollContainer>
</SplitContainer>
</DefaultWindow>

View File

@@ -0,0 +1,98 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.FontManagement;
[GenerateTypedNameReferences]
internal sealed partial class SystemFontDebugWindow : DefaultWindow
{
private static readonly int[] ExampleFontSizes = [8, 12, 16, 24, 36];
private const string ExampleString = "The quick brown fox jumps over the lazy dog";
[Dependency] private readonly ISystemFontManager _systemFontManager = default!;
public SystemFontDebugWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
var buttonGroup = new ButtonGroup();
foreach (var group in _systemFontManager.SystemFontFaces.GroupBy(k => k.FamilyName).OrderBy(k => k.Key))
{
var fonts = group.ToArray();
SelectorContainer.AddChild(new Selector(this, buttonGroup, group.Key, fonts));
}
}
private void SelectFontFamily(ISystemFontFace[] fonts)
{
FamilyLabel.Text = fonts[0].FamilyName;
FaceContainer.RemoveAllChildren();
foreach (var font in fonts)
{
var exampleContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(8)
};
foreach (var size in ExampleFontSizes)
{
var fontInstance = font.Load(size);
var richTextLabel = new RichTextLabel
{
Stylesheet = new Stylesheet([
StylesheetHelpers.Element<RichTextLabel>().Prop("font", fontInstance)
]),
};
richTextLabel.SetMessage(FormattedMessage.FromUnformatted(ExampleString));
exampleContainer.AddChild(richTextLabel);
}
FaceContainer.AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
new RichTextLabel
{
Text = $"""
{font.FullName}
Family: "{font.FamilyName}", face: "{font.FaceName}", PostScript = "{font.PostscriptName}"
Weight: {font.Weight} ({(int) font.Weight}), slant: {font.Slant} ({(int) font.Slant}), width: {font.Width} ({(int) font.Width})
""",
},
exampleContainer
},
Margin = new Thickness(0, 0, 0, 8)
});
}
}
private sealed class Selector : Control
{
public Selector(SystemFontDebugWindow window, ButtonGroup group, string family, ISystemFontFace[] fonts)
{
var button = new Button
{
Text = family,
Group = group,
ToggleMode = true
};
AddChild(button);
button.OnPressed += _ => window.SelectFontFamily(fonts);
}
}
}

View File

@@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.FontManagement;
internal abstract class SystemFontManagerBase
{
/// <summary>
/// The "standard" locale used when looking up the PostScript name of a font face.
/// </summary>
/// <remarks>
/// <para>
/// Font files allow the PostScript name to be localized, however in practice
/// we would really like to have a language-unambiguous identifier to refer to a font file.
/// We use this locale (en-US) to look up teh PostScript font name, if there are multiple provided.
/// This matches the behavior of the Local Font Access web API:
/// https://wicg.github.io/local-font-access/#concept-font-representation
/// </para>
/// </remarks>
protected static readonly CultureInfo StandardLocale = new("en-US", false);
protected readonly IFontManagerInternal FontManager;
protected readonly ISawmill Sawmill;
protected readonly Lock Lock = new();
protected readonly List<BaseHandle> Fonts = [];
public IEnumerable<ISystemFontFace> SystemFontFaces { get; }
public SystemFontManagerBase(ILogManager logManager, IFontManagerInternal fontManager)
{
FontManager = fontManager;
Sawmill = logManager.GetSawmill("font.system");
SystemFontFaces = Fonts.AsReadOnly();
}
protected abstract IFontFaceHandle LoadFontFace(BaseHandle handle);
protected static string GetLocalizedForLocaleOrFirst(LocalizedStringSet set, CultureInfo culture)
{
var matchCulture = culture;
while (!Equals(matchCulture, CultureInfo.InvariantCulture))
{
if (set.Values.TryGetValue(culture.Name, out var value))
return value;
matchCulture = matchCulture.Parent;
}
return set.Values[set.Primary];
}
protected abstract class BaseHandle(SystemFontManagerBase parent) : ISystemFontFace
{
private IFontFaceHandle? _cachedFont;
public required string PostscriptName { get; init; }
public required LocalizedStringSet FullNames;
public required LocalizedStringSet FamilyNames;
public required LocalizedStringSet FaceNames;
public required FontWeight Weight { get; init; }
public required FontSlant Slant { get; init; }
public required FontWidth Width { get; init; }
public string FullName => GetLocalizedFullName(CultureInfo.CurrentCulture);
public string FamilyName => GetLocalizedFamilyName(CultureInfo.CurrentCulture);
public string FaceName => GetLocalizedFaceName(CultureInfo.CurrentCulture);
public string GetLocalizedFullName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FullNames, culture);
}
public string GetLocalizedFamilyName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FamilyNames, culture);
}
public string GetLocalizedFaceName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FaceNames, culture);
}
public Font Load(int size)
{
var handle = GetFaceHandle();
var instance = parent.FontManager.MakeInstance(handle, size);
return new VectorFont(instance, size);
}
private IFontFaceHandle GetFaceHandle()
{
lock (parent.Lock)
{
if (_cachedFont != null)
return _cachedFont;
parent.Sawmill.Verbose($"Loading system font face: {PostscriptName}");
return _cachedFont = parent.LoadFontFace(this);
}
}
}
protected struct LocalizedStringSet
{
public static readonly LocalizedStringSet Empty = FromSingle("");
/// <summary>
/// The first locale to appear in the list of localized strings.
/// Used as fallback if the desired locale is not provided.
/// </summary>
public required string Primary;
public required Dictionary<string, string> Values;
public static LocalizedStringSet FromSingle(string value, string language = "en")
{
return new LocalizedStringSet
{
Primary = language,
Values = new Dictionary<string, string> { { language, value } }
};
}
}
protected sealed class MemoryMappedFontMemoryHandle : IFontMemoryHandle
{
private readonly MemoryMappedFile _mappedFile;
private readonly MemoryMappedViewAccessor _accessor;
public MemoryMappedFontMemoryHandle(string filePath)
{
_mappedFile = MemoryMappedFile.CreateFromFile(
filePath,
FileMode.Open,
null,
0,
MemoryMappedFileAccess.Read);
_accessor = _mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
}
public unsafe byte* GetData()
{
byte* pointer = null;
_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
return pointer;
}
public nint GetDataSize()
{
return (nint)_accessor.Capacity;
}
public void Dispose()
{
_accessor.Dispose();
_mappedFile.Dispose();
}
}
}

View File

@@ -0,0 +1,195 @@
#if MACOS
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Robust.Client.Interop.MacOS;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using CF = Robust.Client.Interop.MacOS.CoreFoundation;
using CT = Robust.Client.Interop.MacOS.CoreText;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses CoreText on macOS.
/// </summary>
internal sealed class SystemFontManagerCoreText : SystemFontManagerBase, ISystemFontManagerInternal
{
private static readonly FontWidth[] FontWidths = Enum.GetValues<FontWidth>();
public bool IsSupported => true;
public SystemFontManagerCoreText(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager,
fontManager)
{
}
public unsafe void Initialize()
{
Sawmill.Verbose("Getting CTFontCollection...");
var collection = CT.CTFontCollectionCreateFromAvailableFonts(null);
var array = CT.CTFontCollectionCreateMatchingFontDescriptors(collection);
var count = CF.CFArrayGetCount(array);
Sawmill.Verbose($"Have {count} descriptors...");
for (nint i = 0; i < count.Value; i++)
{
var item = (__CTFontDescriptor*)CF.CFRetain(CF.CFArrayGetValueAtIndex(array, new CLong(i)));
try
{
LoadFontDescriptor(item);
}
catch (Exception ex)
{
Sawmill.Error($"Failed to load font descriptor: {ex}");
}
finally
{
CF.CFRelease(item);
}
}
CF.CFRelease(array);
CF.CFRelease(collection);
}
private unsafe void LoadFontDescriptor(__CTFontDescriptor* descriptor)
{
var displayName = GetFontAttributeManaged(descriptor, CT.kCTFontDisplayNameAttribute);
var postscriptName = GetFontAttributeManaged(descriptor, CT.kCTFontNameAttribute);
var familyName = GetFontAttributeManaged(descriptor, CT.kCTFontFamilyNameAttribute);
var styleName = GetFontAttributeManaged(descriptor, CT.kCTFontStyleNameAttribute);
var url = (__CFURL*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontURLAttribute);
const int maxPath = 1024;
var buf = stackalloc byte[maxPath];
var result = CF.CFURLGetFileSystemRepresentation(url, 1, buf, new CLong(maxPath));
if (result == 0)
throw new Exception("CFURLGetFileSystemRepresentation failed!");
// Sawmill.Verbose(CF.CFStringToManaged(CF.CFURLGetString(url)));
CF.CFRelease(url);
var traits = (__CFDictionary*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontTraitsAttribute);
var (weight, slant, width) = ParseTraits(traits);
CF.CFRelease(traits);
var path = Marshal.PtrToStringUTF8((nint)buf)!;
Fonts.Add(new Handle(this)
{
PostscriptName = postscriptName,
FullNames = LocalizedStringSet.FromSingle(displayName),
FamilyNames = LocalizedStringSet.FromSingle(familyName),
FaceNames = LocalizedStringSet.FromSingle(styleName),
Weight = weight,
Slant = slant,
Width = width,
Path = path
});
}
private static unsafe (FontWeight, FontSlant, FontWidth) ParseTraits(__CFDictionary* dictionary)
{
var weight = FontWeight.Normal;
var slant = FontSlant.Normal;
var width = FontWidth.Normal;
var weightVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWeightTrait);
if (weightVal != null)
weight = ConvertWeight(weightVal);
var slantVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontSlantTrait);
if (slantVal != null)
slant = ConvertSlant(slantVal);
var widthVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWidthTrait);
if (widthVal != null)
width = ConvertWidth(widthVal);
return (weight, slant, width);
}
private static readonly (float, FontWeight)[] FontWeightTable =
[
((float) AppKit.NSFontWeightUltraLight, FontWeight.UltraLight),
((float) AppKit.NSFontWeightThin, FontWeight.Thin),
((float) AppKit.NSFontWeightLight, FontWeight.Light),
((float) AppKit.NSFontWeightRegular, FontWeight.Regular),
((float) AppKit.NSFontWeightMedium, FontWeight.Medium),
((float) AppKit.NSFontWeightSemiBold, FontWeight.SemiBold),
((float) AppKit.NSFontWeightBold, FontWeight.Bold),
((float) AppKit.NSFontWeightHeavy, FontWeight.Heavy),
((float) AppKit.NSFontWeightBlack, FontWeight.Black)
];
private static unsafe FontWeight ConvertWeight(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
var valCopy = val;
return FontWeightTable.MinBy(tup => Math.Abs(tup.Item1 - valCopy)).Item2;
}
private static unsafe FontWidth ConvertWidth(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
// Normalize to 0-1 range
val = (val + 1) / 2;
var lerped = MathHelper.Lerp((float)FontWidths[0], (float)FontWidths[^1], val);
return FontWidths.MinBy(x => Math.Abs((float)x - lerped));
}
private static unsafe FontSlant ConvertSlant(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
// Normalize to 0-1 range
return val == 0 ? FontSlant.Normal : FontSlant.Italic;
}
private static unsafe string GetFontAttributeManaged(__CTFontDescriptor* descriptor, __CFString* key)
{
var str = (__CFString*)CT.CTFontDescriptorCopyAttribute(descriptor, key);
try
{
return CF.CFStringToManaged(str);
}
finally
{
CF.CFRelease(str);
}
}
public void Shutdown()
{
// Nothing to do.
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var path = ((Handle)handle).Path;
Sawmill.Verbose(path);
// CTFontDescriptor does not seem to have any way to identify *which* index in the font file should be accessed.
// So we have to just load every one until the postscript name matches.
return FontManager.LoadWithPostscriptName(new MemoryMappedFontMemoryHandle(path), handle.PostscriptName);
}
private sealed class Handle(SystemFontManagerCoreText parent) : BaseHandle(parent)
{
public required string Path;
}
}
#endif

View File

@@ -0,0 +1,503 @@
#if WINDOWS
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.DirectX.DWRITE_FACTORY_TYPE;
using static TerraFX.Interop.DirectX.DWRITE_FONT_PROPERTY_ID;
using static TerraFX.Interop.Windows.Windows;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
/// </summary>
internal sealed unsafe class SystemFontManagerDirectWrite : SystemFontManagerBase, ISystemFontManagerInternal
{
// For future implementors of other platforms:
// a significant amount of code in this file will be shareable with that of other platforms,
// so some refactoring is warranted.
private readonly IConfigurationManager _cfg;
private IDWriteFactory3* _dWriteFactory;
private IDWriteFontSet* _systemFontSet;
public bool IsSupported => true;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
/// </summary>
public SystemFontManagerDirectWrite(
ILogManager logManager,
IConfigurationManager cfg,
IFontManagerInternal fontManager)
: base(logManager, fontManager)
{
_cfg = cfg;
}
public void Initialize()
{
CreateDWriteFactory();
_systemFontSet = GetSystemFontSet(_dWriteFactory);
lock (Lock)
{
var fontCount = _systemFontSet->GetFontCount();
for (var i = 0u; i < fontCount; i++)
{
LoadSingleFontFromSet(_systemFontSet, i);
}
}
Sawmill.Verbose($"Loaded {Fonts.Count} fonts");
}
public void Shutdown()
{
_systemFontSet->Release();
_systemFontSet = null;
_dWriteFactory->Release();
_dWriteFactory = null;
lock (Lock)
{
foreach (var systemFont in Fonts)
{
((Handle)systemFont).FontFace->Release();
}
Fonts.Clear();
}
}
private void LoadSingleFontFromSet(IDWriteFontSet* set, uint fontIndex)
{
// Get basic parameters that every font should probably have?
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_POSTSCRIPT_NAME, out var postscriptNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FULL_NAME, out var fullNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, out var familyNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FACE_NAME, out var faceNames))
return;
// I assume these parameters can't be missing in practice, but better safe than sorry.
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_WEIGHT, out var weight);
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STYLE, out var style);
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STRETCH, out var stretch);
var parsedWeight = ParseFontWeight(weight);
var parsedSlant = ParseFontSlant(style);
var parsedWidth = ParseFontWidth(stretch);
IDWriteFontFaceReference* reference = null;
var result = set->GetFontFaceReference(fontIndex, &reference);
ThrowIfFailed(result);
var handle = new Handle(this, reference)
{
PostscriptName = GetLocalizedForLocaleOrFirst(postscriptNames, StandardLocale),
FullNames = fullNames,
FamilyNames = familyNames,
FaceNames = faceNames,
Weight = parsedWeight,
Slant = parsedSlant,
Width = parsedWidth
};
Fonts.Add(handle);
}
private static FontWeight ParseFontWeight(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontWeight.Regular;
return (FontWeight)Parse.Int32(strings[0].Value);
}
private static FontSlant ParseFontSlant(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontSlant.Normal;
return (FontSlant)Parse.Int32(strings[0].Value);
}
private static FontWidth ParseFontWidth(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontWidth.Normal;
return (FontWidth)Parse.Int32(strings[0].Value);
}
private void CreateDWriteFactory()
{
fixed (IDWriteFactory3** pFactory = &_dWriteFactory)
{
var result = DirectX.DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof<IDWriteFactory3>(),
(IUnknown**)pFactory);
ThrowIfFailed(result);
}
}
private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
{
IDWriteFactory6* factory6;
IDWriteFontSet* fontSet;
var result = factory->QueryInterface(__uuidof<IDWriteFactory6>(), (void**)&factory6);
if (result.SUCCEEDED)
{
Sawmill.Verbose("IDWriteFactory6 available, using newer GetSystemFontSet");
result = factory6->GetSystemFontSet(
_cfg.GetCVar(CVars.FontWindowsDownloadable),
(IDWriteFontSet1**)(&fontSet));
factory6->Release();
}
else
{
Sawmill.Verbose("IDWriteFactory6 not available");
result = factory->GetSystemFontSet(&fontSet);
}
ThrowIfFailed(result, "GetSystemFontSet");
return fontSet;
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var fontFace = ((Handle)handle).FontFace;
IDWriteFontFile* file = null;
IDWriteFontFileLoader* loader = null;
try
{
var result = fontFace->GetFontFile(&file);
ThrowIfFailed(result, "IDWriteFontFaceReference::GetFontFile");
result = file->GetLoader(&loader);
ThrowIfFailed(result, "IDWriteFontFile::GetLoader");
void* referenceKey;
uint referenceKeyLength;
result = file->GetReferenceKey(&referenceKey, &referenceKeyLength);
ThrowIfFailed(result, "IDWriteFontFile::GetReferenceKey");
IDWriteLocalFontFileLoader* localLoader;
result = loader->QueryInterface(__uuidof<IDWriteLocalFontFileLoader>(), (void**)&localLoader);
if (result.SUCCEEDED)
{
Sawmill.Verbose("Loading font face via memory mapped file...");
// We can get the local file path on disk. This means we can directly load it via mmap.
uint filePathLength;
ThrowIfFailed(
localLoader->GetFilePathLengthFromKey(referenceKey, referenceKeyLength, &filePathLength),
"IDWriteLocalFontFileLoader::GetFilePathLengthFromKey");
var filePath = new char[filePathLength + 1];
fixed (char* pFilePath = filePath)
{
ThrowIfFailed(
localLoader->GetFilePathFromKey(
referenceKey,
referenceKeyLength,
pFilePath,
(uint)filePath.Length),
"IDWriteLocalFontFileLoader::GetFilePathFromKey");
}
var path = new string(filePath, 0, (int)filePathLength);
localLoader->Release();
return FontManager.Load(new MemoryMappedFontMemoryHandle(path));
}
else
{
Sawmill.Verbose("Loading font face via stream...");
// DirectWrite doesn't give us anything to go with for this file, read it into regular memory.
// If the font file has multiple faces, which is possible, then this approach will duplicate memory.
// That sucks, but I'm really not sure whether there's any way around this short of
// comparing the memory contents by hashing to check equality.
// As I'm pretty sure we can't like reference equality check the font objects somehow.
IDWriteFontFileStream* stream;
result = loader->CreateStreamFromKey(referenceKey, referenceKeyLength, &stream);
ThrowIfFailed(result, "IDWriteFontFileLoader::CreateStreamFromKey");
using var streamObject = new DirectWriteStream(stream);
return FontManager.Load(streamObject, (int)fontFace->GetFontFaceIndex());
}
}
finally
{
if (file != null)
file->Release();
if (loader != null)
loader->Release();
}
}
private static bool TryGetStrings(
IDWriteFontSet* set,
uint listIndex,
DWRITE_FONT_PROPERTY_ID property,
[NotNullWhen(true)] out DWriteLocalizedString[]? strings)
{
BOOL exists;
IDWriteLocalizedStrings* dWriteStrings = null;
var result = set->GetPropertyValues(
listIndex,
property,
&exists,
&dWriteStrings);
ThrowIfFailed(result, "IDWriteFontSet::GetPropertyValues");
if (!exists)
{
strings = null;
return false;
}
try
{
strings = GetStrings(dWriteStrings);
return true;
}
finally
{
dWriteStrings->Release();
}
}
private static bool TryGetStringsSet(
IDWriteFontSet* set,
uint listIndex,
DWRITE_FONT_PROPERTY_ID property,
out LocalizedStringSet strings)
{
if (!TryGetStrings(set, listIndex, property, out var stringsArray))
{
strings = default;
return false;
}
strings = StringsToSet(stringsArray);
return true;
}
private static DWriteLocalizedString[] GetStrings(IDWriteLocalizedStrings* localizedStrings)
{
IDWriteStringList* list;
ThrowIfFailed(localizedStrings->QueryInterface(__uuidof<IDWriteStringList>(), (void**)&list));
try
{
return GetStrings(list);
}
finally
{
list->Release();
}
}
private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
{
var array = new DWriteLocalizedString[stringList->GetCount()];
var stringPool = ArrayPool<char>.Shared.Rent(256);
for (var i = 0; i < array.Length; i++)
{
uint length;
ThrowIfFailed(stringList->GetStringLength((uint)i, &length), "IDWriteStringList::GetStringLength");
ExpandIfNecessary(ref stringPool, length + 1);
fixed (char* pArr = stringPool)
{
ThrowIfFailed(
stringList->GetString((uint)i, pArr, (uint)stringPool.Length),
"IDWriteStringList::GetString");
}
var value = new string(stringPool, 0, (int)length);
ThrowIfFailed(stringList->GetLocaleNameLength((uint)i, &length), "IDWriteStringList::GetLocaleNameLength");
ExpandIfNecessary(ref stringPool, length + 1);
fixed (char* pArr = stringPool)
{
ThrowIfFailed(
stringList->GetLocaleName((uint)i, pArr, (uint)stringPool.Length),
"IDWriteStringList::GetLocaleName");
}
var localeName = new string(stringPool, 0, (int)length);
array[i] = new DWriteLocalizedString(value, localeName);
}
ArrayPool<char>.Shared.Return(stringPool);
return array;
}
private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
{
if (requiredLength < array.Length)
return;
ArrayPool<char>.Shared.Return(array);
array = ArrayPool<char>.Shared.Rent(checked((int)requiredLength));
}
private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
{
var dict = new Dictionary<string, string>();
foreach (var (value, localeName) in strings)
{
dict[localeName] = value;
}
return new LocalizedStringSet { Primary = strings[0].LocaleName, Values = dict };
}
private sealed class Handle(SystemFontManagerDirectWrite parent, IDWriteFontFaceReference* fontFace) : BaseHandle(parent)
{
public readonly IDWriteFontFaceReference* FontFace = fontFace;
}
/// <summary>
/// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
/// </summary>
private sealed class DirectWriteStream : Stream
{
private readonly IDWriteFontFileStream* _stream;
private readonly ulong _size;
private ulong _position;
private bool _disposed;
public DirectWriteStream(IDWriteFontFileStream* stream)
{
_stream = stream;
fixed (ulong* pSize = &_size)
{
var result = _stream->GetFileSize(pSize);
ThrowIfFailed(result, "IDWriteFontFileStream::GetFileSize");
}
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
return Read(buffer.AsSpan(offset, count));
}
public override int Read(Span<byte> buffer)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DirectWriteStream));
var readLength = (uint)buffer.Length;
if (readLength + _position > _size)
readLength = (uint)(_size - _position);
void* fragmentStart;
void* fragmentContext;
var result = _stream->ReadFileFragment(&fragmentStart, _position, readLength, &fragmentContext);
ThrowIfFailed(result);
var data = new ReadOnlySpan<byte>(fragmentStart, (int)readLength);
data.CopyTo(buffer);
_stream->ReleaseFileFragment(fragmentContext);
_position += readLength;
return (int)readLength;
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position += offset;
break;
case SeekOrigin.End:
Position = Length + offset;
break;
default:
throw new ArgumentOutOfRangeException(nameof(origin), origin, null);
}
return Position;
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => (long)_size;
public override long Position
{
get => (long)_position;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, _size);
_position = (ulong)value;
}
}
protected override void Dispose(bool disposing)
{
_stream->Release();
_disposed = true;
}
}
private record struct DWriteLocalizedString(string Value, string LocaleName);
}
#endif

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// A fallback implementation of <see cref="ISystemFontManager"/> that just loads no fonts.
/// </summary>
internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal
{
public void Initialize()
{
}
public void Shutdown()
{
}
public bool IsSupported => false;
public IEnumerable<ISystemFontFace> SystemFontFaces => [];
}

View File

@@ -0,0 +1,235 @@
#if FREEDESKTOP
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Log;
using SpaceWizards.Fontconfig.Interop;
namespace Robust.Client.Graphics.FontManagement;
internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal
{
private static readonly (int Fc, FontWidth Width)[] WidthTable = [
(Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed),
(Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed),
(Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed),
(Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed),
(Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal),
(Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded),
(Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded),
(Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded),
(Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded),
];
public bool IsSupported => true;
public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager)
: base(logManager, fontManager)
{
}
public void Initialize()
{
Sawmill.Verbose("Initializing Fontconfig...");
var result = Fontconfig.FcInit();
if (result == Fontconfig.FcFalse)
throw new InvalidOperationException("Failed to initialize fontconfig!");
Sawmill.Verbose("Listing fonts...");
var os = Fontconfig.FcObjectSetCreate();
AddToObjectSet(os, Fontconfig.FC_FAMILY);
AddToObjectSet(os, Fontconfig.FC_FAMILYLANG);
AddToObjectSet(os, Fontconfig.FC_STYLE);
AddToObjectSet(os, Fontconfig.FC_STYLELANG);
AddToObjectSet(os, Fontconfig.FC_FULLNAME);
AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG);
AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME);
AddToObjectSet(os, Fontconfig.FC_SLANT);
AddToObjectSet(os, Fontconfig.FC_WEIGHT);
AddToObjectSet(os, Fontconfig.FC_WIDTH);
AddToObjectSet(os, Fontconfig.FC_FILE);
AddToObjectSet(os, Fontconfig.FC_INDEX);
var allPattern = Fontconfig.FcPatternCreate();
var set = Fontconfig.FcFontList(null, allPattern, os);
for (var i = 0; i < set->nfont; i++)
{
var pattern = set->fonts[i];
try
{
LoadPattern(pattern);
}
catch (Exception e)
{
Sawmill.Error($"Error while loading pattern: {e}");
}
}
Fontconfig.FcPatternDestroy(allPattern);
Fontconfig.FcObjectSetDestroy(os);
Fontconfig.FcFontSetDestroy(set);
}
public void Shutdown()
{
// Nada.
}
private void LoadPattern(FcPattern* pattern)
{
var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0];
var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0];
var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG);
var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG);
var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG);
var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME);
if (psName == null)
return;
var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN];
var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR];
var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL];
Fonts.Add(new Handle(this)
{
FilePath = path,
FileIndex = idx,
FaceNames = style ?? LocalizedStringSet.Empty,
FullNames = fullName ?? LocalizedStringSet.Empty,
FamilyNames = family ?? LocalizedStringSet.Empty,
PostscriptName = psName[0],
Slant = SlantFromFontconfig(slant[0]),
Weight = WeightFromFontconfig(weight[0]),
Width = WidthFromFontconfig(width[0])
});
}
private static FontWeight WeightFromFontconfig(int value)
{
return (FontWeight)Fontconfig.FcWeightToOpenType(value);
}
private static FontSlant SlantFromFontconfig(int value)
{
return value switch
{
Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic,
Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic,
_ => FontSlant.Normal,
};
}
private static FontWidth WidthFromFontconfig(int value)
{
return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width;
}
private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan<byte> value)
{
var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value)));
if (result == Fontconfig.FcFalse)
throw new InvalidOperationException("Failed to add to object set!");
}
private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan<byte> @object)
{
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) =>
{
byte* str = null;
var res = Fontconfig.FcPatternGetString(p, o, i, &str);
value = Marshal.PtrToStringUTF8((nint)str)!;
return res;
});
}
private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan<byte> @object)
{
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) =>
{
FcResult res;
fixed (int* pValue = &value)
{
res = Fontconfig.FcPatternGetInteger(p, o, i, pValue);
}
return res;
});
}
private delegate FcResult GetValue<T>(FcPattern* p, sbyte* o, int i, out T value);
private static unsafe T[]? PatternGetValues<T>(FcPattern* pattern, ReadOnlySpan<byte> @object, GetValue<T> getValue)
{
var list = new List<T>();
var i = 0;
while (true)
{
var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value);
if (result == FcResult.FcResultMatch)
{
list.Add(value);
}
else if (result == FcResult.FcResultNoMatch)
{
return null;
}
else if (result == FcResult.FcResultNoId)
{
break;
}
else
{
throw new Exception($"FcPatternGetString gave error: {result}");
}
}
return list.ToArray();
}
private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan<byte> @object, ReadOnlySpan<byte> objectLang)
{
var values = PatternGetStrings(pattern, @object);
var languages = PatternGetStrings(pattern, objectLang);
if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length)
return null;
var dict = new Dictionary<string, string>();
for (var i = 0; i < values.Length; i++)
{
var val = values[i];
var lang = languages[i];
dict.TryAdd(lang, val);
}
return new LocalizedStringSet
{
Primary = languages[0],
Values = dict
};
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var cast = (Handle)handle;
return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex);
}
private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent)
{
public required string FilePath;
public required int FileIndex;
}
}
#endif

View File

@@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics
{
@@ -20,6 +20,7 @@ namespace Robust.Client.Graphics
private const int SheetHeight = 256;
private readonly IClyde _clyde;
private readonly ISawmill _sawmill;
private uint _baseFontDpi = 96;
@@ -28,22 +29,56 @@ namespace Robust.Client.Graphics
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
new();
public FontManager(IClyde clyde)
public FontManager(IClyde clyde, ILogManager logManager)
{
_clyde = clyde;
_library = new Library();
_sawmill = logManager.GetSawmill("font");
}
public IFontFaceHandle Load(Stream stream)
public IFontFaceHandle Load(Stream stream, int index = 0)
{
// Freetype directly operates on the font memory managed by us.
// As such, the font data should be pinned in POH.
var fontData = stream.CopyToPinnedArray();
var face = new Face(_library, fontData, 0);
var handle = new FontFaceHandle(face);
return Load(new ArrayMemoryHandle(fontData), index);
}
public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0)
{
var face = FaceLoad(memory, index);
var handle = new FontFaceHandle(face, memory);
return handle;
}
public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName)
{
var numFaces = 1;
for (var i = 0; i < numFaces; i++)
{
var face = FaceLoad(memory, i);
numFaces = face.FaceCount;
if (face.GetPostscriptName() == postscriptName)
return new FontFaceHandle(face, memory);
face.Dispose();
}
// Fallback, load SOMETHING.
_sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}");
return new FontFaceHandle(FaceLoad(memory, 0), memory);
}
private unsafe Face FaceLoad(IFontMemoryHandle memory, int index)
{
return new Face(_library,
(nint)memory.GetData(),
checked((int)memory.GetDataSize()),
index);
}
void IFontManagerInternal.SetFontDpi(uint fontDpi)
{
_baseFontDpi = fontDpi;
@@ -235,10 +270,13 @@ namespace Robust.Client.Graphics
private sealed class FontFaceHandle : IFontFaceHandle
{
// Keep this alive to avoid it being GC'd.
private readonly IFontMemoryHandle _memoryHandle;
public Face Face { get; }
public FontFaceHandle(Face face)
public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle)
{
_memoryHandle = memoryHandle;
Face = face;
}
}
@@ -377,5 +415,32 @@ namespace Robust.Client.Graphics
public CharMetrics Metrics;
public AtlasTexture? Texture;
}
private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle
{
private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
public unsafe byte* GetData()
{
return (byte*) _gcHandle.AddrOfPinnedObject();
}
public IntPtr GetDataSize()
{
return array.Length;
}
public void Dispose()
{
_gcHandle.Free();
_gcHandle = default;
GC.SuppressFinalize(this);
}
~ArrayMemoryHandle()
{
Dispose();
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.IO;
using System.Text;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
{
@@ -10,7 +10,15 @@ namespace Robust.Client.Graphics
}
internal interface IFontManagerInternal : IFontManager
{
IFontFaceHandle Load(Stream stream);
IFontFaceHandle Load(Stream stream, int index = 0);
IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0);
/// <summary>
/// Load a specified font in a font collection.
/// </summary>
/// <param name="memory">Memory for the entire font collection.</param>
/// <param name="postscriptName">The postscript name of the font to load.</param>
IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void SetFontDpi(uint fontDpi);
}
@@ -22,8 +30,6 @@ namespace Robust.Client.Graphics
internal interface IFontInstanceHandle
{
Texture? GetCharTexture(Rune codePoint, float scale);
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
@@ -35,6 +41,12 @@ namespace Robust.Client.Graphics
int GetLineHeight(float scale);
}
internal unsafe interface IFontMemoryHandle : IDisposable
{
byte* GetData();
nint GetDataSize();
}
/// <summary>
/// Metrics for a single glyph in a font.
/// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information.

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.Globalization;
namespace Robust.Client.Graphics;
/// <summary>
/// Provides access to fonts installed on the user's operating system.
/// </summary>
/// <remarks>
/// <para>
/// Different operating systems ship different fonts, so you should generally not rely on any one
/// specific font being available. This system is primarily provided for allowing user preference.
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace"/>
public interface ISystemFontManager
{
/// <summary>
/// Whether access to system fonts is currently supported on this platform.
/// </summary>
bool IsSupported { get; }
/// <summary>
/// The list of font face available from the operating system.
/// </summary>
IEnumerable<ISystemFontFace> SystemFontFaces { get; }
}
/// <summary>
/// A single font face, provided by the user's operating system.
/// </summary>
/// <seealso cref="ISystemFontManager"/>
public interface ISystemFontFace
{
/// <summary>
/// The PostScript name of the font face.
/// This is generally the closest to an unambiguous unique identifier as you're going to get.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial-ItalicMT"
/// </para>
/// </remarks>
string PostscriptName { get; }
/// <summary>
/// The full name of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial Cursiva"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFullName"/>
string FullName { get; }
/// <summary>
/// The family name of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFamilyName"/>
string FamilyName { get; }
/// <summary>
/// The face name (or "style name") of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Cursiva"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFaceName"/>
string FaceName { get; }
/// <summary>
/// Get the <see cref="FullName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFullName(CultureInfo culture);
/// <summary>
/// Get the <see cref="FamilyName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFamilyName(CultureInfo culture);
/// <summary>
/// Get the <see cref="FaceName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFaceName(CultureInfo culture);
/// <summary>
/// The weight of the font face.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// The slant of the font face.
/// </summary>
FontSlant Slant { get; }
/// <summary>
/// The width of the font face.
/// </summary>
FontWidth Width { get; }
/// <summary>
/// Load the font face so that it can be used in-engine.
/// </summary>
/// <param name="size">The size to load the font at.</param>
/// <returns>A font object that can be used to render text.</returns>
Font Load(int size);
}
/// <summary>
/// Engine-internal API for <see cref="ISystemFontManager"/>.
/// </summary>
internal interface ISystemFontManagerInternal : ISystemFontManager
{
void Initialize();
void Shutdown();
}

View File

@@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Stopwatch = Robust.Shared.Timing.Stopwatch;
namespace Robust.Client.Graphics;
internal interface ILoadingScreenManager
{
void BeginLoadingSection(string sectionName);
/// <summary>
/// Start a loading bar "section" for the given method.
/// Must be ended with EndSection.
/// </summary>
void BeginLoadingSection(object method);
void EndLoadingSection();
/// <summary>
/// Will run the giving function and add a custom "section" for it on the loading screen.
/// </summary>
void LoadingStep(Action action, object method);
}
/// <summary>
/// Manager that creates and displays a basic splash screen and loading bar.
/// </summary>
internal sealed class LoadingScreenManager : ILoadingScreenManager
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
private readonly Stopwatch _sw = new();
#region UI constants
private const int LoadingBarWidth = 250;
private const int LoadingBarHeight = 20;
private const int LoadingBarOutlineOffset = 5;
private static readonly Vector2i LogoLoadingBarOffset = (0, 20);
private static readonly Vector2i LoadTimesIndent = (20, 0);
private const int NumLongestLoadTimes = 5;
private static readonly Color LoadingBarColor = Color.White;
#endregion
#region Cvars
private string _splashLogo = "";
private bool _showLoadingBar;
private bool _showDebug;
#endregion
private const string FontLocation = "/EngineFonts/NotoSans/NotoSans-Regular.ttf";
private const int FontSize = 11;
private VectorFont? _font;
// Number of loading sections for the loading bar. This has to be manually set!
private int _numberOfLoadingSections;
// The name of the section and how much time it took to load
internal readonly List<(string Name, TimeSpan LoadTime)> Times = [];
private int _currentSection;
private string? _currentSectionName;
private bool _currentlyInSection;
private bool _finished;
public void Initialize(int sections)
{
if (_finished)
return;
_clyde.VsyncEnabled = false;
_numberOfLoadingSections = sections;
_sawmill = _logManager.GetSawmill("loading");
_splashLogo = _cfg.GetCVar(CVars.DisplaySplashLogo);
_showLoadingBar = _cfg.GetCVar(CVars.LoadingShowBar);
_showDebug = _cfg.GetCVar(CVars.LoadingShowDebug);
if (_resourceCache.TryGetResource<FontResource>(FontLocation, out var fontResource))
_font = new VectorFont(fontResource, FontSize);
else
_sawmill.Error($"Could not load font: {FontLocation}");
}
public void BeginLoadingSection(string sectionName) => BeginLoadingSection(sectionName, false);
public void BeginLoadingSection(string sectionName, bool dontRender)
{
if (_finished)
return;
if (_currentlyInSection)
throw new InvalidOperationException("You cannot begin more than one section at a time!");
_currentlyInSection = true;
_currentSectionName = sectionName;
if (!dontRender)
{
// This ensures that if the screen was resized or something the new size is properly updated to clyde.
_clyde.ProcessInput(new FrameEventArgs((float)_sw.Elapsed.TotalSeconds));
_sw.Restart();
_clyde.Render();
}
else
{
_sw.Restart();
}
}
/// <summary>
/// Start a loading bar "section" for the given method.
/// Must be ended with EndSection.
/// </summary>
public void BeginLoadingSection(object method)
{
if (_finished)
return;
BeginLoadingSection(method.GetType().Name);
}
public void EndLoadingSection()
{
if (_finished)
return;
var time = _sw.Elapsed;
if (_currentSectionName != null)
Times.Add((_currentSectionName, time));
_currentSection++;
_currentlyInSection = false;
}
/// <summary>
/// Will run the giving function and add a custom "section" for it on the loading screen.
/// </summary>
public void LoadingStep(Action action, object method)
{
if (_finished)
return;
BeginLoadingSection(method as string ?? method.GetType().Name);
action();
EndLoadingSection();
}
public void Finish()
{
if (_finished)
return;
if (_currentSection != _numberOfLoadingSections)
_sawmill.Error($"The number of seen loading sections isn't equal to the total number of loading sections! Seen: {_currentSection}, Total: {_numberOfLoadingSections}");
_finished = true;
}
#region Drawing functions
/// <summary>
/// Draw out the splash and loading screen.
/// </summary>
public void DrawLoadingScreen(IRenderHandle handle, Vector2i screenSize)
{
if (_finished)
return;
var scale = UserInterfaceManager.CalculateUIScale(_clyde.MainWindow.ContentScale.X, _cfg);
// Start at the center!
var location = screenSize / 2;
DrawSplash(handle, ref location, scale);
DrawLoadingBar(handle, ref location, scale);
if (_showDebug)
{
DrawCurrentLoading(handle, ref location, scale);
DrawTopTimes(handle, ref location, scale);
}
}
private void DrawSplash(IRenderHandle handle, ref Vector2i startLocation, float scale)
{
if (!_resourceCache.TryGetResource<TextureResource>(_splashLogo, out var textureResource))
return;
var drawSize = textureResource.Texture.Size * scale;
handle.DrawingHandleScreen.DrawTextureRect(textureResource.Texture, UIBox2.FromDimensions(startLocation - drawSize / 2, drawSize));
startLocation += Vector2i.Up * (int) drawSize.Y / 2;
}
private void DrawLoadingBar(IRenderHandle handle, ref Vector2i location, float scale)
{
var barWidth = (int)(LoadingBarWidth * scale);
var barHeight = (int)(LoadingBarHeight * scale);
var outlineOffset = (int)(LoadingBarOutlineOffset * scale);
// Always do the offsets, it looks a lot better!
location.X -= barWidth / 2;
location += (Vector2i) (LogoLoadingBarOffset * scale);
if (!_showLoadingBar)
return;
var sectionWidth = barWidth / _numberOfLoadingSections;
var barTopLeft = location;
var barBottomRight = new Vector2i(_currentSection * sectionWidth % barWidth, barHeight);
var barBottomRightMax = new Vector2i(barWidth, barHeight);
var outlinePosition = barTopLeft + Vector2i.DownLeft * outlineOffset;
var outlineSize = barBottomRightMax + Vector2i.UpRight * 2 * outlineOffset;
// Outline
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(outlinePosition, outlineSize), LoadingBarColor, false);
// Progress bar
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(barTopLeft, barBottomRight), LoadingBarColor);
location += Vector2i.Up * outlineSize;
}
// Draw the currently loading section to the screen.
private void DrawCurrentLoading(IRenderHandle handle, ref Vector2i location, float scale)
{
if (_font == null || _currentSectionName == null)
return;
handle.DrawingHandleScreen.DrawString(_font, location, _currentSectionName, scale, Color.White);
location += Vector2i.Up * _font.GetLineHeight(scale);
}
// Draw the slowest loading times to the screen.
private void DrawTopTimes(IRenderHandle handle, ref Vector2i location, float scale)
{
if (_font == null)
return;
location += (Vector2i)(LoadTimesIndent * scale);
var offset = 0;
var x = 0;
Times.Sort((a, b) => b.LoadTime.CompareTo(a.LoadTime));
foreach (var (name, time) in Times)
{
if (x >= NumLongestLoadTimes)
break;
var entry = $"{time.TotalSeconds:F2} - {name}";
handle.DrawingHandleScreen.DrawString(_font, location + new Vector2i(0, offset), entry, scale, Color.White);
offset += _font.GetLineHeight(scale);
x++;
}
location += Vector2i.Up * offset;
}
#endregion // Drawing functions
}
internal sealed class ShowTopLoadingTimesCommand : IConsoleCommand
{
[Dependency] private readonly LoadingScreenManager _mgr = default!;
public string Command => "loading_top";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var sorted = _mgr.Times.Where(x => x.LoadTime > TimeSpan.FromSeconds(0.01)).OrderByDescending(x => x.LoadTime);
foreach (var (name, time) in sorted)
{
shell.WriteLine($"{time.TotalSeconds:F2} - {name}");
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics.FontManagement;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that proxies to platform-specific implementations,
/// and adds additional logging.
/// </summary>
internal sealed class SystemFontManager : ISystemFontManagerInternal, IPostInjectInit
{
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ISawmill _sawmill = default!;
private ISystemFontManagerInternal _implementation = default!;
public bool IsSupported => _implementation.IsSupported;
public IEnumerable<ISystemFontFace> SystemFontFaces => _implementation.SystemFontFaces;
public void Initialize()
{
_implementation = GetImplementation();
_sawmill.Verbose($"Using {_implementation.GetType()}");
_sawmill.Debug("Initializing system font manager implementation");
try
{
var sw = RStopwatch.StartNew();
_implementation.Initialize();
_sawmill.Debug($"Done initializing system font manager in {sw.Elapsed}");
}
catch (Exception e)
{
// This is a non-critical engine system that has to parse significant amounts of external data.
// Best to fail gracefully to avoid full startup failures.
_sawmill.Error($"Error while initializing system font manager, resorting to fallback: {e}");
_implementation = new SystemFontManagerFallback();
}
}
public void Shutdown()
{
_sawmill.Verbose("Shutting down system font manager");
try
{
_implementation.Shutdown();
}
catch (Exception e)
{
_sawmill.Error($"Exception shutting down system font manager: {e}");
return;
}
_sawmill.Verbose("Successfully shut down system font manager");
}
private ISystemFontManagerInternal GetImplementation()
{
if (!_cfg.GetCVar(CVars.FontSystem))
return new SystemFontManagerFallback();
#if WINDOWS
return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager);
#elif FREEDESKTOP
return new SystemFontManagerFontconfig(_logManager, _fontManager);
#elif MACOS
return new SystemFontManagerCoreText(_logManager, _fontManager);
#else
return new SystemFontManagerFallback();
#endif
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("font.system");
// _sawmill.Level = LogLevel.Verbose;
}
}

View File

@@ -0,0 +1,24 @@
// ReSharper disable InconsistentNaming
#if MACOS
namespace Robust.Client.Interop.MacOS;
/// <summary>
/// Binding to macOS AppKit.
/// </summary>
internal static class AppKit
{
// Values pulled from here:
// https://chromium.googlesource.com/chromium/src/+/b5019b491932dfa597acb3a13a9e7780fb6525a9/ui/gfx/platform_font_mac.mm#53
public const double NSFontWeightUltraLight = -0.8;
public const double NSFontWeightThin = -0.6;
public const double NSFontWeightLight = -0.4;
public const double NSFontWeightRegular = 0;
public const double NSFontWeightMedium = 0.23;
public const double NSFontWeightSemiBold = 0.30;
public const double NSFontWeightBold = 0.40;
public const double NSFontWeightHeavy = 0.56;
public const double NSFontWeightBlack = 0.62;
}
#endif

View File

@@ -0,0 +1,97 @@
#if MACOS
using System.Runtime.InteropServices;
using CFIndex = System.Runtime.InteropServices.CLong;
using Boolean = byte;
namespace Robust.Client.Interop.MacOS;
// ReSharper disable InconsistentNaming
/// <summary>
/// Binding to macOS Core Foundation.
/// </summary>
internal static unsafe class CoreFoundation
{
private const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
public const int kCFNumberFloat32Type = 5;
public static string CFStringToManaged(__CFString* str)
{
var length = CFStringGetLength(str);
return string.Create(
checked((int)length.Value),
(nint)str,
static (span, arg) =>
{
fixed (char* pBuffer = span)
{
CFStringGetCharacters((__CFString*)arg,
new CFRange
{
location = new CFIndex(0),
length = new CFIndex(span.Length),
},
pBuffer);
}
});
}
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFRetain(void* cf);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFRelease(void* cf);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFArrayGetCount(__CFArray* array);
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFArrayGetValueAtIndex(__CFArray* array, CFIndex index);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFStringGetLength(__CFString* str);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFStringGetCharacters(__CFString* str, CFRange range, char* buffer);
[DllImport(CoreFoundationLibrary)]
internal static extern Boolean CFURLGetFileSystemRepresentation(
__CFURL* url,
Boolean resolveAgainstBase,
byte* buffer,
CFIndex maxBufLen);
[DllImport(CoreFoundationLibrary)]
internal static extern __CFString* CFURLGetString(__CFURL* url);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFDictionaryGetCount(__CFDictionary* theDict);
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFDictionaryGetValue(__CFDictionary* theDict, void* key);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFDictionaryGetKeysAndValues(__CFDictionary* theDict, void** keys, void** values);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFNumberGetValue(__CFNumber* number, CLong theType, void* valuePtr);
}
internal struct __CFNumber;
internal struct __CFString;
internal struct __CFURL;
internal struct __CFArray;
internal struct __CFDictionary;
internal struct CFRange
{
public CFIndex location;
public CFIndex length;
}
#endif

View File

@@ -0,0 +1,54 @@
#if MACOS
using System.Runtime.InteropServices;
namespace Robust.Client.Interop.MacOS;
// ReSharper disable InconsistentNaming
/// <summary>
/// Binding to macOS Core Text.
/// </summary>
internal static unsafe class CoreText
{
private const string CoreTextLibrary = "/System/Library/Frameworks/CoreText.framework/CoreText";
public static readonly __CFString* kCTFontURLAttribute;
public static readonly __CFString* kCTFontNameAttribute;
public static readonly __CFString* kCTFontDisplayNameAttribute;
public static readonly __CFString* kCTFontFamilyNameAttribute;
public static readonly __CFString* kCTFontStyleNameAttribute;
public static readonly __CFString* kCTFontTraitsAttribute;
public static readonly __CFString* kCTFontWeightTrait;
public static readonly __CFString* kCTFontWidthTrait;
public static readonly __CFString* kCTFontSlantTrait;
static CoreText()
{
var lib = NativeLibrary.Load(CoreTextLibrary);
kCTFontURLAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontURLAttribute));
kCTFontNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontNameAttribute));
kCTFontDisplayNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontDisplayNameAttribute));
kCTFontFamilyNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontFamilyNameAttribute));
kCTFontStyleNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontStyleNameAttribute));
kCTFontTraitsAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontTraitsAttribute));
kCTFontWeightTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWeightTrait));
kCTFontWidthTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWidthTrait));
kCTFontSlantTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontSlantTrait));
}
[DllImport(CoreTextLibrary)]
public static extern __CTFontCollection* CTFontCollectionCreateFromAvailableFonts(__CFDictionary* options);
[DllImport(CoreTextLibrary)]
public static extern __CFArray* CTFontCollectionCreateMatchingFontDescriptors(__CTFontCollection* collection);
[DllImport(CoreTextLibrary)]
public static extern void* CTFontDescriptorCopyAttribute(__CTFontDescriptor* descriptor, __CFString* attribute);
[DllImport(CoreTextLibrary)]
public static extern __CFDictionary* CTFontDescriptorCopyAttributes(__CTFontDescriptor* descriptor);
}
internal struct __CTFontCollection;
internal struct __CTFontDescriptor;
#endif

View File

@@ -141,7 +141,8 @@ public sealed partial class PhysicsSystem
if ((contact.Flags & ContactFlags.Filter) != 0x0)
{
if (!ShouldCollide(fixtureA, fixtureB) ||
!ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
!ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) ||
!ShouldCollideJoints(uidA, uidB))
{
contact.IsTouching = false;
continue;

View File

@@ -70,6 +70,11 @@
<Import Project="..\MSBuild\Robust.Properties.targets" />
<ItemGroup Condition="'$(IsFreedesktop)' == 'True'">
<PackageReference Include="SpaceWizards.Fontconfig.Interop" />
<RobustLinkAssemblies Include="SpaceWizards.Fontconfig.Interop" />
</ItemGroup>
<Import Project="..\MSBuild\XamlIL.targets" />
<Import Project="..\MSBuild\Robust.Trimming.targets" />

View File

@@ -19,6 +19,9 @@ namespace Robust.Client.UserInterface
get => _stylesheet;
set
{
if (ReferenceEquals(_stylesheet, value))
return;
_stylesheet = value;
StylesheetUpdateRecursive();
}

View File

@@ -27,6 +27,7 @@ namespace Robust.Client.UserInterface.Controls
private ReadOnlyMemory<char> _textMemory;
private bool _clipText;
private AlignMode _align;
private Font? _fontOverride;
public Label()
{
@@ -106,7 +107,16 @@ namespace Robust.Client.UserInterface.Controls
[ViewVariables] public VAlignMode VAlign { get; set; }
public Font? FontOverride { get; set; }
public Font? FontOverride
{
get => _fontOverride;
set
{
_fontOverride = value;
_textDimensionCacheValid = false;
InvalidateMeasure();
}
}
private Font ActualFont
{

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -20,9 +20,10 @@ namespace Robust.Client.UserInterface.Controls
private readonly List<ButtonData> _buttonData = new();
private readonly Dictionary<int, int> _idMap = new();
private readonly Popup _popup;
private readonly BoxContainer _popupVBox;
private readonly BoxContainer _popupContentsBox;
private readonly Label _label;
private readonly TextureRect _triangle;
private readonly LineEdit _filterBox;
public int ItemCount => _buttonData.Count;
@@ -39,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
private bool _hideTriangle;
private bool _filterable;
/// <summary>
/// StyleClasses to apply to the options that popup when clicking this button.
@@ -50,6 +52,17 @@ namespace Robust.Client.UserInterface.Controls
public string Prefix { get; set; } = string.Empty;
public bool PrefixMargin { get; set; } = true;
public bool Filterable
{
get => _filterable;
set
{
_filterable = value;
_filterBox.Visible = value;
UpdateFilters();
}
}
public OptionButton()
{
OptionStyleClasses = new List<string>();
@@ -62,14 +75,26 @@ namespace Robust.Client.UserInterface.Controls
};
AddChild(hBox);
_popupVBox = new BoxContainer
var popupVBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
Orientation = LayoutOrientation.Vertical, Children =
{
(_filterBox = new LineEdit
{
PlaceHolder = Loc.GetString("option-button-filter"),
SelectAllOnFocus = true,
Visible = false,
}),
(_popupContentsBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
})
}
};
OptionsScroll = new()
{
Children = { _popupVBox },
Children = { popupVBox },
ReturnMeasure = true,
MaxHeight = 300
};
@@ -100,6 +125,11 @@ namespace Robust.Client.UserInterface.Controls
Visible = !HideTriangle
};
hBox.AddChild(_triangle);
_filterBox.OnTextChanged += _ =>
{
UpdateFilters();
};
}
public void AddItem(Texture icon, string label, int? id = null)
@@ -140,13 +170,14 @@ namespace Robust.Client.UserInterface.Controls
};
_idMap.Add(id.Value, _buttonData.Count);
_buttonData.Add(data);
_popupVBox.AddChild(button);
_popupContentsBox.AddChild(button);
if (_buttonData.Count == 1)
{
Select(0);
}
ButtonOverride(button);
UpdateFilter(data);
}
private void TogglePopup(bool show)
@@ -164,6 +195,9 @@ namespace Robust.Client.UserInterface.Controls
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
Root.ModalRoot.AddChild(_popup);
_popup.Open(box);
if (_filterable)
_filterBox.GrabKeyboardFocus();
}
else
{
@@ -201,7 +235,7 @@ namespace Robust.Client.UserInterface.Controls
buttonDatum.Button.OnPressed -= ButtonOnPressed;
}
_buttonData.Clear();
_popupVBox.DisposeAllChildren();
_popupContentsBox.DisposeAllChildren();
SelectedId = 0;
}
@@ -229,7 +263,7 @@ namespace Robust.Client.UserInterface.Controls
var data = _buttonData[idx];
data.Button.OnPressed -= ButtonOnPressed;
_idMap.Remove(data.Id);
_popupVBox.RemoveChild(data.Button);
_popupContentsBox.RemoveChild(data.Button);
_buttonData.RemoveAt(idx);
var newIdx = 0;
foreach (var buttonData in _buttonData)
@@ -330,6 +364,25 @@ namespace Robust.Client.UserInterface.Controls
TogglePopup(false);
}
private void UpdateFilters()
{
foreach (var entry in _buttonData)
{
UpdateFilter(entry);
}
}
private void UpdateFilter(ButtonData data)
{
if (!_filterable)
{
data.Button.Visible = true;
return;
}
data.Button.Visible = data.Text.Contains(_filterBox.Text, StringComparison.CurrentCultureIgnoreCase);
}
public sealed class ItemSelectedEventArgs : EventArgs
{
public OptionButton Button { get; }

View File

@@ -266,7 +266,7 @@ namespace Robust.Client.UserInterface.Controls
return _getStyleBox()?.MinimumSize ?? Vector2.Zero;
}
private void _invalidateEntries()
internal void _invalidateEntries()
{
_totalContentHeight = 0;
var font = _getFont();
@@ -336,6 +336,14 @@ namespace Robust.Client.UserInterface.Controls
base.UIScaleChanged();
}
protected override void StylePropertiesChanged()
{
base.StylePropertiesChanged();
// Font may have changed.
_invalidateEntries();
}
internal static float GetScrollSpeed(Font font, float scale)
{
return font.GetLineHeight(scale) * 2;

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
@@ -10,6 +11,7 @@ public sealed partial class FontPrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
[Obsolete("Font prototype is a bad API.")]
[DataField("path", required: true)]
public ResPath Path { get; private set; } = default!;
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -40,12 +41,31 @@ public sealed class FontTag : IMarkupTagHandler
/// Creates the a vector font from the supplied font id.<br/>
/// The size of the resulting font will be either the size supplied as a parameter to the tag, the previous font size or 12
/// </summary>
[Obsolete("Stop using font prototypes")]
public static Font CreateFont(
Stack<Font> contextFontStack,
MarkupNode node,
IResourceCache cache,
IPrototypeManager prototypeManager,
string fontId)
{
var size = GetSizeForFontTag(contextFontStack, node);
var hijack = IoCManager.Resolve<FontTagHijackHolder>();
if (hijack.Hijack?.Invoke(fontId, size) is { } overriden)
return overriden;
if (!prototypeManager.TryIndex<FontPrototype>(fontId, out var prototype))
prototype = prototypeManager.Index<FontPrototype>(DefaultFont);
var fontResource = cache.GetResource<FontResource>(prototype.Path);
return new VectorFont(fontResource, size);
}
/// <summary>
/// Get the desired font size for the given markup node.
/// </summary>
public static int GetSizeForFontTag(Stack<Font> contextFontStack, MarkupNode node)
{
var size = DefaultSize;
@@ -68,10 +88,6 @@ public sealed class FontTag : IMarkupTagHandler
if (node.Attributes.TryGetValue("size", out var sizeParameter))
size = (int) (sizeParameter.LongValue ?? size);
if (!prototypeManager.TryIndex<FontPrototype>(fontId, out var prototype))
prototype = prototypeManager.Index<FontPrototype>(DefaultFont);
var fontResource = cache.GetResource<FontResource>(prototype.Path);
return new VectorFont(fontResource, size);
return size;
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface.RichText;
/// <returns>The font to replace the lookup with. Return null to fall back to default behavior.</returns>
/// <seealso cref="FontTagHijackHolder"/>
public delegate Font? FontTagHijack(ProtoId<FontPrototype> protoId, int size);
/// <summary>
/// Allows replacing font resolution done by <see cref="FontPrototype"/>
/// </summary>
public sealed class FontTagHijackHolder
{
[Dependency] private readonly IUserInterfaceManager _ui = null!;
/// <summary>
/// Called when a font prototype gets resolved.
/// </summary>
public FontTagHijack? Hijack;
/// <summary>
/// Indicate that the results of <see cref="Hijack"/> may have changed,
/// and that engine things relying on it must be updated.
/// </summary>
public void HijackUpdated()
{
// This isn't fool-proof, but it's probably good enough.
// Recursively navigate the UI tree and invalidate rich text controls.
var queue = new Queue<Control>();
foreach (var root in _ui.AllRoots)
{
queue.Enqueue(root);
}
while (queue.TryDequeue(out var control))
{
foreach (var child in control.Children)
{
queue.Enqueue(child);
}
if (control is OutputPanel output)
output._invalidateEntries();
else if (control is RichTextLabel label)
label.InvalidateMeasure();
}
}
}

View File

@@ -254,8 +254,13 @@ namespace Robust.Client.UserInterface
control.Visible = true;
var invertedScale = 1f / uiScale;
control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
control.Measure(new Vector2(Width, Height));
control.Arrange(UIBox2.FromDimensions(
baseLine.X * invertedScale,
(baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale,
control.DesiredSize.X,
control.DesiredSize.Y
));
var advanceX = control.DesiredPixelSize.X;
controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale);
baseLine += new Vector2(advanceX, 0);

View File

@@ -2,6 +2,7 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
@@ -97,11 +98,16 @@ internal partial class UserInterfaceManager
}, true);
}
internal static float CalculateUIScale(float osScale, IConfigurationManager cfg)
{
var cfgScale = cfg.GetCVar(CVars.DisplayUIScale);
return cfgScale == 0 ? osScale : cfgScale;
}
private float CalculateAutoScale(WindowRoot root)
{
//Grab the OS UIScale or the value set through CVAR debug
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
var osScale = CalculateUIScale(root.Window.ContentScale.X, _configurationManager);
var windowSize = root.Window.RenderTarget.Size;
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale

View File

@@ -29,6 +29,11 @@ namespace Robust.Client.UserInterface
{
internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal
{
/// <summary>
/// A type that will always be instantiated anyways.
/// </summary>
public static readonly Type XamlHotReloadWarmupType = typeof(DropDownDebugConsole);
[Dependency] private readonly IDependencyCollection _rootDependencies = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;

View File

@@ -2,7 +2,10 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
@@ -36,6 +39,8 @@ internal sealed class XamlImplementationStorage
/// </summary>
private readonly Dictionary<string, Type> _fileType = new();
private readonly Dictionary<Type, string> _fileTypeReverse = new();
/// <summary>
/// For each type, store the JIT-compiled implementation of Populate.
/// </summary>
@@ -50,6 +55,8 @@ internal sealed class XamlImplementationStorage
private readonly ISawmill _sawmill;
private readonly XamlJitDelegate _jitDelegate;
private readonly Lock _compileLock = new();
/// <summary>
/// Create the storage.
/// </summary>
@@ -102,6 +109,8 @@ internal sealed class XamlImplementationStorage
/// <param name="assembly">an assembly</param>
public void Add(Assembly assembly)
{
using var _ = _compileLock.EnterScope();
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
{
// this can fail, but if it does, that means something is _really_ wrong
@@ -132,6 +141,8 @@ internal sealed class XamlImplementationStorage
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
);
}
_fileTypeReverse.Add(type, fileName);
}
}
@@ -145,6 +156,8 @@ internal sealed class XamlImplementationStorage
/// </remarks>
public void ForceReloadAll()
{
using var _ = _compileLock.EnterScope();
foreach (var (fileName, fileContent) in _fileContent)
{
SetImplementation(fileName, fileContent, true);
@@ -161,9 +174,19 @@ internal sealed class XamlImplementationStorage
/// <returns>true if not a no-op</returns>
public bool CanSetImplementation(string fileName)
{
using var _ = _compileLock.EnterScope();
return _fileType.ContainsKey(fileName);
}
public MethodInfo? CompileType(Type type)
{
if (_fileTypeReverse.TryGetValue(type, out var fileName))
return SetImplementation(fileName, _fileContent[fileName], quiet: true);
_sawmill.Warning($"Type {type} has no XAML file!");
return null;
}
/// <summary>
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
/// <paramref name="fileContent"/>.
@@ -174,12 +197,14 @@ internal sealed class XamlImplementationStorage
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
/// <param name="fileContent">the new implementation</param>
/// <param name="quiet">if true, then don't bother to log</param>
public void SetImplementation(string fileName, string fileContent, bool quiet)
public MethodInfo? SetImplementation(string fileName, string fileContent, bool quiet)
{
using var _ = _compileLock.EnterScope();
if (!_fileType.TryGetValue(fileName, out var type))
{
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
return;
return null;
}
var uri =
@@ -190,12 +215,14 @@ internal sealed class XamlImplementationStorage
{
_sawmill.Debug($"replacing {fileName} for {type}");
}
var impl = _jitDelegate(type, uri, fileName, fileContent);
if (impl != null)
{
_populateImplementations[type] = impl;
}
_fileContent[fileName] = fileContent;
return impl;
}
/// <summary>
@@ -210,8 +237,12 @@ internal sealed class XamlImplementationStorage
{
if (!_populateImplementations.TryGetValue(t, out var implementation))
{
// pop out if we never JITed anything
return false;
// JIT if needed.
implementation = CompileType(t);
// pop out if we never JITed anything/couldn't JIT
if (implementation == null)
return false;
}
implementation.Invoke(null, [null, o]);

View File

@@ -2,6 +2,9 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Reflection;
@@ -17,6 +20,7 @@ public sealed class XamlProxyManager: IXamlProxyManager
ISawmill _sawmill = null!;
[Dependency] IReflectionManager _reflectionManager = null!;
[Dependency] ILogManager _logManager = null!;
[Dependency] private readonly IConfigurationManager _cfg = null!;
XamlImplementationStorage _xamlImplementationStorage = null!;
@@ -31,8 +35,21 @@ public sealed class XamlProxyManager: IXamlProxyManager
_sawmill = _logManager.GetSawmill("xamlhotreload");
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
AddAssemblies();
var preload = _cfg.GetCVar(CVars.UIXamlJitPreload);
AddAssemblies(reload: preload);
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
if (!preload)
{
// Compile any type at all on another thread, so we don't hold up main thread init with loading
// the entire XAML compiler machinery.
// In my testing, it took like 0.5s on debug to run the first XAML compile. Yeah.
ThreadPool.QueueUserWorkItem(_ =>
{
_xamlImplementationStorage.CompileType(UserInterfaceManager.XamlHotReloadWarmupType);
});
}
}
/// <summary>
@@ -61,7 +78,7 @@ public sealed class XamlProxyManager: IXamlProxyManager
/// Add all the types from all known assemblies, then force-JIT everything
/// again.
/// </summary>
private void AddAssemblies()
private void AddAssemblies(bool reload = true)
{
foreach (var a in _reflectionManager.Assemblies)
{
@@ -74,8 +91,8 @@ public sealed class XamlProxyManager: IXamlProxyManager
}
}
// Always use the JITed versions on debug builds
_xamlImplementationStorage.ForceReloadAll();
if (reload)
_xamlImplementationStorage.ForceReloadAll();
}
/// <summary>

View File

@@ -58,13 +58,11 @@ internal sealed class ReloadManager : IReloadManager
{
foreach (var file in _reloadQueue)
{
var rootedFile = file.ToRootedPath();
if (!_res.ContentFileExists(rootedFile))
if (!_res.ContentFileExists(file))
continue;
_sawmill.Info($"Reloading {rootedFile}");
OnChanged?.Invoke(rootedFile);
_sawmill.Info($"Reloading {file}");
OnChanged?.Invoke(file);
}
_reloadQueue.Clear();
@@ -133,12 +131,13 @@ internal sealed class ReloadManager : IReloadManager
var relPath = Path.GetRelativePath(rootIter, args.FullPath);
if (relPath == args.FullPath)
{
// Not relative.
// Different root (i.e., "C:/" and "D:/")
continue;
}
var file = ResPath.FromRelativeSystemPath(relPath);
_reloadQueue.Add(file);
var file = ResPath.FromRelativeSystemPath(relPath).ToRootedPath();
if (!file.CanonPath.Contains("/../"))
_reloadQueue.Add(file);
}
});
}

View File

@@ -166,8 +166,12 @@ namespace Robust.Server
public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
Options = options;
_config.Initialize(true);
_config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server
_config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared
if (Options.LoadConfigAndUserData)
{
string? path = _commandLineArgs?.ConfigFile;
@@ -192,9 +196,6 @@ namespace Robust.Server
}
}
_config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server
_config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared
CVarDefaultOverrides.OverrideServer(_config);
_config.OverrideConVars(EnvironmentVariables.GetEnvironmentCVars());

View File

@@ -87,6 +87,9 @@ internal sealed partial class PvsSystem
catch (Exception e)
{
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}

View File

@@ -48,6 +48,9 @@ internal sealed partial class PvsSystem
{
var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}

View File

@@ -6,4 +6,7 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
#if DEVELOPMENT
[assembly: InternalsVisibleTo("Content.Benchmarks")]
#endif

View File

@@ -1,3 +1,22 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Robust.UnitTesting", AllInternalsVisible = true)]
// The following allows another friend assembly access to the types marked as internal.
// SS14 engine assemblies are friends.
// This way internal is "Content can't touch this".
[assembly: InternalsVisibleTo("Robust.Server")]
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.Lite")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]
#if NET5_0_OR_GREATER
[module: SkipLocalsInit]
#endif
#if DEVELOPMENT
[assembly: InternalsVisibleTo("Content.Benchmarks")]
#endif

View File

@@ -1204,7 +1204,7 @@ namespace Robust.Shared
CVarDef.Create("display.use_US_QWERTY_hotkeys", false, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<string> DisplayWindowingApi =
CVarDef.Create("display.windowing_api", "glfw", CVar.CLIENTONLY);
CVarDef.Create("display.windowing_api", "sdl3", CVar.CLIENTONLY);
/// <summary>
/// If true and on Windows 11 Build 22000,
@@ -1280,13 +1280,6 @@ namespace Robust.Shared
* PHYSICS
*/
/// <summary>
/// How much to expand broadphase checking for. This is useful for cross-grid collisions.
/// Performance impact if additional broadphases are being checked.
/// </summary>
public static readonly CVarDef<float> BroadphaseExpand =
CVarDef.Create("physics.broadphase_expand", 2f, CVar.ARCHIVE | CVar.REPLICATED);
/// <summary>
/// The target minimum ticks per second on the server.
/// This is used for substepping and will help with clipping/physics issues and such.
@@ -1836,6 +1829,15 @@ namespace Robust.Shared
/// </remarks>
public static readonly CVarDef<bool> CfgCheckUnused = CVarDef.Create("cfg.check_unused", true);
/// <summary>
/// Storage for CVars that should be rolled back next client startup.
/// </summary>
/// <remarks>
/// This CVar is utilized through <see cref="IConfigurationManager"/>'s rollback functionality.
/// </remarks>
internal static readonly CVarDef<string>
CfgRollbackData = CVarDef.Create("cfg.rollback_data", "", CVar.ARCHIVE);
/*
* Network Resource Manager
*/
@@ -1915,5 +1917,50 @@ namespace Robust.Shared
/// </summary>
public static readonly CVarDef<string> XamlHotReloadMarkerName =
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
/// <summary>
/// If true, all XAML UIs will be JITed for hot reload on client startup.
/// If false, they will be JITed on demand.
/// </summary>
public static readonly CVarDef<bool> UIXamlJitPreload =
CVarDef.Create("ui.xaml_jit_preload", false, CVar.CLIENTONLY);
/*
* FONT
*/
/// <summary>
/// If false, disable system font support.
/// </summary>
public static readonly CVarDef<bool> FontSystem =
CVarDef.Create("font.system", true, CVar.CLIENTONLY);
/// <summary>
/// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API.
/// </summary>
public static readonly CVarDef<bool> FontWindowsDownloadable =
CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* LOADING
*/
/// <summary>
/// Whether to show explicit loading bar during client initialization.
/// </summary>
public static readonly CVarDef<bool> LoadingShowBar =
CVarDef.Create("loading.show_bar", true, CVar.CLIENTONLY);
#if TOOLS
private const bool DefaultShowDebug = true;
#else
private const bool DefaultShowDebug = false;
#endif
/// <summary>
/// Whether to show "debug" info in the loading screen.
/// </summary>
public static readonly CVarDef<bool> LoadingShowDebug =
CVarDef.Create("loading.show_debug", DefaultShowDebug, CVar.CLIENTONLY);
}
}

View File

@@ -7,9 +7,11 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Collections;
using System.Numerics;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Robust.Shared.ComponentTrees;
@@ -19,7 +21,7 @@ namespace Robust.Shared.ComponentTrees;
[UsedImplicitly]
public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
where TComp : Component, IComponentTreeEntry<TComp>, new()
where TComp : Component, IComponentTreeEntry<TComp>
{
[Dependency] private readonly RecursiveMoveSystem _recursiveMoveSys = default!;
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
@@ -27,9 +29,17 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
private readonly HashSet<EntityUid> _updated = new();
protected EntityQuery<TComp> Query;
/// <summary>
/// Whether this lookup tree should even be enabled.
/// </summary>
/// <remarks>
/// This can be used to disable some trees if they are not required, which helps improve performance a bit.
/// </remarks>
protected virtual bool Enabled => true;
private bool _initialized;
/// <summary>
/// If true, this system will update the tree positions every frame update. See also <see cref="DoTickUpdate"/>. Some systems may need to do both.
/// </summary>
@@ -55,6 +65,10 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
{
base.Initialize();
if (!Enabled)
return;
_initialized = true;
UpdatesOutsidePrediction = DoTickUpdate;
UpdatesAfter.Add(typeof(SharedTransformSystem));
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
@@ -86,10 +100,21 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
public override void Shutdown()
{
if (!_initialized)
return;
_initialized = false;
if (Recursive)
{
_recursiveMoveSys.OnTreeRecursiveMove -= HandleRecursiveMove;
}
}
private bool CheckEnabled()
{
if (_initialized)
return true;
Log.Error($"Attempted to use disabled lookup tree");
return false;
}
#region Queue Update
@@ -105,6 +130,9 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null)
{
if (!_initialized)
return;
if (component.TreeUpdateQueued || !Resolve(uid, ref xform))
return;
@@ -132,12 +160,10 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
protected virtual void OnTreeRemove(EntityUid uid, TTreeComp component, ComponentRemove args)
{
if (Terminating(uid))
return;
foreach (var entry in component.Tree)
{
entry.Component.TreeUid = null;
entry.Component.Tree = null;
}
component.Tree.Clear();
@@ -145,6 +171,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args)
{
// IIRC, this is to prevent a tree-update spam as each of the entity's children get detached to nullspace.
RemComp(uid, component);
}
@@ -162,13 +189,13 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
#region Update Trees
public override void Update(float frameTime)
{
if (DoTickUpdate)
if (DoTickUpdate && _initialized)
UpdateTreePositions();
}
public override void FrameUpdate(float frameTime)
{
if (DoFrameUpdate)
if (DoFrameUpdate && _initialized)
UpdateTreePositions();
}
@@ -178,23 +205,25 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
/// </summary>
public void UpdateTreePositions()
{
if (!CheckEnabled())
return;
if (_updateQueue.Count == 0)
return;
var xforms = GetEntityQuery<TransformComponent>();
var trees = GetEntityQuery<TTreeComp>();
while (_updateQueue.TryDequeue(out var entry))
{
var (comp, xform) = entry;
// Was this entity queued multiple times?
DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?");
comp.TreeUpdateQueued = false;
if (!comp.Running)
continue;
if (!_updated.Add(entry.Uid))
continue;
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
{
RemoveFromTree(comp);
@@ -211,8 +240,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
{
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value,
xforms);
newTree!.Value);
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
continue;
@@ -228,13 +256,10 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value,
xforms);
newTree!.Value);
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
}
_updated.Clear();
}
private void RemoveFromTree(TComp component)
@@ -253,8 +278,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
var (pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
entry.Component.TreeUid.Value);
return ExtractAabb(in entry, pos, rot);
}
@@ -268,6 +292,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
public IEnumerable<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2 worldAABB)
{
if (!CheckEnabled())
return [];
// Anything that queries these trees should only do so if there are no queued updates, otherwise it can lead to
// errors. Currently there is no easy way to enforce this, but this should work as long as nothing queries the
// trees directly:
@@ -299,26 +325,105 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
return state.trees;
}
#region HashSet
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
public void QueryAabb(HashSet<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true)
{
var state = new HashSet<ComponentTreeEntry<TComp>>();
QueryAabb(state, mapId, worldBounds, approx);
return state;
}
[Obsolete("Use Entity<T> variant")]
internal void QueryAabb(
HashSet<ComponentTreeEntry<TComp>> results,
MapId mapId,
Box2Rotated worldBounds,
bool approx = true)
{
if (!CheckEnabled())
return;
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
treeComp.Tree.QueryAabb(ref state, static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds, approx);
treeComp.Tree.QueryAabb(ref results,
static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
return state;
}
public void QueryAabb(
HashSet<Entity<TComp, TransformComponent>> results,
MapId mapId,
Box2Rotated worldBounds,
bool approx = true)
{
if (!CheckEnabled())
return;
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
treeComp.Tree.QueryAabb(ref results,
static (ref HashSet<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
}
#endregion
#region List
public void QueryAabb(List<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
public void QueryAabb(
List<Entity<TComp, TransformComponent>> results,
MapId mapId,
Box2Rotated worldBounds,
bool approx = true)
{
if (!CheckEnabled())
return;
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
treeComp.Tree.QueryAabb(ref results,
static (ref List<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
{
state.Add(value);
return true;
},
bounds,
approx);
}
}
#endregion
public void QueryAabb<TState>(
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
@@ -336,6 +441,9 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
Box2Rotated worldBounds,
bool approx = true)
{
if (!CheckEnabled())
return;
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
{
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
@@ -343,14 +451,92 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
}
}
#endregion
#region Rays
[Obsolete("use IntersectRay")]
public List<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
{
var list = new List<RayCastResults>();
if (!returnOnFirstHit)
{
IntersectRay(list, mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
return list;
}
var result = IntersectRay(mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
if (result != null)
list.Add(result.Value);
return list;
}
/// <summary>
/// Perform a ray intersection and return on the first hit.
/// </summary>
public RayCastResults? IntersectRay(MapId mapId, in Ray ray, float length)
{
var state = new QueryState(length);
IntersectRayInternal(mapId, in ray, length, ref state, QueryCallback);
return state.Result;
}
/// <summary>
/// Perform a ray intersection and populate a provided list of results.
/// </summary>
public void IntersectRay(List<RayCastResults> results, MapId mapId, in Ray ray, float maxLength)
{
results.Clear();
var state = new QueryState(maxLength, results);
IntersectRayInternal(mapId, in ray, maxLength, ref state, QueryCallback);
}
/// <summary>
/// Perform a ray intersection with a predicate and return on the first hit.
/// </summary>
public RayCastResults? IntersectRay<TState>(
MapId mapId,
in Ray ray,
float length,
TState predicateState,
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
{
var state = new QueryState<TState>(new(length), predicateState, ignore);
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
return state.Inner.Result;
}
/// <summary>
/// Perform a ray intersection with a predicate and populate a provided list of results.
/// </summary>
public void IntersectRay<TState>(
List<RayCastResults> results,
MapId mapId,
in Ray ray,
float length,
TState predicateState,
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
{
var state = new QueryState<TState>(new(length, results), predicateState, ignore);
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
}
private void IntersectRayInternal<TState>(
MapId mapId,
in Ray ray,
float maxLength,
ref TState state,
DynamicTree<ComponentTreeEntry<TComp>>.RayQueryCallbackDelegate<TState> callback)
where TState : IDone
{
if (mapId == MapId.Nullspace)
return new ();
var queryState = new QueryState<TState>(maxLength, returnOnFirstHit, state, predicate);
return;
if (!CheckEnabled())
return;
var endPoint = ray.Position + ray.Direction * maxLength;
var worldBox = new Box2(Vector2.Min(ray.Position, endPoint), Vector2.Max(ray.Position, endPoint));
@@ -359,43 +545,79 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
var (_, treeRot, matrix) = XformSystem.GetWorldPositionRotationInvMatrix(treeUid);
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
var treeRay = new Ray(Vector2.Transform(ray.Position, matrix), relativeAngle);
comp.Tree.QueryRay(ref queryState, QueryCallback, treeRay);
if (returnOnFirstHit && queryState.List.Count > 0)
break;
}
return queryState.List;
static bool QueryCallback(
ref QueryState<TState> state,
in ComponentTreeEntry<TComp> value,
in Vector2 point,
float distFromOrigin)
{
if (distFromOrigin > state.MaxLength || state.Predicate.Invoke(value.Uid, state.State))
return true;
state.List.Add(new RayCastResults(distFromOrigin, point, value.Uid));
return !state.ReturnOnFirstHit;
comp.Tree.QueryRay(ref state, callback, treeRay);
if (state.Done)
return;
}
}
private readonly struct QueryState<TState>
static bool QueryCallback(
ref QueryState state,
in ComponentTreeEntry<TComp> value,
in Vector2 point,
float dist)
{
public readonly float MaxLength;
public readonly bool ReturnOnFirstHit;
public readonly List<RayCastResults> List = new();
public readonly TState State;
public readonly Func<EntityUid, TState, bool> Predicate;
if (dist > state.MaxLength)
return true;
public QueryState(float maxLength, bool returnOnFirstHit, TState state, Func<EntityUid, TState, bool> predictate)
if (state.ReturnOnFirstHit)
{
MaxLength = maxLength;
ReturnOnFirstHit = returnOnFirstHit;
State = state;
Predicate = predictate;
state.Result = new RayCastResults(dist, point, value.Uid);
return false;
}
state.List.Add(new RayCastResults(dist, point, value.Uid));
return true;
}
private static bool PredicateQueryCallback<TState>(
ref QueryState<TState> state,
in ComponentTreeEntry<TComp> value,
in Vector2 point,
float dist)
{
if (dist > state.Inner.MaxLength)
return true;
if (state.Ignore.Invoke(value, state.PredicateState))
return true;
if (state.Inner.ReturnOnFirstHit)
{
state.Inner.Result = new RayCastResults(dist, point, value.Uid);
return false;
}
state.Inner.List.Add(new RayCastResults(dist, point, value.Uid));
return true;
}
private struct QueryState<TPredicateState>(
QueryState inner,
TPredicateState predicateState,
Func<Entity<TComp, TransformComponent>, TPredicateState, bool> ignore) : IDone
{
public readonly TPredicateState PredicateState = predicateState;
public readonly Func<Entity<TComp, TransformComponent>, TPredicateState, bool> Ignore = ignore;
public QueryState Inner = inner;
public bool Done => Inner.Done;
}
private struct QueryState(float maxLength, List<RayCastResults>? list = null) : IDone
{
public readonly float MaxLength = maxLength;
[MemberNotNullWhen(false, nameof(List))]
public readonly bool ReturnOnFirstHit => List == null;
public readonly List<RayCastResults>? List = list;
public RayCastResults? Result;
public bool Done => Result != null;
}
private interface IDone
{
bool Done { get; }
}
#endregion
}

View File

@@ -24,7 +24,7 @@ public interface IComponentTreeEntry<TComp> where TComp : Component
public DynamicTree<ComponentTreeEntry<TComp>>? Tree { get; set; }
/// <summary>
/// Whether or not the component should currently be added to a tree.
/// Whether the component should currently be added to a tree.
/// </summary>
public bool AddToTree { get; }

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Console;
@@ -58,6 +59,25 @@ namespace Robust.Shared.Configuration
throw new NotSupportedException();
}
internal static IEnumerable<CompletionOption> GetCVarCompletionOptions(IConfigurationManager cfg)
{
return cfg.GetRegisteredCVars()
.Select(c => new CompletionOption(c, GetCVarValueHint(cfg, c)));
}
private static string GetCVarValueHint(IConfigurationManager cfg, string cVar)
{
var flags = cfg.GetCVarFlags(cVar);
if ((flags & CVar.CONFIDENTIAL) != 0)
return Loc.GetString("cmd-cvar-value-hidden");
var value = cfg.GetCVar<object>(cVar).ToString() ?? "";
if (value.Length > 50)
value = $"{value[..51]}…";
return value;
}
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
@@ -120,8 +140,7 @@ namespace Robust.Shared.Configuration
var helpQuestion = Loc.GetString("cmd-cvar-compl-list");
return CompletionResult.FromHintOptions(
_cfg.GetRegisteredCVars()
.Select(c => new CompletionOption(c, GetCVarValueHint(_cfg, c)))
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
.Union(new[] { new CompletionOption("?", helpQuestion) })
.OrderBy(c => c.Value),
Loc.GetString("cmd-cvar-arg-name"));
@@ -134,19 +153,6 @@ namespace Robust.Shared.Configuration
var type = _cfg.GetCVarType(cvar);
return CompletionResult.FromHint($"<{type.Name}>");
}
private string GetCVarValueHint(IConfigurationManager cfg, string cVar)
{
var flags = cfg.GetCVarFlags(cVar);
if ((flags & CVar.CONFIDENTIAL) != 0)
return Loc.GetString("cmd-cvar-value-hidden");
var value = cfg.GetCVar<object>(cVar).ToString() ?? "";
if (value.Length > 50)
value = $"{value[..51]}…";
return value;
}
}
internal sealed class CVarSubsCommand : LocalizedCommands
@@ -191,4 +197,83 @@ namespace Robust.Shared.Configuration
return CompletionResult.Empty;
}
}
internal sealed class ConfigMarkRollbackCommand : IConsoleCommand
{
[Dependency] private readonly IConfigurationManager _cfg = null!;
public string Command => "config_rollback_mark";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length is < 1 or > 2)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
_cfg.MarkForRollback(args[0]);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromOptions(
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
.OrderBy(c => c.Value));
}
return CompletionResult.Empty;
}
}
internal sealed class ConfigUnmarkRollbackCommand : IConsoleCommand
{
[Dependency] private readonly IConfigurationManager _cfg = null!;
public string Command => "config_rollback_unmark";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length is < 1 or > 2)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
_cfg.UnmarkForRollback(args[0]);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromOptions(
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
.OrderBy(c => c.Value));
}
return CompletionResult.Empty;
}
}
internal sealed class ConfigApplyRollbackCommand : IConsoleCommand
{
[Dependency] private readonly IConfigurationManager _cfg = null!;
public string Command => "config_rollback_apply";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
_cfg.ApplyRollback();
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nett;
namespace Robust.Shared.Configuration;
internal partial class ConfigurationManager
{
public void MarkForRollback(params CVarDef[] cVars)
{
MarkForRollback(cVars.Select(c => c.Name).ToArray());
}
public void MarkForRollback(params string[] cVars)
{
var alreadyPending = LoadPendingRollbackTable() ?? [];
foreach (var cVar in cVars)
{
alreadyPending[cVar] = GetCVar(cVar);
}
SavePendingRollbackTable(alreadyPending);
}
public void UnmarkForRollback(params CVarDef[] cVars)
{
UnmarkForRollback(cVars.Select(c => c.Name).ToArray());
}
public void UnmarkForRollback(params string[] cVars)
{
var alreadyPending = LoadPendingRollbackTable() ?? [];
foreach (var cVar in cVars)
{
alreadyPending.Remove(cVar);
}
SavePendingRollbackTable(alreadyPending);
}
private void SavePendingRollbackTable(Dictionary<string, object> pending)
{
var tbl = SaveToTomlTable(pending.Keys, cVar => pending[cVar]);
var str = Toml.WriteString(tbl);
SetCVar(CVars.CfgRollbackData, str);
}
public void ApplyRollback()
{
var rollbackValue = GetCVar(CVars.CfgRollbackData);
if (string.IsNullOrWhiteSpace(rollbackValue))
return;
_sawmill.Debug("We have CVars to roll back!");
try
{
var tblRoot = Toml.ReadString(rollbackValue);
var loaded = LoadFromTomlTable(tblRoot);
_sawmill.Info($"Rolled back CVars: {string.Join(", ", loaded)}");
}
catch (Exception e)
{
_sawmill.Error($"Failed to load rollback data:\n{e}");
}
finally
{
SetCVar(CVars.CfgRollbackData, "");
SaveToFile();
}
}
private Dictionary<string, object>? LoadPendingRollbackTable()
{
var rollbackValue = GetCVar(CVars.CfgRollbackData);
if (string.IsNullOrWhiteSpace(rollbackValue))
return null;
try
{
var tblRoot = Toml.ReadString(rollbackValue);
return ParseCVarValuesFromToml(tblRoot).ToDictionary();
}
catch (Exception e)
{
_sawmill.Error($"Failed to load rollback data:\n{e}");
return null;
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Robust.Shared.Configuration
/// Stores and manages global configuration variables.
/// </summary>
[Virtual]
internal class ConfigurationManager : IConfigurationManagerInternal
internal partial class ConfigurationManager : IConfigurationManagerInternal
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
@@ -60,36 +60,53 @@ namespace Robust.Shared.Configuration
/// <inheritdoc />
public HashSet<string> LoadFromTomlStream(Stream file)
{
var loaded = new HashSet<string>();
TomlTable tblRoot;
try
{
var callbackEvents = new ValueList<ValueChangedInvoke>();
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
foreach (var (cvar, value) in ParseCVarValuesFromToml(file))
{
loaded.Add(cvar);
LoadTomlVar(cvar, value, ref callbackEvents);
}
}
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
tblRoot = Toml.ReadStream(file);
}
catch (Exception e)
{
loaded.Clear();
_sawmill.Error("Unable to load configuration from stream:\n{0}", e);
_sawmill.Error("Unable to load configuration from table:\n{0}", e);
return [];
}
return LoadFromTomlTable(tblRoot);
}
private HashSet<string> LoadFromTomlTable(TomlTable table)
{
var loaded = new HashSet<string>();
var callbackEvents = new ValueList<ValueChangedInvoke>();
try
{
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
foreach (var (cvar, value) in ParseCVarValuesFromToml(table))
{
loaded.Add(cvar);
LoadParsedVar(cvar, value, ref callbackEvents);
}
}
}
finally
{
RunDeferredInvokeCallbacks(in callbackEvents);
}
return loaded;
}
private void LoadTomlVar(
private void RunDeferredInvokeCallbacks(in ValueList<ValueChangedInvoke> callbackEvents)
{
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
}
private void LoadParsedVar(
string cvar,
object value,
ref ValueList<ValueChangedInvoke> changedInvokes)
@@ -108,7 +125,7 @@ namespace Robust.Shared.Configuration
}
catch
{
_sawmill.Error($"TOML parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Toml type: {value.GetType()}");
_sawmill.Error($"Parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Parsed type: {value.GetType()}");
return;
}
}
@@ -119,16 +136,24 @@ namespace Robust.Shared.Configuration
else
{
//or add another unregistered CVar
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
cfgVar = new ConfigVar(cvar, null!, CVar.NONE) { Value = value };
_configVars.Add(cvar, cfgVar);
cfgVar = AddUnregisteredCVar(cvar, value);
}
cfgVar.ConfigModified = true;
}
private ConfigVar AddUnregisteredCVar(string name, object value)
{
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
var cfgVar = new ConfigVar(name, null!, CVar.NONE) { Value = value };
_configVars.Add(name, cfgVar);
return cfgVar;
}
public HashSet<string> LoadDefaultsFromTomlStream(Stream stream)
{
var tblRoot = Toml.ReadStream(stream);
var loaded = new HashSet<string>();
var callbackEvents = new ValueList<ValueChangedInvoke>();
@@ -136,7 +161,7 @@ namespace Robust.Shared.Configuration
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
foreach (var (cVarName, value) in ParseCVarValuesFromToml(stream))
foreach (var (cVarName, value) in ParseCVarValuesFromToml(tblRoot))
{
if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered)
{
@@ -181,9 +206,13 @@ namespace Robust.Shared.Configuration
{
try
{
using var file = File.OpenRead(configFile);
var result = LoadFromTomlStream(file);
HashSet<string> result;
using (var file = File.OpenRead(configFile))
{
result = LoadFromTomlStream(file);
}
SetSaveFile(configFile);
ApplyRollback();
_sawmill.Info($"Configuration loaded from file");
return result;
}
@@ -223,6 +252,13 @@ namespace Robust.Shared.Configuration
/// <inheritdoc />
public void SaveToTomlStream(Stream stream, IEnumerable<string> cvars)
{
var table = SaveToTomlTable(cvars);
Toml.WriteStream(table, stream);
}
private TomlTable SaveToTomlTable(IEnumerable<string> cvars, Func<string, object>? overrideValue = null)
{
var tblRoot = Toml.Create();
@@ -233,10 +269,18 @@ namespace Robust.Shared.Configuration
if (!_configVars.TryGetValue(name, out var cVar))
continue;
var value = cVar.Value;
if (value == null && cVar.Registered)
object? value;
if (overrideValue != null)
{
value = cVar.DefaultValue;
value = overrideValue(name);
}
else
{
value = cVar.Value;
if (value == null && cVar.Registered)
{
value = cVar.DefaultValue;
}
}
if (value == null)
@@ -263,6 +307,8 @@ namespace Robust.Shared.Configuration
}
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
// If you add a type here, add it to .Rollback.cs too!!!
// I can't share the code because of how the serialization layers work :(
switch (value)
{
case Enum val:
@@ -293,7 +339,7 @@ namespace Robust.Shared.Configuration
}
}
Toml.WriteStream(tblRoot, stream);
return tblRoot;
}
/// <inheritdoc />
@@ -495,7 +541,7 @@ namespace Robust.Shared.Configuration
public void LoadCVarsFromType(Type containingType)
{
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.Static))
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
{
var fieldType = defField.FieldType;
if (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(CVarDef<>))
@@ -754,10 +800,25 @@ namespace Robust.Shared.Configuration
private void InvokeValueChanged(in ValueChangedInvoke invoke)
{
OnCVarValueChanged?.Invoke(invoke.Info);
try
{
OnCVarValueChanged?.Invoke(invoke.Info);
}
catch (Exception e)
{
_sawmill.Error($"Error while running OnCVarValueChanged callback: {e}");
}
foreach (var entry in invoke.Invoke.Entries)
{
entry.Value!.Invoke(invoke.Value, in invoke.Info);
try
{
entry.Value!.Invoke(invoke.Value, in invoke.Info);
}
catch (Exception e)
{
_sawmill.Error($"Error while running OnValueChanged callback: {e}");
}
}
}
@@ -768,10 +829,8 @@ namespace Robust.Shared.Configuration
return new ValueChangedInvoke(info, var.ValueChanged);
}
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(Stream stream)
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(TomlTable tblRoot)
{
var tblRoot = Toml.ReadStream(stream);
return ProcessTomlObject(tblRoot, "");
IEnumerable<(string cvar, object value)> ProcessTomlObject(TomlObject obj, string tablePath)

View File

@@ -272,5 +272,113 @@ namespace Robust.Shared.Configuration
where T : notnull;
public event Action<CVarChangeInfo>? OnCVarValueChanged;
//
// Rollback
//
/// <summary>
/// Snapshot a CVar to be rolled back later, even in the event of a client crash.
/// </summary>
/// <remarks>
/// <para>
/// This set of APIs is intended for settings menus that want to show the user a
/// "Do these settings look correct?" prompt with timeout,
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
/// </para>
/// <para>
/// Rollback is applied on <see cref="ApplyRollback"/> call or client restart,
/// unless CVars are unmarked again via <see cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
/// </para>
/// <para>
/// Rollback is tracked in the config file too, and this command does not save it automatically. Of course,
/// not saving the config file before client exit also effectively rolls back CVars.
/// </para>
/// <para>
/// Calling this method if a CVar is already marked for rollback will simply update the snapshot value.
/// </para>
/// </remarks>
/// <param name="cVars">The CVars to roll back.</param>
/// <seealso cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
void MarkForRollback(params CVarDef[] cVars);
/// <summary>
/// Snapshot a CVar to be rolled back later, even in the event of a client crash.
/// </summary>
/// <remarks>
/// <para>
/// This set of APIs is intended for settings menus that want to show the user a
/// "Do these settings look correct?" prompt with timeout,
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
/// </para>
/// <para>
/// Rollback is applied on <see cref="ApplyRollback"/> call or client restart,
/// unless CVars are unmarked again via <see cref="UnmarkForRollback(string[])"/>
/// </para>
/// <para>
/// Rollback is tracked in the config file too, and this command does not save it automatically. Of course,
/// not saving the config file before client exit also effectively rolls back CVars.
/// </para>
/// <para>
/// Calling this method if a CVar is already marked for rollback will simply update the snapshot value.
/// </para>
/// </remarks>
/// <param name="cVars">The CVar names to snapshot and (possibly) roll back later.</param>
/// <seealso cref="UnmarkForRollback(string[])"/>
void MarkForRollback(params string[] cVars);
/// <summary>
/// Unmark a CVar for rollback.
/// </summary>
/// <remarks>
/// <para>
/// This set of APIs is intended for settings menus that want to show the user a
/// "Do these settings look correct?" prompt with timeout,
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
/// </para>
/// <para>
/// Rollback is tracked in the config file too, and this command does not save it automatically.
/// Users must still call <see cref="SaveToFile"/> manually to avoid rollback happening.
/// </para>
/// </remarks>
/// <param name="cVars">The CVars to unmark for rollback.</param>
/// <seealso cref="MarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
/// <seealso cref="ApplyRollback"/>
void UnmarkForRollback(params CVarDef[] cVars);
/// <summary>
/// Unmark a CVar for rollback.
/// </summary>
/// <remarks>
/// <para>
/// This set of APIs is intended for settings menus that want to show the user a
/// "Do these settings look correct?" prompt with timeout,
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
/// </para>
/// <para>
/// Rollback is tracked in the config file too, and this command does not save it automatically.
/// Users must still call <see cref="SaveToFile"/> manually to avoid rollback happening.
/// </para>
/// </remarks>
/// <param name="cVars">The CVars to unmark for rollback.</param>
/// <seealso cref="MarkForRollback(string[])"/>
/// <seealso cref="ApplyRollback"/>
void UnmarkForRollback(params string[] cVars);
/// <summary>
/// Apply all pending CVar rollbacks.
/// </summary>
/// <para>
/// This set of APIs is intended for settings menus that want to show the user a
/// "Do these settings look correct?" prompt with timeout,
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
/// </para>
/// <remarks>
/// This implicitly saves the config file to ensure the config file does not contain
/// rollback data for longer than necessary.
/// </remarks>
/// <seealso cref="MarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
/// <seealso cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
void ApplyRollback();
}
}

View File

@@ -19,7 +19,7 @@ public abstract class LocalizedCommands : IConsoleCommand
public virtual string Description => LocalizationManager.TryGetString($"cmd-{Command}-desc", out var val) ? val : "";
/// <inheritdoc />
public virtual string Help => LocalizationManager.TryGetString($"cmd-{Command}-help", out var val) ? val : "";
public virtual string Help => LocalizationManager.TryGetString($"cmd-{Command}-help", out var val, ("command", Command)) ? val : "";
/// <inheritdoc />
public virtual bool RequireServerOrSingleplayer => false;

View File

@@ -219,6 +219,9 @@ public abstract partial class SharedContainerSystem
internal void RecursivelyUpdateJoints(Entity<TransformComponent> entity)
{
if (_timing.ApplyingState)
return;
if (JointQuery.TryGetComponent(entity, out var jointComp))
{
// TODO: This is going to be going up while joints going down, although these aren't too common

View File

@@ -95,10 +95,7 @@ public abstract partial class SharedContainerSystem
_lookup.FindAndAddToEntityTree(toRemove, xform: xform);
}
if (TryComp<JointComponent>(toRemove, out var jointComp))
{
_joint.RefreshRelay(toRemove, jointComp);
}
RecursivelyUpdateJoints((toRemove, xform));
// Raise container events (after re-parenting and internal remove).
RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true);

View File

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

View File

@@ -11,7 +11,7 @@ namespace Robust.Shared.ContentPack
{
internal sealed partial class AssemblyTypeChecker
{
public static IEnumerable<string> DumpMetaMembers(Type type)
public static IEnumerable<(string Value, bool IsField)> DumpMetaMembers(Type type)
{
var assemblyLoc = type.Assembly.Location;
@@ -58,7 +58,7 @@ namespace Robust.Shared.ContentPack
var fieldName = metaReader.GetString(fieldDef.Name);
var fieldType = fieldDef.DecodeSignature(provider, 0);
yield return $"{fieldType.WhitelistToString()} {fieldName}";
yield return ($"{fieldType.WhitelistToString()} {fieldName}", IsField: true);
}
foreach (var methodHandle in typeDef.GetMethods())
@@ -79,7 +79,7 @@ namespace Robust.Shared.ContentPack
? ""
: $"<{new string(',', genericCount - 1)}>";
yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})";
yield return ($"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})", IsField: false);
}
}
}

View File

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

View File

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

View File

@@ -68,7 +68,11 @@ namespace Robust.Shared.ContentPack
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
// This also blocks files like "..foo.yml". But whatever, I CBF fixing that.
if (relSysPath.Contains("\\..")
|| relSysPath.Contains("/..")
|| relSysPath.StartsWith(".."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.

View File

@@ -11,12 +11,13 @@ internal sealed record ResourceManifestData(
string? DefaultWindowTitle,
string? WindowIconSet,
string? SplashLogo,
bool? ShowLoadingBar,
bool AutoConnect,
string[]? ClientAssemblies
)
{
public static readonly ResourceManifestData Default =
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true, null);
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, null, true, null);
public static ResourceManifestData LoadResourceManifest(IResourceManager res)
{
@@ -58,6 +59,10 @@ internal sealed record ResourceManifestData(
if (mapping.TryGetNode("splashLogo", out var splashNode))
splashLogo = splashNode.AsString();
bool? showBar = null;
if (mapping.TryGetNode("show_loading_bar", out var showBarNode))
showBar = showBarNode.AsBool();
bool autoConnect = true;
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
autoConnect = autoConnectNode.AsBool();
@@ -70,6 +75,7 @@ internal sealed record ResourceManifestData(
defaultWindowTitle,
windowIconSet,
splashLogo,
showBar,
autoConnect,
clientAssemblies
);

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.
@@ -305,6 +309,32 @@ Types:
SixLabors.ImageSharp.Formats:
IImageEncoder: { All: True }
PixelTypeInfo: { All: True }
SixLabors.ImageSharp.Processing:
ResizeExtensions:
Methods:
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, SixLabors.ImageSharp.Rectangle, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, bool)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Processing.ResizeOptions)"
ProcessingExtensions:
Methods:
- "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
- "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, SixLabors.ImageSharp.Configuration, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Configuration, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])"
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Configuration, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])"
CropExtensions:
Methods:
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)"
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Rectangle)"
IImageProcessingContext: { }
SixLabors.ImageSharp.PixelFormats:
A8: { All: True }
Argb32: { All: True }
@@ -382,6 +412,7 @@ Types:
- "void SaveAsPng(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Png.PngEncoder)"
- "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream)"
- "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Tga.TgaEncoder)"
Rectangle: {All: True}
Size: { All: True }
SizeF: { All: True }
System.Buffers:
@@ -648,6 +679,8 @@ Types:
MethodInfo: { }
TypeAttributes: { } # Enum
TypeInfo: { }
System.Reflection.Metadata:
MetadataUpdateHandlerAttribute: { All: True }
System.Runtime.CompilerServices:
AsyncStateMachineAttribute: { All: True }
AsyncTaskMethodBuilder: { All: True }
@@ -856,13 +889,19 @@ Types:
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object)"
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object, object)"
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object[])"
- "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1<object>)"
- "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1<string>)"
- "System.Text.StringBuilder AppendJoin(char, object[])"
- "System.Text.StringBuilder AppendJoin(char, string[])"
- "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1<object>)"
- "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1<string>)"
- "System.Text.StringBuilder AppendJoin(string, object[])"
- "System.Text.StringBuilder AppendJoin(string, string[])"
- "System.Text.StringBuilder AppendJoin<>(char, System.Collections.Generic.IEnumerable`1<!!0>)"
- "System.Text.StringBuilder AppendJoin<>(string, System.Collections.Generic.IEnumerable`1<!!0>)"
- "System.Text.StringBuilder AppendLine()"
- "System.Text.StringBuilder AppendLine(System.IFormatProvider, ref System.Text.StringBuilder/AppendInterpolatedStringHandler)"
- "System.Text.StringBuilder AppendLine(ref System.Text.StringBuilder/AppendInterpolatedStringHandler)"
- "System.Text.StringBuilder AppendLine(string)"
- "System.Text.StringBuilder Clear()"
- "System.Text.StringBuilder Insert(int, bool)"
@@ -885,6 +924,8 @@ Types:
- "System.Text.StringBuilder Insert(int, ulong)"
- "System.Text.StringBuilder Insert(int, ushort)"
- "System.Text.StringBuilder Remove(int, int)"
- "System.Text.StringBuilder Replace(System.ReadOnlySpan`1<char>, System.ReadOnlySpan`1<char>)"
- "System.Text.StringBuilder Replace(System.ReadOnlySpan`1<char>, System.ReadOnlySpan`1<char>, int, int)"
- "System.Text.StringBuilder Replace(char, char)"
- "System.Text.StringBuilder Replace(char, char, int, int)"
- "System.Text.StringBuilder Replace(string, string)"
@@ -1089,6 +1130,7 @@ Types:
- "string ToHexString(System.ReadOnlySpan`1<byte>)"
Converter`2: { All: True } # Delegate
DateOnly: { All: True }
DateTime: { All: True }
DateTimeKind: { } # Enum
DateTimeOffset: { All: True }
@@ -1421,6 +1463,7 @@ Types:
- "void CopyTo(int, char[], int, int)"
StringComparison: { } # Enum
StringSplitOptions: { } # Enum
TimeOnly: { All: True }
TimeSpan: { All: True }
Type:
# COM, marshalling, interop, etc... stuff omitted.

View File

@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
@@ -33,7 +30,8 @@ namespace Robust.Shared.EntitySerialization;
public sealed class EntityDeserializer :
ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<MapId, ValueDataNode>
{
// See the comments around EntitySerializer's version const for information about the different versions.
// TBH version three isn't even really fully supported anymore, simply due to changes in engine component serialization.
@@ -93,6 +91,7 @@ public sealed class EntityDeserializer :
public readonly LoadResult Result = new();
public readonly Dictionary<int, string> TileMap = new();
public readonly Dictionary<int, EntityUid> UidMap = new();
public readonly Dictionary<int, MapId> AllocatedMapIds = new();
public readonly List<int> MapYamlIds = new();
public readonly List<int> GridYamlIds = new();
public readonly List<int> OrphanYamlIds = new();
@@ -185,6 +184,10 @@ public sealed class EntityDeserializer :
// Alloc entities, and populate the yaml uid -> EntityUid maps
AllocateEntities();
// Assign a map id to each map entity. This is required to de-serialize map coordinates & map ids
if (Options.AssignMapIds)
AllocateMapIds();
// Load the prototype data onto entities, e.g. transform parents, etc.
LoadEntities();
@@ -197,7 +200,7 @@ public sealed class EntityDeserializer :
// Assign MapSaveTileMapComponent to all read grids. This is used to avoid large file diffs if the tile map changes.
StoreGridTileMap();
if (Options.AssignMapids)
if (Options.AssignMapIds)
AssignMapIds();
CheckCategory();
@@ -224,6 +227,15 @@ public sealed class EntityDeserializer :
InitializeMaps();
ProcessDeletions();
if (!Options.AssignMapIds)
return;
foreach (var yamlId in MapYamlIds)
{
if (AllocatedMapIds.TryGetValue(yamlId, out var alloc) && EntMan.EntityExists(UidMap[yamlId]))
DebugTools.AssertEqual(_map.GetMap(alloc), UidMap[yamlId]);
}
}
private void ReadMetadata()
@@ -520,6 +532,18 @@ public sealed class EntityDeserializer :
_log.Debug($"Allocated {Entities.Count} entities in {_stopwatch.Elapsed}");
}
private void AllocateMapIds()
{
if (Result.Version < 7)
return; // MapYamlIds is not populated untill later in older versions
foreach (var yamlMapId in MapYamlIds)
{
var mapUid = UidMap[yamlMapId];
AllocatedMapIds[yamlMapId] = _map.AllocateMapId(mapUid);
}
}
private void ReadMapsAndGrids()
{
if (Result.Version < 7)
@@ -1222,5 +1246,48 @@ public sealed class EntityDeserializer :
: new ValueDataNode("invalid");
}
ValidationNode ITypeValidator<MapId, ValueDataNode>.Validate(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
ISerializationContext? context)
{
return seri.ValidateNode<EntityUid>(node, context);
}
MapId ITypeReader<MapId, ValueDataNode>.Read(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
SerializationHookContext hookCtx,
ISerializationContext? ctx,
ISerializationManager.InstantiationDelegate<MapId>? instanceProvider)
{
if (!Options.AssignMapIds || Result.Version < 7)
{
_log.Error("Cannot deserialize map ids without pre-allocated ids");
return MapId.Nullspace;
}
if (int.TryParse(node.Value, out var val) && AllocatedMapIds.TryGetValue(val, out var map))
return map;
var msg = CurrentReadingEntity is not { } ent
? "Encountered unknown yaml map id"
: $"Encountered unknown yaml map id wile reading entity {ent.YamlId}, component: {CurrentComponent}";
_log.Error(msg);
return MapId.Nullspace;
}
DataNode ITypeWriter<MapId>.Write(
ISerializationManager seri,
MapId value,
IDependencyCollection deps,
bool alwaysWrite,
ISerializationContext? ctx)
{
return seri.WriteValue(_map.GetMapOrInvalid(value), alwaysWrite, ctx);
}
#endregion
}

View File

@@ -38,7 +38,8 @@ namespace Robust.Shared.EntitySerialization;
/// </remarks>
public sealed class EntitySerializer : ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<MapId, ValueDataNode>
{
public const int MapFormatVersion = 7;
// v6->v7: PR #5572 - Added more metadata, List maps/grids/orphans, include some life-stage information
@@ -56,12 +57,12 @@ public sealed class EntitySerializer : ISerializationContext,
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
[Dependency] private readonly IConfigurationManager _conf = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private readonly ISawmill _log;
public readonly Dictionary<EntityUid, int> YamlUidMap = new();
public readonly HashSet<int> YamlIds = new();
public string? CurrentComponent { get; private set; }
public Entity<MetaDataComponent>? CurrentEntity { get; private set; }
public int CurrentEntityYamlUid { get; private set; }
@@ -252,6 +253,42 @@ public sealed class EntitySerializer : ISerializationContext,
Truncate = EntityUid.Invalid;
}
/// <summary>
/// Serializes several entities and all of their children. Note that this will not automatically serialize the
/// entity's parents.
/// </summary>
public void SerializeEntityRecursive(HashSet<EntityUid> roots)
{
if (roots.Count == 0)
return;
InitializeTileMap(roots.First());
HashSet<EntityUid> allEntities = new();
List<(EntityUid Root, HashSet<EntityUid> Children)> entities = new();
foreach(var root in roots)
{
if (!IsSerializable(root))
throw new Exception($"{EntMan.ToPrettyString(root)} is not serializable");
var ents = new HashSet<EntityUid>();
RecursivelyIncludeChildren(root, ents);
entities.Add((root, ents));
allEntities.UnionWith(ents);
}
ReserveYamlIds(allEntities);
foreach (var (root, children) in entities)
{
Truncate = _xformQuery.GetComponent(root).ParentUid;
Truncated.Add(Truncate);
SerializeEntitiesInternal(children);
Truncate = EntityUid.Invalid;
}
}
#endregion
/// <summary>
@@ -939,7 +976,6 @@ public sealed class EntitySerializer : ISerializationContext,
if (YamlUidMap.TryGetValue(value, out var yamlId))
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
if (CurrentComponent == _xformName)
{
if (value == EntityUid.Invalid)
@@ -1052,5 +1088,41 @@ public sealed class EntitySerializer : ISerializationContext,
return serializationManager.WriteValue(uid, alwaysWrite, context);
}
ValidationNode ITypeValidator<MapId, ValueDataNode>.Validate(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
ISerializationContext? context)
{
return seri.ValidateNode<EntityUid>(node, context);
}
MapId ITypeReader<MapId, ValueDataNode>.Read(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
SerializationHookContext hookCtx,
ISerializationContext? ctx,
ISerializationManager.InstantiationDelegate<MapId>? instanceProvider)
{
return EntMan.TryGetComponent(seri.Read<EntityUid>(node, ctx), out MapComponent? mapComp)
? mapComp.MapId
: MapId.Nullspace;
}
DataNode ITypeWriter<MapId>.Write(
ISerializationManager seri,
MapId value,
IDependencyCollection deps,
bool alwaysWrite,
ISerializationContext? ctx)
{
if (_map.TryGetMap(value, out var uid))
return seri.WriteValue(uid, alwaysWrite, ctx);
_log.Error($"Attempted to serialize invalid map id {value} while serializing component '{CurrentComponent}' on entity '{EntMan.ToPrettyString(uid)}'");
return new ValueDataNode("invalid");
}
#endregion
}

View File

@@ -83,10 +83,10 @@ public record struct DeserializationOptions()
public bool LogInvalidEntities = true;
/// <summary>
/// Whether or not to automatically assign map ids to any deserialized map entities.
/// Whether to automatically assign map ids to any deserialized map entities.
/// If false, maps need to be manually given ids before entities are initialized.
/// </summary>
public bool AssignMapids = true;
public bool AssignMapIds = true;
}
/// <summary>

View File

@@ -4,13 +4,11 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
@@ -97,7 +95,7 @@ public sealed partial class MapLoaderSystem
var opts = options ?? MapLoadOptions.Default;
// If we are forcing a map id, we cannot auto-assign ids.
opts.DeserializationOptions.AssignMapids = opts.ForceMapId == null;
opts.DeserializationOptions.AssignMapIds = opts.ForceMapId == null;
if (opts.MergeMap is { } targetId && !_mapSystem.MapExists(targetId))
throw new Exception($"Target map {targetId} does not exist");

View File

@@ -16,7 +16,7 @@ public sealed partial class MapLoaderSystem
public event EntitySerializer.IsSerializableDelegate? OnIsSerializable;
/// <summary>
/// Recursively serialize the given entity and its children.
/// Recursively serialize the given entities and all of their children.
/// </summary>
public (MappingDataNode Node, FileCategory Category) SerializeEntitiesRecursive(
HashSet<EntityUid> entities,
@@ -41,12 +41,7 @@ public sealed partial class MapLoaderSystem
var serializer = new EntitySerializer(_dependency, opts);
serializer.OnIsSerializeable += OnIsSerializable;
foreach (var ent in entities)
{
serializer.SerializeEntityRecursive(ent);
}
serializer.SerializeEntityRecursive(entities);
var data = serializer.Write();
var cat = serializer.GetCategory();

View File

@@ -210,6 +210,9 @@ namespace Robust.Shared.GameObjects
catch (Exception e)
{
_sawmill.Error($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
return false;
}
@@ -285,12 +288,7 @@ namespace Robust.Shared.GameObjects
using (histogram?.WithLabels("QueuedDeletion").NewTimer())
using (_prof.Group("QueueDel"))
{
while (QueuedDeletions.TryDequeue(out var uid))
{
DeleteEntity(uid);
}
QueuedDeletionsSet.Clear();
ProcessQueueudDeletions();
}
using (histogram?.WithLabels("ComponentCull").NewTimer())
@@ -300,6 +298,16 @@ namespace Robust.Shared.GameObjects
}
}
internal virtual void ProcessQueueudDeletions()
{
while (QueuedDeletions.TryDequeue(out var uid))
{
DeleteEntity(uid);
}
QueuedDeletionsSet.Clear();
}
public virtual void FrameUpdate(float frameTime)
{
_entitySystemManager.FrameUpdate(frameTime);
@@ -352,7 +360,9 @@ namespace Robust.Shared.GameObjects
throw new ArgumentException($"Attempted to spawn entity on an invalid map. Coordinates: {coordinates}");
EntityCoordinates coords;
if (transform.Anchored && _mapManager.TryFindGridAt(coordinates, out var gridUid, out var grid))
if (_mapManager.TryFindGridAt(coordinates, out var gridUid, out var grid)
&& MetaQuery.TryGetComponentInternal(gridUid, out var meta)
&& meta.EntityLifeStage < EntityLifeStage.Terminating)
{
coords = new EntityCoordinates(gridUid, _mapSystem.WorldToLocal(gridUid, grid, coordinates.Position));
_xforms.SetCoordinates(newEntity, transform, coords, rotation, unanchor: false);
@@ -599,6 +609,9 @@ namespace Robust.Shared.GameObjects
catch (Exception e)
{
_sawmill.Error($"Caught exception while raising event {nameof(EntityTerminatingEvent)} on entity {ToPrettyString(uid, metadata)}\n{e}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
foreach (var child in xform._children)
@@ -641,6 +654,9 @@ namespace Robust.Shared.GameObjects
catch(Exception e)
{
_sawmill.Error($"Caught exception while trying to recursively delete child entity '{ToPrettyString(child)}' of '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
@@ -659,6 +675,9 @@ namespace Robust.Shared.GameObjects
catch (Exception e)
{
_sawmill.Error($"Caught exception while trying to call shutdown on component of entity '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}
@@ -674,6 +693,9 @@ namespace Robust.Shared.GameObjects
catch (Exception e)
{
_sawmill.Error($"Caught exception while invoking event {nameof(EntityDeleted)} on '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
_eventBus.OnEntityDeleted(uid);

View File

@@ -104,7 +104,7 @@ namespace Robust.Shared.GameObjects
/// <param name="prototypeName">Name of the <see cref="EntityPrototype"/> to spawn.</param>
/// <param name="coordinates">Coordinates to place the newly spawned entity.</param>
/// <param name="overrides">Overrides to add or remove components that differ from the prototype.</param>
/// <param name="rotation">Map rotation to set the newly spawned entity to.</param>
/// <param name="rotation">Local rotation to set the newly spawned entity to.</param>
/// <returns>A new uninitialized entity.</returns>
/// <remarks>If there is a grid at the <paramref name="coordinates"/>, the entity will be parented to the grid.
/// Otherwise, it will be parented to the map.</remarks>

View File

@@ -1,6 +1,8 @@
using System;
using System.Numerics;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
@@ -8,6 +10,8 @@ using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
public abstract class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent, OccluderComponent>
{
public const float MaxRaycastRange = 100f;
public override void Initialize()
{
base.Initialize();
@@ -69,4 +73,84 @@ public abstract class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent
QueueTreeUpdate(uid, comp);
}
#endregion
#region InRangeUnoccluded
/// <summary>
/// Returns true if two points are within the specified range and there are no occluders between them that aren't
/// ignored by the predicate.
/// </summary>
public bool InRangeUnoccluded<TState>(
MapCoordinates origin,
MapCoordinates other,
float range,
TState state,
Func<Entity<OccluderComponent, TransformComponent>, TState, bool> ignore)
{
if (!GetRay(origin, other, range, out var length, out var ray, out var result))
return result;
return IntersectRay(origin.MapId, ray, length, state, ignore) == null;
}
/// <summary>
/// Returns true if two points are within the specified range and there are no occluders between them.
/// </summary>
/// <param name="ignoreTouching">If true, this will use <see cref="IsTouchingEndpoint"/> as a predicate to ignore \
/// occluders that are touching the start or end point.</param>
public bool InRangeUnoccluded(MapCoordinates origin, MapCoordinates other, float range, bool ignoreTouching)
{
if (!GetRay(origin, other, range, out var length, out var ray, out var result))
return result;
if (!ignoreTouching)
return IntersectRay(origin.MapId, ray, length) == null;
var state = (XformSystem, origin.Position, other.Position);
return IntersectRay(origin.MapId, ray, length, state, IsTouchingEndpoint) == null;
}
private bool GetRay(MapCoordinates origin, MapCoordinates other, float range, out float length, out Ray ray, out bool result)
{
ray = default;
length = default;
result = false;
if (other.MapId != origin.MapId || other.MapId == MapId.Nullspace)
return false;
var dir = other.Position - origin.Position;
length = dir.Length();
if (MathHelper.CloseTo(length, 0))
{
result = true;
return false;
}
var normalized = dir / length;
if (range > 0f && length > range + 0.01f)
return false;
if (length > MaxRaycastRange)
{
Log.Warning($"{nameof(InRangeUnoccluded)} check performed over extreme range. Limiting range.");
length = MaxRaycastRange;
}
ray = new Ray(origin.Position, normalized);
return true;
}
/// <summary>
/// Simple predicate for use with <see cref="InRangeUnoccluded"/> that will ignore any occluders that intersect the
/// start and end points.
/// </summary>
public static bool IsTouchingEndpoint(Entity<OccluderComponent, TransformComponent> ent, (SharedTransformSystem Sys, Vector2 Start, Vector2 End) state)
{
var occluderBox = ent.Comp1.BoundingBox;
occluderBox = occluderBox.Translated(state.Sys.GetWorldPosition(ent.Comp2));
return occluderBox.Contains(state.Start) || occluderBox.Contains(state.End);
}
#endregion
}

View File

@@ -13,6 +13,7 @@ namespace Robust.Shared.GameObjects;
public abstract partial class SharedMapSystem
{
protected int LastMapId;
private Dictionary<EntityUid, MapId> _reserved = new();
private void InitializeMap()
{
@@ -128,6 +129,17 @@ public abstract partial class SharedMapSystem
EnsureComp<GridTreeComponent>(uid);
}
/// <summary>
/// Generate & reserve a map-id for a map-entity before it is actually given the component.
/// </summary>
internal MapId AllocateMapId(EntityUid ent)
{
var id = _reserved[ent] = TakeNextMapId();
Maps.Add(id, ent);
UsedIds.Add(id);
return id;
}
internal void AssignMapId(Entity<MapComponent> map, MapId? id = null)
{
if (map.Comp.MapId != MapId.Nullspace)
@@ -148,6 +160,15 @@ public abstract partial class SharedMapSystem
return;
}
if (_reserved.TryGetValue(map.Owner, out var reserved))
{
DebugTools.AssertNull(id);
DebugTools.AssertEqual(Maps[reserved], map.Owner);
DebugTools.Assert(UsedIds.Contains(reserved));
map.Comp.MapId = reserved;
return;
}
map.Comp.MapId = id ?? TakeNextMapId();
if (IsClientSide(map) != map.Comp.MapId.IsClientSide)

View File

@@ -1028,6 +1028,15 @@ public abstract partial class SharedTransformSystem
return GetWorldPositionRotation(component);
}
[Obsolete("Use variant without entity query")]
public (Vector2 Position, Angle Rotation) GetRelativePositionRotation(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
{
return GetRelativePositionRotation(component, relative);
}
/// <summary>
/// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy.
/// </summary>
@@ -1035,15 +1044,14 @@ public abstract partial class SharedTransformSystem
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public (Vector2 Position, Angle Rotation) GetRelativePositionRotation(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
EntityUid relative)
{
var rot = component._localRotation;
var pos = component._localPosition;
var xform = component;
while (xform.ParentUid != relative)
{
if (xform.ParentUid.IsValid() && query.TryGetComponent(xform.ParentUid, out xform))
if (xform.ParentUid.IsValid() && TryComp(xform.ParentUid, out xform))
{
rot += xform._localRotation;
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
@@ -1052,30 +1060,36 @@ public abstract partial class SharedTransformSystem
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
Log.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
var relXform = query.GetComponent(relative);
var relXform = Transform(relative);
pos = Vector2.Transform(pos, GetInvWorldMatrix(relXform));
rot = rot - GetWorldRotation(relXform, query);
rot = rot - GetWorldRotation(relXform);
break;
}
return (pos, rot);
}
[Obsolete("Use variant without entity query")]
public Vector2 GetRelativePosition(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
{
return GetRelativePosition(component, relative);
}
/// <summary>
/// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy.
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 GetRelativePosition(
TransformComponent component,
EntityUid relative,
EntityQuery<TransformComponent> query)
public Vector2 GetRelativePosition(TransformComponent component, EntityUid relative)
{
var pos = component._localPosition;
var xform = component;
while (xform.ParentUid != relative)
{
if (xform.ParentUid.IsValid() && query.TryGetComponent(xform.ParentUid, out xform))
if (xform.ParentUid.IsValid() && TryComp(xform.ParentUid, out xform))
{
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
continue;
@@ -1083,7 +1097,7 @@ public abstract partial class SharedTransformSystem
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
Log.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
var relXform = query.GetComponent(relative);
var relXform = Transform(relative);
pos = Vector2.Transform(pos, GetInvWorldMatrix(relXform));
break;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Map;
@@ -132,6 +133,7 @@ public abstract partial class SharedTransformSystem
/// <summary>
/// Creates map-relative <see cref="EntityCoordinates"/> given some <see cref="MapCoordinates"/>.
/// </summary>
[Pure]
public EntityCoordinates ToCoordinates(MapCoordinates coordinates)
{
if (_map.TryGetMap(coordinates.MapId, out var uid))
@@ -145,11 +147,13 @@ public abstract partial class SharedTransformSystem
/// <summary>
/// Returns the grid that the entity whose position the coordinates are relative to is on.
/// </summary>
[Pure]
public EntityUid? GetGrid(EntityCoordinates coordinates)
{
return GetGrid(coordinates.EntityId);
}
[Pure]
public EntityUid? GetGrid(Entity<TransformComponent?> entity)
{
return !Resolve(entity, ref entity.Comp, logMissing:false) ? null : entity.Comp.GridUid;
@@ -158,6 +162,7 @@ public abstract partial class SharedTransformSystem
/// <summary>
/// Returns the Map Id these coordinates are on.
/// </summary>
[Pure]
public MapId GetMapId(EntityCoordinates coordinates)
{
return GetMapId(coordinates.EntityId);

View File

@@ -5,6 +5,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Shared.Map
@@ -12,8 +13,8 @@ namespace Robust.Shared.Map
/// <summary>
/// A set of coordinates relative to another entity.
/// </summary>
[PublicAPI]
public readonly struct EntityCoordinates : IEquatable<EntityCoordinates>, ISpanFormattable
[PublicAPI, DataRecord]
public readonly record struct EntityCoordinates : ISpanFormattable
{
public static readonly EntityCoordinates Invalid = new(EntityUid.Invalid, Vector2.Zero);
@@ -313,44 +314,6 @@ namespace Robust.Shared.Map
return true;
}
#region IEquatable
/// <inheritdoc />
public bool Equals(EntityCoordinates other)
{
return EntityId.Equals(other.EntityId) && Position.Equals(other.Position);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is EntityCoordinates other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(EntityId, Position);
}
/// <summary>
/// Check for equality by value between two objects.
/// </summary>
public static bool operator ==(EntityCoordinates left, EntityCoordinates right)
{
return left.Equals(right);
}
/// <summary>
/// Check for inequality by value between two objects.
/// </summary>
public static bool operator !=(EntityCoordinates left, EntityCoordinates right)
{
return !left.Equals(right);
}
#endregion
#region Operators
/// <summary>

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Shared.Map
@@ -10,9 +11,9 @@ namespace Robust.Shared.Map
/// <summary>
/// Coordinates relative to a specific map.
/// </summary>
[PublicAPI]
[PublicAPI, DataRecord]
[Serializable, NetSerializable]
public readonly struct MapCoordinates : IEquatable<MapCoordinates>, ISpanFormattable
public readonly record struct MapCoordinates : ISpanFormattable
{
public static readonly MapCoordinates Nullspace = new(Vector2.Zero, MapId.Nullspace);
@@ -95,46 +96,6 @@ namespace Robust.Shared.Map
return (otherCoords.Position - Position).LengthSquared() < range * range;
}
/// <inheritdoc />
public bool Equals(MapCoordinates other)
{
return Position.Equals(other.Position) && MapId.Equals(other.MapId);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is MapCoordinates other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return (Position.GetHashCode() * 397) ^ MapId.GetHashCode();
}
}
/// <summary>
/// Check for equality by value between two objects.
/// </summary>
public static bool operator ==(MapCoordinates a, MapCoordinates b)
{
return a.Equals(b);
}
/// <summary>
/// Check for inequality by value between two objects.
/// </summary>
public static bool operator !=(MapCoordinates a, MapCoordinates b)
{
return !a.Equals(b);
}
/// <summary>
/// Used to deconstruct this object into a tuple.
/// </summary>

View File

@@ -31,6 +31,11 @@ public readonly struct ComponentTreeEntry<T> : IEquatable<ComponentTreeEntry<T>>
xform = Transform;
}
public static implicit operator Entity<T, TransformComponent>(ComponentTreeEntry<T> entry)
{
return new(entry.Uid, entry.Component, entry.Transform);
}
public static implicit operator ComponentTreeEntry<T>((T, TransformComponent) tuple)
{
return new ComponentTreeEntry<T>()

View File

@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -35,10 +32,9 @@ namespace Robust.Shared.Physics.Systems
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private float _broadphaseExpand;
private readonly HashSet<FixtureProxy> _gridMoveBuffer = new();
private readonly Dictionary<EntityUid, Matrix3x2> _broadMatrices = new();
private HashSet<FixtureProxy> _gridMoveBuffer = new();
private float _frameTime;
/*
* Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required.
@@ -55,9 +51,9 @@ namespace Robust.Shared.Physics.Systems
_contactJob = new()
{
_mapManager = _mapManager,
MapManager = _mapManager,
System = this,
BroadphaseExpand = _broadphaseExpand,
TransformSys = EntityManager.System<SharedTransformSystem>(),
// TODO: EntityManager one isn't ready yet?
XformQuery = GetEntityQuery<TransformComponent>(),
};
@@ -71,13 +67,13 @@ namespace Robust.Shared.Physics.Systems
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(SharedTransformSystem));
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
}
private void SetBroadphaseExpand(float value)
{
_contactJob.BroadphaseExpand = value;
_broadphaseExpand = value;
Subs.CVar(_cfg,
CVars.TargetMinimumTickrate,
val =>
{
_frameTime = 1f / val;
},
true);
}
public void Rebuild(BroadphaseComponent component, bool fullBuild)
@@ -109,6 +105,7 @@ namespace Robust.Shared.Physics.Systems
// This is so that if we're on a broadphase that's moving (e.g. a grid) we need to make sure anything
// we move over is getting checked for collisions, and putting it on the movebuffer is the easiest way to do so.
var moveBuffer = _physicsSystem.MoveBuffer;
_gridMoveBuffer.Clear();
foreach (var gridUid in movedGrids)
{
@@ -120,7 +117,7 @@ namespace Robust.Shared.Physics.Systems
continue;
var worldAABB = _transform.GetWorldMatrix(xform).TransformBox(grid.LocalAABB);
var enlargedAABB = worldAABB.Enlarged(_broadphaseExpand);
var enlargedAABB = worldAABB.Enlarged(GetBroadphaseExpand(_physicsQuery.GetComponent(gridUid), _frameTime));
var state = (moveBuffer, _gridMoveBuffer);
QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB);
@@ -135,6 +132,11 @@ namespace Robust.Shared.Physics.Systems
}
}
private float GetBroadphaseExpand(PhysicsComponent body, float frameTime)
{
return body.LinearVelocity.Length() * 1.2f * frameTime;
}
private void QueryMapBroadphase(IBroadPhase broadPhase,
ref (HashSet<FixtureProxy>, HashSet<FixtureProxy>) state,
Box2 enlargedAABB)
@@ -163,11 +165,12 @@ namespace Robust.Shared.Physics.Systems
/// </summary>
internal void FindNewContacts()
{
_contactJob.FrameTime = _frameTime;
_contactJob.Pairs.Clear();
var moveBuffer = _physicsSystem.MoveBuffer;
var movedGrids = _physicsSystem.MovedGrids;
_gridMoveBuffer.Clear();
// Find any entities being driven over that might need to be considered
FindGridContacts(movedGrids);
@@ -195,55 +198,32 @@ namespace Robust.Shared.Physics.Systems
_contactJob.MoveBuffer.Add(proxy);
}
_broadMatrices.Clear();
var broadQuery = AllEntityQuery<BroadphaseComponent>();
// Cache broadphase matrices up front.
// We'll defer the proxy world AABBs until we get contacts rather than doing it on every single move.
// This is because contacts are run in parallel so we can spread the work a bit more and also don't duplicate it per tick.
while (broadQuery.MoveNext(out var bUid, out _))
{
_broadMatrices[bUid] = _transform.GetWorldMatrix(bUid);
}
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
{
_contactJob.ContactBuffer.Add(new List<FixtureProxy>());
}
var count = moveBuffer.Count;
_parallel.ProcessNow(_contactJob, count);
for (var i = 0; i < count; i++)
foreach (var (proxyA, proxyB, flags) in _contactJob.Pairs)
{
var proxies = _contactJob.ContactBuffer[i];
var otherBody = proxyB.Body;
var contactFlags = ContactFlags.None;
if (proxies.Count == 0)
continue;
var proxyA = _contactJob.MoveBuffer[i];
var proxyABody = proxyA.Body;
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
foreach (var other in proxies)
// Because we may be colliding with something asleep (due to the way grid movement works) need
// to make sure the contact doesn't fail.
// This is because we generate a contact across 2 different broadphases where both bodies aren't
// moving locally but are moving in world-terms.
if ((flags & PairFlag.Wake) == PairFlag.Wake)
{
var otherBody = other.Body;
// Because we may be colliding with something asleep (due to the way grid movement works) need
// to make sure the contact doesn't fail.
// This is because we generate a contact across 2 different broadphases where both bodies aren't
// moving locally but are moving in world-terms.
if (proxyA.Fixture.Hard && other.Fixture.Hard &&
(_gridMoveBuffer.Contains(proxyA) || _gridMoveBuffer.Contains(other)))
{
_physicsSystem.WakeBody(proxyA.Entity, force: true, manager: manager, body: proxyABody);
_physicsSystem.WakeBody(other.Entity, force: true, body: otherBody);
}
_physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other);
_physicsSystem.WakeBody(proxyA.Entity, force: true, body: proxyA.Body);
_physicsSystem.WakeBody(proxyB.Entity, force: true, body: otherBody);
}
// TODO: Actually implement this for grids, atm they have their own skrungly fixture handling which prevents this.
if ((PairFlag.Grid & flags) == PairFlag.Grid)
{
contactFlags |= ContactFlags.Grid;
}
_physicsSystem.AddPair(proxyA.FixtureId, proxyB.FixtureId, proxyA, proxyB, flags: contactFlags);
}
moveBuffer.Clear();
@@ -252,6 +232,8 @@ namespace Robust.Shared.Physics.Systems
private void HandleGridCollisions(HashSet<EntityUid> movedGrids)
{
// TODO: Could move this into its own job.
// Ideally we'd just have some way to flag an entity as "AABB moves not proxy" into its own movebuffer.
foreach (var gridUid in movedGrids)
{
var grid = _gridQuery.GetComponent(gridUid);
@@ -301,6 +283,12 @@ namespace Robust.Shared.Physics.Systems
return true;
}
// If the other entity is lower ID and also moved then let that handle the collision.
if (tuple.grid.Owner.Id > uid.Id && tuple._physicsSystem.MovedGrids.Contains(uid))
{
return true;
}
var (_, _, otherGridMatrix, otherGridInvMatrix) = tuple.xformSystem.GetWorldPositionRotationMatrixWithInv(collidingXform);
var otherGridBounds = otherGridMatrix.TransformBox(component.LocalAABB);
var otherTransform = tuple._physicsSystem.GetPhysicsTransform(uid);
@@ -337,6 +325,10 @@ namespace Robust.Shared.Physics.Systems
{
var otherFixture = fixturesB.Fixtures[otherId];
// There's already a contact so ignore it.
if (fixture.Contacts.ContainsKey(otherFixture))
break;
for (var j = 0; j < otherFixture.Shape.ChildCount; j++)
{
var otherAABB = otherFixture.Shape.ComputeAABB(otherTransform, j);
@@ -370,7 +362,7 @@ namespace Robust.Shared.Physics.Systems
FixtureProxy proxy,
Box2 worldAABB,
EntityUid broadphase,
List<FixtureProxy> pairBuffer)
List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer)
{
DebugTools.Assert(proxy.Body.CanCollide);
@@ -401,7 +393,7 @@ namespace Robust.Shared.Physics.Systems
}
var broadphaseComp = _broadphaseQuery.GetComponent(broadphase);
var state = (pairBuffer, proxy);
var state = (pairBuffer, _physicsSystem.MoveBuffer, this, _physicsSystem, proxy);
QueryBroadphase(broadphaseComp.DynamicTree, state, aabb);
@@ -411,23 +403,57 @@ namespace Robust.Shared.Physics.Systems
QueryBroadphase(broadphaseComp.StaticTree, state, aabb);
}
private void QueryBroadphase(IBroadPhase broadPhase, (List<FixtureProxy>, FixtureProxy) state, Box2 aabb)
private void QueryBroadphase(IBroadPhase broadPhase, (List<(FixtureProxy, FixtureProxy, PairFlag)>, HashSet<FixtureProxy> MoveBuffer, SharedBroadphaseSystem Broadphase, SharedPhysicsSystem PhysicsSystem, FixtureProxy) state, Box2 aabb)
{
broadPhase.QueryAabb(ref state, static (
ref (List<FixtureProxy> pairBuffer, FixtureProxy proxy) tuple,
ref (List<(FixtureProxy, FixtureProxy, PairFlag)> pairs, HashSet<FixtureProxy> moveBuffer, SharedBroadphaseSystem broadphase, SharedPhysicsSystem physicsSystem, FixtureProxy proxy) tuple,
in FixtureProxy other) =>
{
DebugTools.Assert(other.Body.CanCollide);
// Logger.DebugS("physics", $"Checking {proxy.Entity} against {other.Fixture.Body.Owner} at {aabb}");
if (tuple.proxy == other ||
!SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture) ||
tuple.proxy.Entity == other.Entity)
if (tuple.proxy.Entity == other.Entity ||
!SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture))
{
return true;
}
tuple.pairBuffer.Add(other);
// Avoid creating duplicate pairs.
// We give priority to whoever has the lower entity ID.
if (tuple.proxy.Entity.Id > other.Entity.Id)
{
// Let the other fixture handle it.
if (tuple.moveBuffer.Contains(other))
return true;
}
// Check if contact already exists.
if (tuple.proxy.Fixture.Contacts.ContainsKey(other.Fixture))
return true;
// TODO: Add in the slow path check here but turnstiles currently explodes this on content so.
if (!tuple.physicsSystem.ShouldCollideJoints(tuple.proxy.Entity, other.Entity))
return true;
// TODO: Sensors handled elsewhere when we do v3 port.
//if (!tuple.proxy.Fixture.Hard || !other.Fixture.Hard)
// return true;
// TODO: Check if interlocked + array is better here which is what box2d does
// It then just heap allocates anything over the array size.
var flags = PairFlag.None;
if (tuple.proxy.Fixture.Hard &&
other.Fixture.Hard &&
(tuple.broadphase._gridMoveBuffer.Contains(tuple.proxy) || tuple.broadphase._gridMoveBuffer.Contains(other)))
{
flags |= PairFlag.Wake;
}
lock (tuple.pairs)
{
tuple.pairs.Add((tuple.proxy, other, flags));
}
return true;
}, aabb, true);
}
@@ -560,39 +586,42 @@ namespace Robust.Shared.Physics.Systems
{
public SharedBroadphaseSystem System = default!;
public SharedTransformSystem TransformSys = default!;
public IMapManager _mapManager = default!;
public float BroadphaseExpand;
public IMapManager MapManager = default!;
public EntityQuery<TransformComponent> XformQuery;
public List<List<FixtureProxy>> ContactBuffer = new();
public List<FixtureProxy> MoveBuffer = new();
public readonly List<FixtureProxy> MoveBuffer = new();
public int BatchSize => 8;
public List<(FixtureProxy, FixtureProxy, PairFlag)> Pairs = new(64);
public float FrameTime;
// Box2D uses 64 but we have to do grid queries for each fixtureproxy which will add a fair bit of overhead.
// Plus we also run events + trycomp for joints on top.
public int BatchSize => 16;
public void Execute(int index)
{
var proxy = MoveBuffer[index];
var broadphaseUid = XformQuery.GetComponent(proxy.Entity).Broadphase?.Uid;
var worldAABB = System._broadMatrices[broadphaseUid!.Value].TransformBox(proxy.AABB);
var buffer = ContactBuffer[index];
buffer.Clear();
var worldAABB = TransformSys.GetWorldMatrix(broadphaseUid!.Value).TransformBox(proxy.AABB);
var mapUid = XformQuery.GetComponent(proxy.Entity).MapUid ?? EntityUid.Invalid;
var broadphaseExpand = System.GetBroadphaseExpand(proxy.Body, FrameTime);
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (System, proxy, worldAABB, buffer);
var state = (System, proxy, worldAABB, Pairs);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
MapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
@@ -602,9 +631,24 @@ namespace Robust.Shared.Physics.Systems
includeMap: false);
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
System.FindPairs(proxy, worldAABB, mapUid, buffer);
System.FindPairs(proxy, worldAABB, mapUid, Pairs);
}
}
[Flags]
private enum PairFlag : byte
{
None = 0,
/// <summary>
/// Should we wake the contacting entities.
/// </summary>
Wake = 1 << 0,
/// <summary>
/// Is it a grid collision.
/// </summary>
Grid = 1 << 1,
}
}
}

View File

@@ -263,16 +263,13 @@ public abstract partial class SharedPhysicsSystem
// Broadphase has already done the faster check for collision mask / layers
// so no point duplicating
// Does a contact already exist?
if (fixtureA.Contacts.ContainsKey(fixtureB))
return;
DebugTools.Assert(!fixtureA.Contacts.ContainsKey(fixtureB));
DebugTools.Assert(!fixtureB.Contacts.ContainsKey(fixtureA));
var xformA = entA.Comp2;
var xformB = entB.Comp2;
// Does a joint override collision? Is at least one body dynamic?
if (!ShouldCollide(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
if (!ShouldCollideSlow(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
return;
// Call the factory.
@@ -310,14 +307,14 @@ public abstract partial class SharedPhysicsSystem
/// <summary>
/// Go through the cached broadphase movement and update contacts.
/// </summary>
internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB)
internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB, ContactFlags flags = ContactFlags.None)
{
AddPair((proxyA.Entity, proxyA.Body, proxyA.Xform),
(proxyB.Entity, proxyB.Body, proxyB.Xform),
fixtureAId, fixtureBId,
proxyA.Fixture, proxyA.ChildIndex,
proxyB.Fixture, proxyB.ChildIndex,
proxyA.Body, proxyB.Body);
proxyA.Body, proxyB.Body, flags: flags);
}
internal static bool ShouldCollide(Fixture fixtureA, Fixture fixtureB)
@@ -447,7 +444,8 @@ public abstract partial class SharedPhysicsSystem
{
// Check default filtering
if (!ShouldCollide(fixtureA, fixtureB) ||
!ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
!ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) ||
!ShouldCollideJoints(uidA, uidB))
{
DestroyContact(contact);
continue;
@@ -720,10 +718,31 @@ public abstract partial class SharedPhysicsSystem
}
}
/// <summary>
/// Is there a joint blocking collision between these bodies.
/// </summary>
internal bool ShouldCollideJoints(Entity<JointComponent?> entA, Entity<JointComponent?> entB)
{
// Does a joint prevent collision?
// if one of them doesn't have jointcomp then they can't share a common joint.
// otherwise, only need to iterate over the joints of one component as they both store the same joint.
if (JointQuery.Resolve(entA.Owner, ref entA.Comp, false) && JointQuery.HasComp(entB))
{
foreach (var joint in entA.Comp.Joints.Values)
{
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
if (!joint.CollideConnected && (entB.Owner == joint.BodyAUid || entB.Owner == joint.BodyBUid))
return false;
}
}
return true;
}
/// <summary>
/// Used to prevent bodies from colliding; may lie depending on joints.
/// </summary>
protected bool ShouldCollide(
internal bool ShouldCollideSlow(
EntityUid uid,
EntityUid other,
PhysicsComponent body,
@@ -757,18 +776,7 @@ public abstract partial class SharedPhysicsSystem
return false;
}
// Does a joint prevent collision?
// if one of them doesn't have jointcomp then they can't share a common joint.
// otherwise, only need to iterate over the joints of one component as they both store the same joint.
if (TryComp(uid, out JointComponent? jointComponentA) && HasComp<JointComponent>(other))
{
foreach (var joint in jointComponentA.Joints.Values)
{
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
if (!joint.CollideConnected && (other == joint.BodyAUid || other == joint.BodyBUid))
return false;
}
}
// Joints already handled before the contact pair is made.
var preventCollideMessage = new PreventCollideEvent(uid, other, body, otherBody, fixture, otherFixture);
RaiseLocalEvent(uid, ref preventCollideMessage);

View File

@@ -1,19 +0,0 @@
using System.Runtime.CompilerServices;
// The following allows another friend assembly access to the types marked as internal.
// SS14 engine assemblies are friends.
// This way internal is "Content can't touch this".
[assembly: InternalsVisibleTo("Robust.Server")]
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.Lite")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]
#if NET5_0_OR_GREATER
[module: SkipLocalsInit]
#endif

View File

@@ -1,7 +1,7 @@
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
@@ -14,7 +14,8 @@ namespace Robust.Shared.Prototypes;
internal sealed class YamlValidationContext :
ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
ITypeSerializer<NetEntity, ValueDataNode>,
ITypeSerializer<MapId, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes => true;
@@ -93,4 +94,40 @@ internal sealed class YamlValidationContext :
return new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture));
}
ValidationNode ITypeValidator<MapId, ValueDataNode>.Validate(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
ISerializationContext? context)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain map ids", true);
}
MapId ITypeReader<MapId, ValueDataNode>.Read(
ISerializationManager seri,
ValueDataNode node,
IDependencyCollection deps,
SerializationHookContext hookCtx,
ISerializationContext? ctx,
ISerializationManager.InstantiationDelegate<MapId>? instanceProvider)
{
return node.Value == "invalid" ? MapId.Nullspace : new MapId(int.Parse(node.Value));
}
DataNode ITypeWriter<MapId>.Write(
ISerializationManager seri,
MapId value,
IDependencyCollection deps,
bool alwaysWrite,
ISerializationContext? ctx)
{
if (value == MapId.Nullspace)
return new ValueDataNode("invalid");
return new ValueDataNode(value.Value.ToString(CultureInfo.InvariantCulture));
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using NetSerializer;
namespace Robust.Shared.Serialization;
/// <summary>
/// Custom serializer implementation for <see cref="BitArray"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is necessary as, since .NET 10, the internal layout of <see cref="BitArray"/> was changed.
/// The type now (internally) implements <see cref="ISerializable"/> for backwards compatibility with existing
/// <c>BinaryFormatter</c> code, but NetSerializer does not support <see cref="ISerializable"/>.
/// </para>
/// <para>
/// This code is designed to be backportable &amp; network compatible with the previous behavior on .NET 9.
/// </para>
/// </remarks>
internal sealed class NetBitArraySerializer : IDynamicTypeSerializer
{
// NOTE: MUST be a IDynamicTypeSerializer for compatibility!
// Can be changed in the future.
// For reference, the layout of BitArray before .NET 10 was:
// private int[] m_array;
// private int m_length;
// private int _version;
// NetSerializer serialized these in the following order (sorted by name):
// _version, m_array, m_length
public bool Handles(Type type)
{
return type == typeof(BitArray);
}
public IEnumerable<Type> GetSubtypes(Type type)
{
return [typeof(int[]), typeof(int)];
}
public void GenerateWriterMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: Stream, arg2: value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
public void GenerateReaderMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: stream, arg2: out value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
[UsedImplicitly]
private static void Write(Serializer serializer, Stream stream, BitArray value)
{
var intCount = (31 + value.Length) >> 5;
var ints = new int[intCount];
value.CopyTo(ints, 0);
serializer.SerializeDirect(stream, 0); // _version
serializer.SerializeDirect(stream, ints); // m_array
serializer.SerializeDirect(stream, value.Length); // m_length
}
[UsedImplicitly]
private static void Read(Serializer serializer, Stream stream, out BitArray value)
{
serializer.DeserializeDirect<int>(stream, out _); // _version
serializer.DeserializeDirect<int[]>(stream, out var array); // m_array
serializer.DeserializeDirect<int>(stream, out var length); // m_length
value = new BitArray(array)
{
Length = length
};
}
}

View File

@@ -89,7 +89,8 @@ namespace Robust.Shared.Serialization
CustomTypeSerializers = new[]
{
MappedStringSerializer.TypeSerializer,
new NetMathSerializer()
new NetMathSerializer(),
new NetBitArraySerializer()
}
};
_serializer = new Serializer(types, settings);

View File

@@ -9,7 +9,8 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
/// <summary>
/// Simple string serializer that just validates that strings correspond to valid component names
/// Simple string serializer that just validates that strings correspond to valid component names.
/// This will not fail when it encounters explicitly ignored components.
/// </summary>
public sealed class ComponentNameSerializer : ITypeSerializer<string, ValueDataNode>
{
@@ -17,7 +18,7 @@ public sealed class ComponentNameSerializer : ITypeSerializer<string, ValueDataN
IDependencyCollection dependencies, ISerializationContext? context = null)
{
var factory = dependencies.Resolve<IComponentFactory>();
if (!factory.TryGetRegistration(node.Value, out _))
if (!factory.TryGetRegistration(node.Value, out _) && factory.GetComponentAvailability(node.Value) != ComponentAvailability.Ignore)
return new ErrorNode(node, $"Unknown component kind: {node.Value}");
return new ValidatedValueNode(node);

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