Compare commits

...

22 Commits

Author SHA1 Message Date
PJB3005
ef26a680fc Version: 267.3.2 2025-12-02 00:56:58 +01:00
PJB3005
62fd21e489 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:58 +01:00
PJB3005
67e3f52a9d Version: 267.3.1 2025-12-01 16:05:19 +01:00
PJB3005
408feb6dd5 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:18 +01: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
41 changed files with 1319 additions and 460 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" />

View File

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

View File

@@ -54,6 +54,35 @@ END TEMPLATE-->
*None yet*
## 267.3.2
## 267.3.1
## 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
@@ -66,7 +95,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

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

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

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

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

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

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

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

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

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

@@ -1093,6 +1093,7 @@ Types:
- "string ToHexString(System.ReadOnlySpan`1<byte>)"
Converter`2: { All: True } # Delegate
DateOnly: { All: True }
DateTime: { All: True }
DateTimeKind: { } # Enum
DateTimeOffset: { All: True }
@@ -1425,6 +1426,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

@@ -352,7 +352,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);

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

@@ -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,18 +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("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

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.TypeParsers;
/// <summary>
/// This is custom type parser that uses reflection to search for constructible types that are the children of some base type.
/// </summary>
internal sealed class ReflectionTypeParser<TBase> : CustomTypeParser<Type> where TBase : class
{
[Dependency] private readonly IReflectionManager _reflection = default!;
private Dictionary<string, Type>? _cache;
private CompletionOption[]? _options;
[MemberNotNull(nameof(_cache))]
[MemberNotNull(nameof(_options))]
private void InitCache()
{
if (_cache != null && _options != null)
return;
_cache = _reflection.GetAllChildren(typeof(TBase))
.Where(x => x.HasParameterlessConstructor())
.ToDictionary(x => x.Name, x => x);
_options = _cache.Keys.Select(x => new CompletionOption(x)).ToArray();
}
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
{
InitCache();
var name = ctx.GetWord();
if (name is null)
{
ctx.Error = new OutOfInputError();
result = null;
return false;
}
if (_cache.TryGetValue(name, out result))
return true;
ctx.Error = new UnknownType(name);
result = null;
return false;
}
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
{
InitCache();
return CompletionResult.FromHintOptions(_options, GetArgHint(arg));
}
}

View File

@@ -33,10 +33,13 @@ namespace Robust.UnitTesting.Client.Graphics
new Angle(-Math.PI)
})
{
eyeManager.CurrentEye.Rotation = angle;
var worldAABB = eyeManager.GetWorldViewport();
var worldPort = eyeManager.GetWorldViewbounds();
Assert.That(worldAABB.EqualsApprox(worldPort.CalcBoundingBox()), $"Invalid EyeRotation bounds found for {angle}: Expected {worldAABB} and received {worldPort.CalcBoundingBox()}");
Assert.That(worldAABB.EqualsApprox(worldPort.CalcBoundingBox()),
$"Invalid EyeRotation bounds found for {angle}: Expected {worldAABB} and received {worldPort.CalcBoundingBox()}");
}
});
}

View File

@@ -196,10 +196,8 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after
// Act
sim.System<MoveEventTestSystem>().ResetCounters();
sim.Transform(ent1).Anchored = true;
xformSys.AnchorEntity(ent1);
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner));
sim.System<MoveEventTestSystem>().AssertMoved();
traversal.Enabled = true;
}
@@ -497,7 +495,7 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
// Act
sim.System<MoveEventTestSystem>().FailOnMove = true;
xformSys.Unanchor(ent1);
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(mapSys.GetMap(coordinates.MapId)));
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner));
sim.System<MoveEventTestSystem>().FailOnMove = false;
traversal.Enabled = true;
}