Compare commits

...

136 Commits

Author SHA1 Message Date
Paul
c6a9113144 version bump 2021-12-08 19:56:32 +01:00
Paul
c76d1d8c5c Merge branch 'master' of https://github.com/space-wizards/RobustToolbox 2021-12-08 19:55:42 +01:00
Paul
0e289873f5 pvs fixes 2021-12-08 19:55:24 +01:00
DrSmugleaf
3c7f87b8c3 Add GetComponentOrNull extension for nullable EUid 2021-12-06 12:44:11 +01:00
DrSmugleaf
d6c9420a74 Add an overload for nullable EUid CompOrNull 2021-12-06 12:38:04 +01:00
Vera Aguilera Puerto
43e67c0db9 Add new EntitySystem proxy methods. (#2310) 2021-12-06 12:33:04 +01:00
metalgearsloth
ca68041199 Optimise showchunkbb a lot (#2308) 2021-12-05 13:38:40 +11:00
Vera Aguilera Puerto
2aa5e8c07f Version 0.7.21 2021-12-03 13:07:22 +01:00
Vera Aguilera Puerto
f8b2412855 holy fucking shit, fuck exception tolerance only being in RELEASE 2021-12-03 13:07:22 +01:00
metalgearsloth
04782b83ab Add overload to draw rotated position (#2304) 2021-12-03 12:41:58 +01:00
metalgearsloth
e2f2b3a26d Get rid of unused dependency on EffectOverlay 2021-12-03 22:24:23 +11:00
metalgearsloth
5a8464b518 Get rid of grid log spam 2021-12-03 21:08:01 +11:00
Vera Aguilera Puerto
5f1feb9bb1 Version 0.7.20 2021-12-03 11:02:48 +01:00
Vera Aguilera Puerto
196e2bb427 The End of Entity (Komm, Süsser Todd: Part 1.0) (#2295)
Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
2021-12-03 11:01:59 +01:00
Acruid
f8bebee904 Remove Entity Transform/MetaData Component Cache (#2296)
Co-authored-by: Vera Aguilera Puerto <gradientvera@outlook.com>
2021-12-03 10:33:06 +01:00
Vera Aguilera Puerto
6515b08b41 Better ToPrettyString with EntityStringRepresentation (#2301) 2021-12-03 10:09:49 +01:00
Vera Aguilera Puerto
6e2f18d0d8 Add joints to RobustServerSimulation. 2021-12-02 12:33:40 +01:00
metalgearsloth
a2ecd63e9d Version: 0.7.19 2021-12-01 18:31:44 +11:00
metalgearsloth
6da9176410 Fixture 2 electric boogaloo (#2297) 2021-12-01 18:31:13 +11:00
metalgearsloth
ae9b771c8c Version: 0.7.18 2021-12-01 13:57:48 +11:00
metalgearsloth
d1e206864c Revert "Quick hotfix for fixtures"
This reverts commit 7316d9e950.
2021-12-01 13:56:32 +11:00
metalgearsloth
f812eb1e27 Revert "Move physics fixtures to its own component (#2220)"
This reverts commit ebc0fc9c60.
2021-12-01 13:54:32 +11:00
metalgearsloth
1dec0dd980 Version: 0.7.17 2021-12-01 13:07:10 +11:00
metalgearsloth
7316d9e950 Quick hotfix for fixtures 2021-12-01 13:06:54 +11:00
metalgearsloth
f7c2305bce Version: 0.7.16 2021-12-01 13:02:55 +11:00
metalgearsloth
ebc0fc9c60 Move physics fixtures to its own component (#2220) 2021-12-01 12:59:42 +11:00
metalgearsloth
d157aab786 Hotfix effect rotation (#2291) 2021-12-01 12:59:28 +11:00
Pieter-Jan Briers
70a5d1bad6 Return of ExpandPvsEvent
Paul has been declared irrational and is extremely coping rn.
2021-11-30 18:35:50 +01:00
Paul
2471bf8b4b we do a little trolling 2021-11-30 18:13:36 +01:00
Paul
e7c5706b04 comments out tests until i fix inventorycode 2021-11-30 18:09:19 +01:00
Paul
7b6d8a1465 fixes at least one test 2021-11-30 18:04:43 +01:00
Paul
31b750bd5b fixes that goddamn AngleSerializerTest 2021-11-30 17:50:52 +01:00
Paul
6ebd0eb4ae fuck mapmanager 2021-11-30 16:59:44 +01:00
Paul
37eac8c73f version 0.7.14 2021-11-30 15:19:13 +01:00
Paul Ritter
44649eea1c pvs refactor (#2247)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2021-11-30 15:07:08 +01:00
Pieter-Jan Briers
caa0212282 Swap ANGLE-related CVars around.
ANGLE is now off by default, angle_custom_swap_chain on if you set ANGLE.
2021-11-29 18:36:14 +01:00
Pieter-Jan Briers
7f9d08c8f9 Fix duplicate GL init on ANGLE GL context. 2021-11-29 18:35:24 +01:00
Pieter-Jan Briers
1c128e6b74 Return of the VRAM command. 2021-11-29 13:23:58 +01:00
Pieter-Jan Briers
099e7c5c48 IDXGIFactory6::EnumAdapterByGpuPreference for GLContextAngle. 2021-11-29 13:14:59 +01:00
Pieter-Jan Briers
2f76908efb Remove PlatformTarget from WebView project.
Fixes a compiler warning.
2021-11-29 08:35:39 +01:00
Pieter-Jan Briers
5adde7d588 Version: 0.7.13 2021-11-29 00:20:45 +01:00
Pieter-Jan Briers
7b1bb7df47 Expand PVS event (#2287) 2021-11-28 23:43:05 +01:00
Pieter-Jan Briers
8e5eb6ebbb Version: 0.7.12 2021-11-28 23:14:42 +01:00
Pieter-Jan Briers
87ef010348 Remove another NativeLibrary.Load -> exception catch.
This time with Optimus stuff.
2021-11-28 23:13:50 +01:00
wrexbe
229d1c248b Make benchmark debuggable (#2285) 2021-11-28 23:11:58 +01:00
wrexbe
8db606c4e4 Remove duplicate PluralRules.Generator in RobustToolbox.sln (#2286) 2021-11-28 23:03:46 +01:00
Pieter-Jan Briers
6b5181269b Do some trimming for client publishes.
Almost completely removes TerraFX and OpenToolkit.Graphics from client publishes size wise. This cuts publish size in half.
2021-11-28 22:26:05 +01:00
Pieter-Jan Briers
17b84c3520 Update to latest TerraFX, compile custom swapchain ANGLE in. 2021-11-28 22:12:17 +01:00
Pieter-Jan Briers
42875fc101 Remove RobustTaskScheduler entirely.
No longer necessary thanks to #2263.
2021-11-28 15:09:01 +01:00
wrexbe
72a7bc2ae7 Unwrap ParsedMain (#2263) 2021-11-28 15:07:25 +01:00
Leon Friedrich
bef4f75419 Fix AudioParams DataDefinition (#2283) 2021-11-28 15:00:23 +01:00
metalgearsloth
216509f89d Fix multi-subscriber crash for PVS (#2282) 2021-11-28 13:22:13 +01:00
20kdc
b7991204f1 Cleanup NativeLibrary.Load usages by replacing them with TryLoad (#2259)
Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
2021-11-28 00:41:53 +01:00
metalgearsloth
02987ac703 Update some hotpaths to use new transform method (#2281) 2021-11-28 00:41:08 +01:00
ike709
5cf8cb262f Updates cefglue (#2258)
Co-authored-by: ike709 <ike709@github.com>
2021-11-28 00:40:10 +01:00
metalgearsloth
49dfca169c Add scale command (#2256) 2021-11-28 00:38:41 +01:00
Acruid
33008a2bce Cannot use 'System.Runtime.CompilerServices.DefaultInterpolatedStringHandler' as a type argument 2021-11-26 19:24:31 -08:00
Acruid
c2e90132c0 Makes the scene command actually check for scene types instead of trying to instantiate random classes like ecs event args. 2021-11-26 08:30:18 -08:00
Vera Aguilera Puerto
9859b5b090 IConsoleCommands get dependencies injected by default now. (#2264)
As the DI gods intended.
2021-11-26 02:28:07 -08:00
metalgearsloth
20aec0a8f9 Add method to get worldpos and worldrot at the same time (#2226)
* Add method to get worldpos and worldrot at the same time

Somewhat of a gain because it halves the number of GetComponent<TransformComponent> + _parent.IsValid() calls

* Remove redundant methods

* Also inverse

* Test

* Import

* Add benchmark

* Delete benchmark
2021-11-26 01:47:51 -08:00
Vera Aguilera Puerto
a9ee78e40d MIDI player looping fix.
SetLoop sets the amount of times to loop a song. If you set it to 1, it'll loop once, if you set it to -1, it'll loop infinitely. This should have been 0 from the start, so it doesn't loop when disabled.
2021-11-25 15:06:55 +01:00
Vera Aguilera Puerto
69f36aac6f Add ToPrettyString method to EntityManager (#2257) 2021-11-24 12:04:18 +01:00
Pieter-Jan Briers
173b41ab9e Send level as log label again in loki. 2021-11-24 08:36:01 +01:00
Vera Aguilera Puerto
7796d7f065 IEntityManager.Clear() actually drops entity system as its xmldoc says. 2021-11-23 11:07:16 +01:00
metalgearsloth
175c111be9 Version: 0.7.11 2021-11-23 18:21:23 +11:00
Acruid
279cc0f83f AppearanceComponent Cleanup (#2253) 2021-11-23 18:16:37 +11:00
DrSmugleaf
b334d927a5 Version: 0.7.10 2021-11-22 19:02:28 +01:00
Vera Aguilera Puerto
bdb9b9af2b Remove PlayerHelpers static class. 2021-11-21 17:58:54 +01:00
Pieter-Jan Briers
5aa634b5eb Don't enable Lidgren UPnP support unless requested.
It wasn't being *used* but discovery was still happening, so...
2021-11-21 17:15:08 +01:00
Javier Guardia Fernández
60d7430fe7 Whitelist CallerArgumentExpressionAttribute, InterpolatedStringHandlerAttribute and IsByRefLikeAttribute (#2251) 2021-11-21 16:04:00 +01:00
Pieter-Jan Briers
a9aeff9b78 Version: 0.7.9 2021-11-21 15:20:41 +01:00
20kdc
e7a0409645 Status host uses net.port by default, add UPnP port forwarding option (#2237)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-11-21 15:19:39 +01:00
ShadowCommander
3e718575ff Change Button Pressed to work with non-focus keybinds (#1978) 2021-11-21 14:52:41 +01:00
Acruid
746ec9eab7 Marked PlayerSession find methods on PlayerManager Obsolete (#2244) 2021-11-21 14:51:17 +01:00
Pieter-Jan Briers
60e6ecb0cc Fix loading of command binds from keybinds file. 2021-11-21 14:48:17 +01:00
metalgearsloth
3a00e0d497 Expose physics contacts as an enumerable (#2250) 2021-11-21 18:41:12 +11:00
Leon Friedrich
24a5020b42 fix rotated box contains (#2248) 2021-11-19 22:59:49 -08:00
20kdc
bbb9e94ce9 Arbitrary occluder rotation (#2218)
* All forms of rotated occluders now work

* Utility getters for vector CW/CCW rotations
2021-11-19 15:09:45 -08:00
pointer-to-null
2bea9576f0 Public FpsCounter class. (#2246) 2021-11-19 15:00:14 -08:00
Tomeno
6e0be2b8c9 Clear placementManager when searching/clearing/closing window (#2245)
Co-authored-by: Tomeno <tomeno@lulzsec.co.uk>
2021-11-19 17:46:40 +01:00
Vera Aguilera Puerto
0a348dd1be Version 0.7.8 2021-11-18 19:34:43 +01:00
metalgearsloth
a95283913b Optimise broadphase a LOT for grids moving (#2243)
Previously it went through the entire movebuffer. Now it just uses the localbounds.
2021-11-18 15:40:28 +01:00
metalgearsloth
ebc73a71c2 Optimise GetBroadphases (#2242)
* Optimise GetBroadphases

Only used in public APIs. Should be significantly faster as it doesn't iterate every fixture on a grid.

* Fix the staged stuff
2021-11-18 15:24:18 +01:00
metalgearsloth
05321f0381 Miscellaneous fixes for thrusters (#2239) 2021-11-17 22:42:00 +01:00
Javier Guardia Fernández
6554144c42 Add CollectionsMarshal.AsSpan<T>(List<T>) to the sandbox whitelist (#2241) 2021-11-17 22:37:57 +01:00
20kdc
28e6cdc92f Clyde audio cleanup & fallback if audio device is not available (#2240) 2021-11-17 14:18:04 +01:00
metalgearsloth
2ec715b70e Remove fixture removal error
Client can predict these removals so it just spams the log.
2021-11-17 22:25:36 +11:00
Acruid
424e0768c8 Fixed some code that I forgot to change to help IoC instantiate protected constructors. 2021-11-17 03:10:12 -08:00
Acruid
e1b9327ec0 Misc Engine Tweaks (#2238) 2021-11-17 09:39:18 +01:00
Flipp Syder
80308a799f Adds collapsible widgets (#2199)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-11-16 19:09:59 +01:00
20kdc
e30f8f3e69 Async status host handlers to reduce ACZ stalling (#2235) 2021-11-16 13:27:29 +01:00
20kdc
302b910cf3 ACZ: Work around exception handling issues in ManagedHttpListener (#2236) 2021-11-16 13:26:10 +01:00
Pieter-Jan Briers
5a6a16e7dc Upgrade Loki sink to 4.0.0-beta3.
This fixes many bugs with it.
2021-11-15 13:42:19 +01:00
Pieter-Jan Briers
8d84c56a72 Remove testing profile opt stuff. 2021-11-15 11:32:51 +01:00
Pieter-Jan Briers
13f821be5f Grid entity deletion, attempt two.
Previous attempt broke an integration test.
2021-11-15 11:06:30 +01:00
Pieter-Jan Briers
d647ab1c61 Fix some warnings. 2021-11-15 10:53:39 +01:00
Pieter-Jan Briers
b902ef290a Fix directly deleting grid entities.
A bad return statement meant that the associated grid was not being properly deleted.

This fix means the game won't effectively crash PVS if a grid entity is directly deleted.
2021-11-15 10:49:05 +01:00
Pieter-Jan Briers
058d897529 Version: 0.7.7 2021-11-15 02:43:49 +01:00
Javier Guardia Fernández
8e173a7a18 Fix FormattedMessage serialization and add tests (#2233) 2021-11-15 02:42:47 +01:00
20kdc
5443f77526 Automatic Client Zipping (#2225) 2021-11-15 02:42:20 +01:00
20kdc
b406526592 Version update and tag script (#2234) 2021-11-15 02:18:20 +01:00
Pieter-Jan Briers
48697da450 Add version to Robust.Engine.props 2021-11-14 22:46:13 +01:00
Javier Guardia Fernández
06f20ea722 Add a method to get a FormattedMessage with markup tags included (#2223) 2021-11-14 19:42:36 +01:00
metalgearsloth
427378f94d Optimise physics broadphase cache (#2224) 2021-11-14 19:42:02 +01:00
Vera Aguilera Puerto
6cf5021efa Add Resolves to IoCManager and EntitySystemManager (#2231) 2021-11-14 19:41:34 +01:00
Tomeno
ad9bda2efe Adds setters on SS14Window for styling header & title (#2232)
Co-authored-by: T <tomeno@lulzsec.co.uk>
2021-11-14 19:19:12 +01:00
Pieter-Jan Briers
f0bf251acf Improve IUserInterfaceManager.RootControl comment 2021-11-14 19:08:14 +01:00
Pieter-Jan Briers
21f5fed32f Fix IsVisibleInTree calculation with secondary window roots 2021-11-14 19:08:14 +01:00
metalgearsloth
cc67a47c32 Pass around filter dependencies (#2228) 2021-11-14 02:08:09 +01:00
Javier Guardia Fernández
e78e7bacfe Add bool Contains(string, System.StringComparison) to the whitelist (#2230) 2021-11-14 01:37:33 +01:00
Vera Aguilera Puerto
6301008ac3 Revert "adds required check to the yamllinter (#2216)"
This reverts commit 772076826a.
2021-11-12 11:27:53 +01:00
Pieter-Jan Briers
dfd572a0aa Change Microsoft.Data.Sqlite reference to SQLitePCLRaw.bundle_e_sqlite, use version to fix .NET 6 build thing. 2021-11-11 18:23:16 +01:00
Pieter-Jan Briers
97fab99971 You can now bind console commands to keybinds. 2021-11-11 18:05:08 +01:00
Pieter-Jan Briers
25d6bd908b Remove BGRA32 uploads from Clyde.
BGRA uploads are not supported on GLES. Use a shader to swizzle WebViewControl contents instead.
2021-11-11 18:05:04 +01:00
Pieter-Jan Briers
fd5a8bf207 Fix GLContextAngle unbinding GL context when resizing secondary windows. 2021-11-11 18:05:03 +01:00
Paul Ritter
772076826a adds required check to the yamllinter (#2216)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2021-11-11 17:39:41 +01:00
Vera Aguilera Puerto
52d669e032 Fixes exceptions on shutdown. (#2219) 2021-11-11 17:39:25 +01:00
metalgearsloth
75fc9089c3 Add weld joints and other misc changes for docking (#2197) 2021-11-11 19:54:08 +11:00
Kara D
0972601a43 Revert "entity creation crash fixes"
This reverts commit 603c252c48.
2021-11-10 12:09:31 -07:00
Paul
603c252c48 entity creation crash fixes 2021-11-10 18:57:47 +01:00
Paul
d5b1c044b7 fixes the crash for realsies 2021-11-10 18:34:21 +01:00
metalgearsloth
4600f0531d Fix centre of mass (#2212) 2021-11-10 17:25:38 +01:00
Leon Friedrich
c88498eca9 Add function to directly perform lookups using an EntityLookupComponent (#2200) 2021-11-11 01:12:49 +11:00
Paul Ritter
f15f1eb345 adds gridremovalevent (#2201)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2021-11-10 23:27:11 +11:00
Pieter-Jan Briers
5be3ced05a UI scaling for WebViewControl. 2021-11-10 11:57:58 +01:00
Pieter-Jan Briers
7f03e88e97 OSWindow not adjusts SetSize when window is resized. 2021-11-10 11:57:12 +01:00
Pieter-Jan Briers
8e3fa3e52d Pass + as command line arg to client/server to execute commands after init. 2021-11-10 02:01:31 +01:00
metalgearsloth
f9ae3e1fc2 Significantly optimise server when PVS disabled (#2196) 2021-11-10 01:38:37 +01:00
mirrorcult
bf9e95fa8a Update README.md 2021-11-09 17:21:37 -07:00
ZorenZal
030a7d265b Added set English language checkbox (#2136) 2021-11-10 01:12:36 +01:00
Pieter-Jan Briers
df70e94743 ununupdated comment 2021-11-10 01:09:06 +01:00
metalgearsloth
d68cd4d7eb Add overlay for physics COM (#2210) 2021-11-10 01:05:08 +01:00
Pieter-Jan Briers
d098881bff Make BVH tree expansion exponential.
Fixes #2215
2021-11-10 00:59:53 +01:00
ike709
b8fbe32c27 Bump Robust.Client.WebView TargetFramework to .NET 6 (#2214) 2021-11-09 20:17:37 +01:00
Pieter-Jan Briers
02d2bd31e7 Update Lidgren submodule; re-enable encryption. 2021-11-09 19:34:55 +01:00
Pieter-Jan Briers
bd0dba0df0 Disable network encryption to bandaid it for .NET 6. 2021-11-09 18:07:59 +01:00
246 changed files with 8415 additions and 3952 deletions

View File

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

View File

@@ -6,4 +6,5 @@
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
<Import Project="Robust.Engine.Version.props" />
</Project>

View File

@@ -0,0 +1,122 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<!--
Stuff for using ILLink trimming without self-contained deployments.
This is not something officially supported by the .NET SDK currently, but we can simply run ILLink ourselves.
-->
<!--
A lot of stuff taken from Microsoft.NET.ILLink.targets in the SDK files.
-->
<ItemDefinitionGroup>
<RobustLinkRoots>
<Visible>false</Visible>
</RobustLinkRoots>
<RobustLinkAssemblies>
<Visible>false</Visible>
</RobustLinkAssemblies>
</ItemDefinitionGroup>
<Target Name="RobustILLink"
BeforeTargets="ILLink"
Condition="'$(PublishTrimmed)' != 'true' And
'$(RobustILLink)' == 'true'"
DependsOnTargets="_ComputeAssembliesToPostprocessOnPublish">
<ComputeManagedAssemblies Assemblies="@(ResolvedFileToPublish)">
<Output TaskParameter="ManagedAssemblies" ItemName="_ResolvedFileToPublishFiltered" />
</ComputeManagedAssemblies>
<JoinItems Left="@(_ResolvedFileToPublishFiltered)" LeftKey="FileName" LeftMetadata="*"
Right="@(RobustLinkRoots)"
ItemSpecToUse="Left">
<Output TaskParameter="JoinResult" ItemName="_RobustLinkRootsJoined" />
</JoinItems>
<JoinItems Left="@(_ResolvedFileToPublishFiltered)" LeftKey="FileName" LeftMetadata="*"
Right="@(RobustLinkAssemblies)"
ItemSpecToUse="Left">
<Output TaskParameter="JoinResult" ItemName="_RobustLinkAssembliesJoined" />
</JoinItems>
<PropertyGroup>
<TrimMode Condition=" '$(TrimMode)' == '' ">link</TrimMode>
<TrimmerDefaultAction Condition=" '$(TrimmerDefaultAction)' == '' ">copy</TrimmerDefaultAction>
<_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs)</_ExtraTrimmerArgs>
<ILLinkTreatWarningsAsErrors Condition=" '$(ILLinkTreatWarningsAsErrors)' == '' ">$(TreatWarningsAsErrors)</ILLinkTreatWarningsAsErrors>
<TrimmerSingleWarn Condition=" '$(TrimmerSingleWarn)' == '' ">true</TrimmerSingleWarn>
</PropertyGroup>
<ItemGroup>
<RobustAssemblyToLink Include="@(_RobustLinkRootsJoined)">
<TrimMode>Copy</TrimMode>
</RobustAssemblyToLink>
<RobustAssemblyToLink Include="@(_RobustLinkAssembliesJoined)">
<TrimMode>Link</TrimMode>
</RobustAssemblyToLink>
</ItemGroup>
<ItemGroup>
<!-- The linker implicitly picks up PDBs next to input assemblies. We will filter these out of the publish set. -->
<__PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(RobustAssemblyToLink->'%(RelativeDir)%(Filename).pdb')" />
<_PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(__PDBToLink)" />
</ItemGroup>
<ItemGroup>
<_LinkedResolvedFileToPublishCandidate Include="@(RobustAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" />
<_LinkedResolvedFileToPublishCandidate Include="@(_PDBToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" />
</ItemGroup>
<!--<Message Text="@(ResolvedFileToPublish)" Importance="high" />-->
<ItemGroup>
<_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)"
Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" />
</ItemGroup>
<Delete Files="@(_LinkedResolvedFileToPublishCandidate)" />
<ILLink AssemblyPaths="@(RobustAssemblyToLink)"
ReferenceAssemblyPaths="@(ReferencePath)"
RootAssemblyNames="@(RobustLinkRoots)"
TrimMode="Skip"
DefaultAction="$(TrimmerDefaultAction)"
RemoveSymbols="false"
FeatureSettings="@(_TrimmerFeatureSettings)"
CustomData="@(_TrimmerCustomData)"
BeforeFieldInit="$(_TrimmerBeforeFieldInit)"
OverrideRemoval="$(_TrimmerOverrideRemoval)"
UnreachableBodies="$(_TrimmerUnreachableBodies)"
UnusedInterfaces="$(_TrimmerUnusedInterfaces)"
IPConstProp="$(_TrimmerIPConstProp)"
Sealer="$(_TrimmerSealer)"
Warn="$(ILLinkWarningLevel)"
NoWarn="$(NoWarn)"
TreatWarningsAsErrors="$(ILLinkTreatWarningsAsErrors)"
WarningsAsErrors="$(WarningsAsErrors)"
WarningsNotAsErrors="$(WarningsNotAsErrors)"
SingleWarn="$(TrimmerSingleWarn)"
CustomSteps="@(_TrimmerCustomSteps)"
RootDescriptorFiles="@(TrimmerRootDescriptor)"
OutputDirectory="$(IntermediateLinkDir)"
DumpDependencies="$(_TrimmerDumpDependencies)"
ExtraArgs="$(_ExtraTrimmerArgs)"
ToolExe="$(_DotNetHostFileName)"
ToolPath="$(_DotNetHostDirectory)"
ContinueOnError="ErrorAndContinue">
<Output TaskParameter="ExitCode" PropertyName="_ILLinkExitCode" />
</ILLink>
<Touch Files="$(_LinkSemaphore)" AlwaysCreate="true" Condition=" '$(_ILLinkExitCode)' == '0' " />
<ItemGroup>
<_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" />
<ResolvedFileToPublish Remove="@(RobustAssemblyToLink)" />
<ResolvedFileToPublish Remove="@(_PDBToLink)" />
<ResolvedFileToPublish Include="@(_LinkedResolvedFileToPublish)" />
</ItemGroup>
</Target>
</Project>

View File

@@ -18,20 +18,18 @@ namespace OpenToolkit.GraphicsLibraryFramework
NativeLibrary.SetDllImportResolver(typeof(GLFWNative).Assembly, (name, assembly, path) =>
{
// Please keep in sync with what Robust.Shared/DllMapHelper.cs does.
if (name != "glfw3.dll")
{
return IntPtr.Zero;
}
if (OperatingSystem.IsLinux())
{
return NativeLibrary.Load("libglfw.so.3", assembly, path);
}
string rName = null;
if (OperatingSystem.IsLinux()) rName = "libglfw.so.3";
else if (OperatingSystem.IsMacOS()) rName = "libglfw.3.dylib";
if (OperatingSystem.IsMacOS())
{
return NativeLibrary.Load("libglfw.3.dylib", assembly, path);
}
if ((rName != null) && NativeLibrary.TryLoad(rName, assembly, path, out var handle))
return handle;
return IntPtr.Zero;
});

View File

@@ -10,7 +10,7 @@ Use the [content repo](https://github.com/space-wizards/space-station-14) for ac
## Documentation/Wiki
The [HackMD Wiki](https://hackmd.io/@ss14/docs/wiki) has documentation on SS14s content, engine, game design and more. We also have lots of resources for new contributors to the project.
The [wiki](https://docs.spacestation14.io/) has documentation on SS14s content, engine, game design and more. We also have lots of resources for new contributors to the project.
## Contributing

View File

@@ -7,3 +7,8 @@
- type: shader
id: shaded
kind: canvas
- type: shader
id: bgra
kind: source
path: "/Shaders/Internal/bgra.swsl"

View File

@@ -0,0 +1,6 @@
// Swaps B and R channel so you can render BGRA stuff without swizzling at upload time.
// Currently used by CEF.
void fragment() {
COLOR = zTexture(UV).bgra;
}

View File

@@ -1,4 +1,6 @@
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using System;
namespace Robust.Benchmarks
{
@@ -8,7 +10,14 @@ namespace Robust.Benchmarks
// --anyCategories=ctg1,ctg2
public static void Main(string[] args)
{
#if DEBUG
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
Console.WriteLine("THE DEBUG BUILD IS ONLY GOOD FOR FIXING A CRASHING BENCHMARK\n");
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
#else
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
#endif
}
}
}

View File

@@ -9,14 +9,16 @@ namespace Robust.Client.WebView.Cef
{
internal sealed class ImageBuffer
{
public Image<Bgra32> Buffer { get; private set; } = new(1, 1);
public Image<Rgba32> Buffer { get; private set; } = new(1, 1);
public unsafe void UpdateBuffer(int width, int height, IntPtr buffer, CefRectangle dirtyRect)
{
if (width != Buffer.Width || height != Buffer.Height)
UpdateSize(width, height);
var span = new ReadOnlySpan<Bgra32>((void*) buffer, width * height);
// NOTE: Image data from CEF is actually BGRA32, not RGBA32.
// OpenGL ES does not allow uploading BGRA data, so we pretend it's RGBA32 and use a shader to swizzle it.
var span = new ReadOnlySpan<Rgba32>((void*) buffer, width * height);
ImageSharpExt.Blit(
span,
@@ -28,7 +30,7 @@ namespace Robust.Client.WebView.Cef
private void UpdateSize(int width, int height)
{
Buffer = new Image<Bgra32>(width, height);
Buffer = new Image<Rgba32>(width, height);
}
}
}

View File

@@ -18,7 +18,9 @@ namespace Robust.Client.WebView.Cef
{
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
{
var impl = new ControlImpl(owner);
var shader = _prototypeManager.Index<ShaderPrototype>("bgra");
var shaderInstance = shader.Instance();
var impl = new ControlImpl(owner, shaderInstance);
_dependencyCollection.InjectDependencies(impl);
return impl;
}
@@ -131,10 +133,12 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IInputManager _inputMgr = default!;
public readonly WebViewControl Owner;
private readonly ShaderInstance _shaderInstance;
public ControlImpl(WebViewControl owner)
public ControlImpl(WebViewControl owner, ShaderInstance shaderInstance)
{
Owner = owner;
_shaderInstance = shaderInstance;
}
private const int ScrollSpeed = 50;
@@ -183,7 +187,7 @@ namespace Robust.Client.WebView.Cef
// Create the web browser! And by default, we go to about:blank.
var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);
var texture = _clyde.CreateBlankTexture<Bgra32>(Vector2i.One);
var texture = _clyde.CreateBlankTexture<Rgba32>(Vector2i.One);
_data = new LiveData(texture, client, browser, renderer);
}
@@ -386,7 +390,7 @@ namespace Robust.Client.WebView.Cef
_data.Browser.GetHost().NotifyMoveOrResizeStarted();
_data.Browser.GetHost().WasResized();
_data.Texture.Dispose();
_data.Texture = _clyde.CreateBlankTexture<Bgra32>((Owner.PixelWidth, Owner.PixelHeight));
_data.Texture = _clyde.CreateBlankTexture<Rgba32>((Owner.PixelWidth, Owner.PixelHeight));
}
public void Draw(DrawingHandleScreen handle)
@@ -404,6 +408,7 @@ namespace Robust.Client.WebView.Cef
Math.Min(Owner.PixelWidth, bufImg.Width),
Math.Min(Owner.PixelHeight, bufImg.Height)));
handle.UseShader(_shaderInstance);
handle.DrawTexture(_data.Texture, Vector2.Zero);
}
@@ -533,8 +538,7 @@ namespace Robust.Client.WebView.Cef
if (_control.Owner.Disposed)
return false;
// TODO CEF: Get actual scale factor?
screenInfo.DeviceScaleFactor = 1.0f;
screenInfo.DeviceScaleFactor = _control.Owner.UIScale;
return true;
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
@@ -12,6 +13,7 @@ namespace Robust.Client.WebView.Cef
private CefApp _app = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public void Initialize()
{

View File

@@ -3,8 +3,7 @@
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>WinExe</OutputType>
</PropertyGroup>

View File

@@ -297,7 +297,7 @@ namespace Robust.Client.Audio.Midi
set
{
lock (_playerStateLock)
_player?.SetLoop(value ? -1 : 1);
_player?.SetLoop(value ? -1 : 0);
_loopMidi = value;
}
}

View File

@@ -5,6 +5,7 @@ using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Audio;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Input;
using Robust.Client.Map;
@@ -82,8 +83,9 @@ namespace Robust.Client
case GameController.DisplayMode.Headless:
IoCManager.Register<IClyde, ClydeHeadless>();
IoCManager.Register<IClipboardManager, ClydeHeadless>();
IoCManager.Register<IClydeAudio, ClydeHeadless>();
IoCManager.Register<IClydeInternal, ClydeHeadless>();
IoCManager.Register<IClydeAudio, ClydeAudioHeadless>();
IoCManager.Register<IClydeAudioInternal, ClydeAudioHeadless>();
IoCManager.Register<IInputManager, InputManager>();
IoCManager.Register<IFileDialogManager, DummyFileDialogManager>();
IoCManager.Register<IUriOpener, UriOpenerDummy>();
@@ -91,8 +93,9 @@ namespace Robust.Client
case GameController.DisplayMode.Clyde:
IoCManager.Register<IClyde, Clyde>();
IoCManager.Register<IClipboardManager, Clyde>();
IoCManager.Register<IClydeAudio, Clyde>();
IoCManager.Register<IClydeInternal, Clyde>();
IoCManager.Register<IClydeAudio, FallbackProxyClydeAudio>();
IoCManager.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
IoCManager.Register<IInputManager, ClydeInputManager>();
IoCManager.Register<IFileDialogManager, FileDialogManager>();
IoCManager.Register<IUriOpener, UriOpener>();

View File

@@ -4,177 +4,186 @@ using Robust.Shared;
using Robust.Shared.Utility;
using C = System.Console;
namespace Robust.Client
namespace Robust.Client;
internal sealed class CommandLineArgs
{
internal sealed class CommandLineArgs
public MountOptions MountOptions { get; }
public bool Headless { get; }
public bool SelfContained { get; }
public bool Connect { get; }
public string ConnectAddress { get; }
public string? Ss14Address { get; }
public bool Launcher { get; }
public string? Username { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
public IReadOnlyList<string> ExecCommands { get; set; }
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
{
public MountOptions MountOptions { get; }
public bool Headless { get; }
public bool SelfContained { get; }
public bool Connect { get; }
public string ConnectAddress { get; }
public string? Ss14Address { get; }
public bool Launcher { get; }
public string? Username { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
parsed = null;
var headless = false;
var selfContained = false;
var connect = false;
var connectAddress = "localhost";
string? ss14Address = null;
var launcher = false;
string? username = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
var execCommands = new List<string>();
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
{
parsed = null;
var headless = false;
var selfContained = false;
var connect = false;
var connectAddress = "localhost";
string? ss14Address = null;
var launcher = false;
string? username = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
var arg = enumerator.Current;
if (arg == "--connect")
{
var arg = enumerator.Current;
if (arg == "--connect")
connect = true;
}
else if (arg == "--connect-address")
{
if (!enumerator.MoveNext())
{
connect = true;
}
else if (arg == "--connect-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing connection address.");
return false;
}
connectAddress = enumerator.Current;
}
else if (arg == "--ss14-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing SS14 address.");
return false;
}
ss14Address = enumerator.Current;
}
else if (arg == "--self-contained")
{
selfContained = true;
}
else if (arg == "--launcher")
{
launcher = true;
}
else if (arg == "--headless")
{
headless = true;
}
else if (arg == "--username")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing username.");
return false;
}
username = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
C.WriteLine("Missing connection address.");
return false;
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
connectAddress = enumerator.Current;
}
else if (arg == "--ss14-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing SS14 address.");
return false;
}
parsed = new CommandLineArgs(
headless,
selfContained,
connect,
launcher,
username,
cvars,
logLevels,
connectAddress,
ss14Address,
mountOptions);
ss14Address = enumerator.Current;
}
else if (arg == "--self-contained")
{
selfContained = true;
}
else if (arg == "--launcher")
{
launcher = true;
}
else if (arg == "--headless")
{
headless = true;
}
else if (arg == "--username")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing username.");
return false;
}
return true;
username = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
return false;
}
else if (arg.StartsWith("+"))
{
execCommands.Add(arg[1..]);
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
}
private static void PrintHelp()
{
C.WriteLine(@"
parsed = new CommandLineArgs(
headless,
selfContained,
connect,
launcher,
username,
cvars,
logLevels,
connectAddress,
ss14Address,
mountOptions,
execCommands);
return true;
}
private static void PrintHelp()
{
C.WriteLine(@"
Usage: Robust.Client [options] [+command [+command]]
Options:
--headless Run without graphics/audio/input.
--self-contained Store data relative to executable instead of user-global locations.
@@ -189,30 +198,34 @@ Options:
--mount-dir Resource directory to mount.
--mount-zip Resource zip to mount.
--help Display this help text and exit.
");
}
private CommandLineArgs(
bool headless,
bool selfContained,
bool connect,
bool launcher,
string? username,
IReadOnlyCollection<(string key, string value)> cVars,
IReadOnlyCollection<(string key, string value)> logLevels,
string connectAddress, string? ss14Address,
MountOptions mountOptions)
{
Headless = headless;
SelfContained = selfContained;
Connect = connect;
Launcher = launcher;
Username = username;
CVars = cVars;
LogLevels = logLevels;
ConnectAddress = connectAddress;
Ss14Address = ss14Address;
MountOptions = mountOptions;
}
+command: You can pass a set of commands, prefixed by +,
to be executed in the console in order after the game has finished initializing.
");
}
private CommandLineArgs(
bool headless,
bool selfContained,
bool connect,
bool launcher,
string? username,
IReadOnlyCollection<(string key, string value)> cVars,
IReadOnlyCollection<(string key, string value)> logLevels,
string connectAddress, string? ss14Address,
MountOptions mountOptions,
IReadOnlyList<string> execCommands)
{
Headless = headless;
SelfContained = selfContained;
Connect = connect;
Launcher = launcher;
Username = username;
CVars = cVars;
LogLevels = logLevels;
ConnectAddress = connectAddress;
Ss14Address = ss14Address;
MountOptions = mountOptions;
ExecCommands = execCommands;
}
}

View File

@@ -24,6 +24,9 @@ namespace Robust.Client.Console.Commands
case "aabbs":
system.Flags ^= PhysicsDebugFlags.AABBs;
break;
case "com":
system.Flags ^= PhysicsDebugFlags.COM;
break;
case "contactnormals":
system.Flags ^= PhysicsDebugFlags.ContactNormals;
break;

View File

@@ -162,6 +162,7 @@ namespace Robust.Client.Debugging
ShapeInfo = 1 << 3,
Joints = 1 << 4,
AABBs = 1 << 5,
COM = 1 << 6,
}
internal sealed class PhysicsDebugOverlay : Overlay
@@ -190,13 +191,13 @@ namespace Robust.Client.Debugging
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
private void DrawWorld(DrawingHandleWorld worldHandle)
private void DrawWorld(DrawingHandleWorld worldHandle, OverlayDrawArgs args)
{
var viewport = _eyeManager.GetWorldViewport();
var viewBounds = _eyeManager.GetWorldViewbounds();
var viewBounds = args.WorldBounds;
var viewAABB = args.WorldAABB;
var mapId = _eyeManager.CurrentMap;
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0 && !viewport.IsEmpty())
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0)
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
@@ -237,7 +238,32 @@ namespace Robust.Client.Debugging
}
}
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.AABBs) != 0 && !viewport.IsEmpty())
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.COM) != 0)
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
Color color;
const float Alpha = 0.25f;
float size;
if (physBody.Owner.HasComponent<MapGridComponent>())
{
color = Color.Orange.WithAlpha(Alpha);
size = 1f;
}
else
{
color = Color.Purple.WithAlpha(Alpha);
size = 0.2f;
}
var transform = physBody.GetTransform();
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), size, color);
}
}
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.AABBs) != 0)
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
@@ -271,7 +297,7 @@ namespace Robust.Client.Debugging
{
if (jointComponent.JointCount == 0 ||
!_entityManager.TryGetComponent(jointComponent.Owner.Uid, out TransformComponent? xf1) ||
!viewport.Contains(xf1.WorldPosition)) continue;
!viewAABB.Contains(xf1.WorldPosition)) continue;
foreach (var (_, joint) in jointComponent.Joints)
{
@@ -312,7 +338,7 @@ namespace Robust.Client.Debugging
}
}
private void DrawScreen(DrawingHandleScreen screenHandle)
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
{
var mapId = _eyeManager.CurrentMap;
var mousePos = _inputManager.MouseScreenPosition;
@@ -359,10 +385,10 @@ namespace Robust.Client.Debugging
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen((DrawingHandleScreen) args.DrawingHandle);
DrawScreen((DrawingHandleScreen) args.DrawingHandle, args);
break;
case OverlaySpace.WorldSpace:
DrawWorld((DrawingHandleWorld) args.DrawingHandle);
DrawWorld((DrawingHandleWorld) args.DrawingHandle, args);
break;
}
}
@@ -382,8 +408,8 @@ namespace Robust.Client.Debugging
if (edge.OneSided)
{
worldHandle.DrawCircle(v1, 0.5f, color);
worldHandle.DrawCircle(v2, 0.5f, color);
worldHandle.DrawCircle(v1, 0.1f, color);
worldHandle.DrawCircle(v2, 0.1f, color);
}
break;
@@ -416,11 +442,45 @@ namespace Robust.Client.Debugging
var p1 = matrix1.Transform(joint.LocalAnchorA);
var p2 = matrix2.Transform(joint.LocalAnchorB);
var xfa = new Transform(xf1, xform1.WorldRotation);
var xfb = new Transform(xf2, xform2.WorldRotation);
switch (joint)
{
case DistanceJoint:
worldHandle.DrawLine(xf1, xf2, JointColor);
break;
case PrismaticJoint prisma:
var pA = Transform.Mul(xfa, joint.LocalAnchorA);
var pB = Transform.Mul(xfb, joint.LocalAnchorB);
var axis = Transform.Mul(xfa.Quaternion2D, prisma._localXAxisA);
Color c1 = new(0.7f, 0.7f, 0.7f);
Color c2 = new(0.3f, 0.9f, 0.3f);
Color c3 = new(0.9f, 0.3f, 0.3f);
Color c4 = new(0.3f, 0.3f, 0.9f);
Color c5 = new(0.4f, 0.4f, 0.4f);
worldHandle.DrawLine(pA, pB, c5);
if (prisma.EnableLimit)
{
var lower = pA + axis * prisma.LowerTranslation;
var upper = pA + axis * prisma.UpperTranslation;
var perp = Transform.Mul(xfa.Quaternion2D, prisma._localYAxisA);
worldHandle.DrawLine(lower, upper, c1);
worldHandle.DrawLine(lower - perp * 0.5f, lower + perp * 0.5f, c2);
worldHandle.DrawLine(upper - perp * 0.5f, upper + perp * 0.5f, c3);
}
else
{
worldHandle.DrawLine(pA - axis * 1.0f, pA + axis * 1.0f, c1);
}
worldHandle.DrawCircle(pA, 0.5f, c1);
worldHandle.DrawCircle(pB, 0.5f, c4);
break;
default:
worldHandle.DrawLine(xf1, p1, JointColor);
worldHandle.DrawLine(p1, p2, JointColor);

View File

@@ -59,6 +59,7 @@ namespace Robust.Client
[Dependency] private readonly IViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
@@ -86,6 +87,7 @@ namespace Robust.Client
internal bool StartupContinue(DisplayMode displayMode)
{
_clyde.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
_taskManager.Initialize();
@@ -198,6 +200,8 @@ namespace Robust.Client
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
ProgramShared.RunExecCommands(_console, _commandLineArgs?.ExecCommands);
return true;
}
@@ -437,6 +441,7 @@ namespace Robust.Client
private void Update(FrameEventArgs frameEventArgs)
{
_webViewHook?.Update();
_clydeAudio.FrameProcess(frameEventArgs);
_clyde.FrameProcess(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
_stateManager.FrameUpdate(frameEventArgs);
@@ -535,6 +540,7 @@ namespace Robust.Client
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
_clydeAudio.Shutdown();
}
private sealed record ResourceManifestData(string[] Modules);

View File

@@ -27,7 +27,6 @@ namespace Robust.Client.GameObjects
RegisterClass<ClientOccluderComponent>();
RegisterClass<OccluderTreeComponent>();
RegisterClass<EyeComponent>();
RegisterClass<AppearanceComponent>();
RegisterClass<AnimationPlayerComponent>();
RegisterClass<TimerComponent>();

View File

@@ -40,12 +40,12 @@ namespace Robust.Client.GameObjects
void IClientEntityManagerInternal.InitializeEntity(IEntity entity)
{
base.InitializeEntity((Entity)entity);
base.InitializeEntity(entity);
}
void IClientEntityManagerInternal.StartEntity(IEntity entity)
{
base.StartEntity((Entity)entity);
base.StartEntity(entity);
}
#region IEntityNetworkManager impl

View File

@@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(SharedAppearanceComponent))]
public sealed class AppearanceComponent : SharedAppearanceComponent
{
[ViewVariables]
private Dictionary<object, object> data = new();
[ViewVariables]
[DataField("visuals")]
internal List<AppearanceVisualizer> Visualizers = new();
[ViewVariables]
private bool _appearanceDirty;
public override void SetData(string key, object value)
{
SetData(key, value);
}
public override void SetData(Enum key, object value)
{
SetData(key, value);
}
public override T GetData<T>(string key)
{
return (T) data[key];
}
public override T GetData<T>(Enum key)
{
return (T) data[key];
}
internal T GetData<T>(object key)
{
return (T) data[key];
}
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
internal bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
{
if (this.data.TryGetValue(key, out var dat))
{
data = (T) dat;
return true;
}
data = default!;
return false;
}
private void SetData(object key, object value)
{
if (data.TryGetValue(key, out var existing) && existing.Equals(value)) return;
data[key] = value;
MarkDirty();
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is not AppearanceComponentState actualState)
return;
var stateDiff = data.Count != actualState.Data.Count;
if (!stateDiff)
{
foreach (var (key, value) in data)
{
if (!actualState.Data.TryGetValue(key, out var stateValue) ||
!value.Equals(stateValue))
{
stateDiff = true;
break;
}
}
}
if (!stateDiff) return;
data = actualState.Data;
MarkDirty();
}
internal void MarkDirty()
{
if (_appearanceDirty)
{
return;
}
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
_appearanceDirty = true;
}
internal void UnmarkDirty()
{
_appearanceDirty = false;
}
protected override void Initialize()
{
base.Initialize();
foreach (var visual in Visualizers)
{
visual.InitializeEntity(Owner);
}
MarkDirty();
}
}
/// <summary>
/// Handles the visualization of data inside of an appearance component.
/// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract class AppearanceVisualizer
{
/// <summary>
/// Initializes an entity to be managed by this appearance controller.
/// DO NOT assume this is your only entity. Visualizers are shared.
/// </summary>
public virtual void InitializeEntity(IEntity entity)
{
}
/// <summary>
/// Called whenever appearance data for an entity changes.
/// Update its visuals here.
/// </summary>
/// <param name="component">The appearance component of the entity that might need updating.</param>
public virtual void OnChangeData(AppearanceComponent component)
{
}
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Client.GameObjects;
/// <summary>
/// Handles the visualization of data inside of an appearance component.
/// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract class AppearanceVisualizer
{
/// <summary>
/// Initializes an entity to be managed by this appearance controller.
/// DO NOT assume this is your only entity. Visualizers are shared.
/// </summary>
public virtual void InitializeEntity(IEntity entity)
{
}
/// <summary>
/// Called whenever appearance data for an entity changes.
/// Update its visuals here.
/// </summary>
/// <param name="component">The appearance component of the entity that might need updating.</param>
public virtual void OnChangeData(AppearanceComponent component)
{
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects;
/// <summary>
/// This is the client instance of <see cref="AppearanceComponent"/>.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(AppearanceComponent))]
public sealed class ClientAppearanceComponent : AppearanceComponent
{
[ViewVariables]
private bool _appearanceDirty;
[ViewVariables]
[DataField("visuals")]
internal List<AppearanceVisualizer> Visualizers = new();
protected override void MarkDirty()
{
if (_appearanceDirty)
return;
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
_appearanceDirty = true;
}
protected override void Initialize()
{
base.Initialize();
foreach (var visual in Visualizers)
{
visual.InitializeEntity(Owner);
}
MarkDirty();
}
internal void UnmarkDirty()
{
_appearanceDirty = false;
}
}

View File

@@ -74,10 +74,10 @@ namespace Robust.Client.GameObjects
return;
}
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var position = Owner.Transform.Coordinates;
void CheckDir(Direction dir, OccluderDir oclDir)
{
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var position = Owner.Transform.Coordinates;
foreach (var neighbor in grid.GetInDir(position, dir))
{
if (Owner.EntityManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
@@ -88,10 +88,20 @@ namespace Robust.Client.GameObjects
}
}
CheckDir(Direction.North, OccluderDir.North);
CheckDir(Direction.East, OccluderDir.East);
CheckDir(Direction.South, OccluderDir.South);
CheckDir(Direction.West, OccluderDir.West);
var angle = Owner.Transform.LocalRotation;
var dirRolling = angle.GetCardinalDir();
// dirRolling starts at effective south
CheckDir(dirRolling, OccluderDir.South);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.West);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.North);
dirRolling = dirRolling.GetClockwise90Degrees();
CheckDir(dirRolling, OccluderDir.East);
}
[Flags]

View File

@@ -213,6 +213,7 @@ namespace Robust.Client.GameObjects
layer.Color = layerDatum.Color;
layer.Rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Scale = layerDatum.Scale;
@@ -1259,7 +1260,7 @@ namespace Robust.Client.GameObjects
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
var localMatrix = GetLocalMatrix();
var spriteMatrix = GetLocalMatrix();
foreach (var layer in Layers)
{
@@ -1270,9 +1271,10 @@ namespace Robust.Client.GameObjects
var numDirs = GetLayerDirectionCount(layer);
var layerRotation = worldRotation + layer.Rotation;
var layerPosition = worldPosition + layerRotation.RotateVec(layer._offset);
CalcModelMatrix(numDirs, eyeRotation, layerRotation, worldPosition, out var modelMatrix);
Matrix3.Multiply(ref localMatrix, ref modelMatrix, out var transformMatrix);
CalcModelMatrix(numDirs, eyeRotation, layerRotation, layerPosition, out var modelMatrix);
Matrix3.Multiply(ref spriteMatrix, ref modelMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
RenderLayer(drawingHandle, layer, eyeRotation, layerRotation, overrideDirection);
@@ -1290,7 +1292,7 @@ namespace Robust.Client.GameObjects
var layerColor = color * layer.Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter) + layer.Offset;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
@@ -1723,7 +1725,7 @@ namespace Robust.Client.GameObjects
}
}
private Vector2 _offset;
internal Vector2 _offset;
[ViewVariables]
public DirectionOffset DirOffset { get; set; }

View File

@@ -14,8 +14,7 @@ namespace Robust.Client.GameObjects
private static Box2 SpriteAabbFunc(in SpriteComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
var worldRot = value.Owner.Transform.WorldRotation;
var (worldPos, worldRot) = value.Owner.Transform.GetWorldPositionRotation();
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
var tree = RenderingTreeSystem.GetRenderTree(value.Owner);
@@ -50,7 +49,6 @@ namespace Robust.Client.GameObjects
internal static Box2 LightAabbFunc(PointLightComponent value, Vector2? worldPos = null)
{
// Lights are circles so don't need entity's rotation
worldPos ??= value.Owner.Transform.WorldPosition;
var tree = RenderingTreeSystem.GetRenderTree(value.Owner);
var boxSize = value.Radius * 2;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -7,9 +7,9 @@ namespace Robust.Client.GameObjects
[UsedImplicitly]
internal sealed class AppearanceSystem : EntitySystem
{
private readonly Queue<AppearanceComponent> _queuedUpdates = new();
private readonly Queue<ClientAppearanceComponent> _queuedUpdates = new();
public void EnqueueUpdate(AppearanceComponent component)
public void EnqueueUpdate(ClientAppearanceComponent component)
{
_queuedUpdates.Enqueue(component);
}

View File

@@ -440,9 +440,6 @@ namespace Robust.Client.GameObjects
}
}
/// <inheritdoc />
public int DefaultSoundRange => 25;
/// <inheritdoc />
public int OcclusionCollisionMask { get; set; }

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
SubscribeNetworkEvent<EffectSystemMessage>(CreateEffect);
SubscribeLocalEvent<EffectSystemMessage>(CreateEffect);
var overlay = new EffectOverlay(this, prototypeManager, _mapManager, _playerManager, _entityManager);
var overlay = new EffectOverlay(this, prototypeManager, _playerManager, _entityManager);
overlayManager.AddOverlay(overlay);
}
@@ -335,14 +335,12 @@ namespace Robust.Client.GameObjects
private readonly ShaderInstance _unshadedShader;
private readonly EffectSystem _owner;
private readonly IMapManager _mapManager;
private readonly IEntityManager _entityManager;
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager)
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IPlayerManager playerMan, IEntityManager entityManager)
{
_owner = owner;
_unshadedShader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
_mapManager = mapMan;
_playerManager = playerMan;
_entityManager = entityManager;
}
@@ -371,13 +369,18 @@ namespace Robust.Client.GameObjects
currentShader = newShader;
}
// TODO: Should be doing matrix transformations
var effectSprite = effect.EffectSprite;
var effectOrigin = effect.AttachedEntity?.Transform.MapPosition.Position + effect.AttachedOffset ??
effect.Coordinates.ToMapPos(_entityManager);
var coordinates =
(effect.AttachedEntity?.Transform.Coordinates ?? effect.Coordinates)
.Offset(effect.AttachedOffset);
var rotation = _entityManager.GetComponent<TransformComponent>(coordinates.EntityId).WorldRotation;
var effectOrigin = coordinates.ToMapPos(_entityManager);
var effectArea = Box2.CenteredAround(effectOrigin, effect.Size);
var rotatedBox = new Box2Rotated(effectArea, effect.Rotation, effectOrigin);
var rotatedBox = new Box2Rotated(effectArea, effect.Rotation - rotation, effectOrigin);
worldHandle.DrawTextureRect(effectSprite, rotatedBox, ToColor(effect.Color));
}

View File

@@ -1,9 +1,12 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -61,23 +64,40 @@ namespace Robust.Client.GameObjects
{
var currentMap = _eyeManager.CurrentMap;
var viewport = _eyeManager.GetWorldViewport();
var worldHandle = args.WorldHandle;
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
{
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
if (!_entityManager.TryGetComponent<PhysicsComponent>(gridEnt.Uid, out var body)) continue;
var gridInternal = (IMapGridInternal)grid;
var worldMatrix = _entityManager.GetComponent<TransformComponent>(gridEnt.Uid).WorldMatrix;
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
var transform = body.GetTransform();
gridInternal.GetMapChunks(viewport, out var chunkEnumerator);
foreach (var fixture in body.Fixtures)
while (chunkEnumerator.MoveNext(out var chunk))
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
foreach (var fixture in chunk.Fixtures)
{
var aabb = fixture.Shape.ComputeAABB(transform, i);
var poly = (PolygonShape) fixture.Shape;
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.2f));
args.WorldHandle.DrawRect(aabb, Color.Red.WithAlpha(0.5f), false);
var verts = new Vector2[poly.Vertices.Length];
for (var i = 0; i < poly.Vertices.Length; i++)
{
verts[i] = Transform.Mul(transform, poly.Vertices[i]);
}
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green.WithAlpha(0.2f));
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
var aabb = fixture.Shape.ComputeAABB(transform, i);
args.WorldHandle.DrawRect(aabb, Color.Red.WithAlpha(0.5f), false);
}
}
}
}

View File

@@ -247,7 +247,7 @@ namespace Robust.Client.GameObjects
internal static RenderingTreeComponent? GetRenderTree(IEntity entity)
{
if (entity.Transform.MapID == MapId.Nullspace ||
if (entity.Deleted || entity.Transform.MapID == MapId.Nullspace ||
entity.HasComponent<RenderingTreeComponent>()) return null;
var parent = entity.Transform.Parent?.Owner;
@@ -265,7 +265,7 @@ namespace Robust.Client.GameObjects
private bool IsVisible(SpriteComponent component)
{
return component.Visible && !component.ContainerOccluded;
return component.Visible && !component.ContainerOccluded && !component.Deleted;
}
public override void FrameUpdate(float frameTime)
@@ -312,7 +312,7 @@ namespace Robust.Client.GameObjects
{
light.TreeUpdateQueued = false;
if (!light.Enabled || light.ContainerOccluded)
if (light.Deleted || !light.Enabled || light.ContainerOccluded)
{
ClearLight(light);
continue;

View File

@@ -18,6 +18,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
@@ -39,6 +40,8 @@ namespace Robust.Client.GameStates
_pendingSystemMessages
= new();
private readonly Dictionary<EntityUid, MapId> _hiddenEntities = new();
private uint _metaCompNetId;
[Dependency] private readonly IComponentFactory _compFactory = default!;
@@ -379,7 +382,6 @@ namespace Robust.Client.GameStates
var outputData = new Dictionary<EntityUid, Dictionary<uint, ComponentState>>();
Debug.Assert(_players.LocalPlayer != null, "_players.LocalPlayer != null");
var player = _players.LocalPlayer.Session;
var bus = _entityManager.EventBus;
@@ -390,7 +392,7 @@ namespace Robust.Client.GameStates
foreach (var (netId, component) in _entityManager.GetNetComponents(createdEntity))
{
var state = _entityManager.GetComponentState(bus, component, player);
var state = _entityManager.GetComponentState(bus, component);
if(state.GetType() == typeof(ComponentState))
continue;
@@ -426,16 +428,22 @@ namespace Robust.Client.GameStates
ReadOnlySpan<EntityState> nextEntStates)
{
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
var toInitialize = new List<Entity>();
var toInitialize = new List<IEntity>();
var created = new List<EntityUid>();
var toHide = new List<EntityUid>();
var toShow = new List<EntityUid>();
foreach (var es in curEntStates)
{
EntityUid uid;
//Known entities
if (_entities.TryGetEntity(es.Uid, out var entity))
{
// Logger.Debug($"[{IGameTiming.TickStampStatic}] MOD {es.Uid}");
toApply.Add(entity, (es, null));
if(_hiddenEntities.ContainsKey(es.Uid))
toShow.Add(es.Uid);
uid = es.Uid;
}
else //Unknown entities
{
@@ -445,11 +453,14 @@ namespace Robust.Client.GameStates
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
}
// Logger.Debug($"[{IGameTiming.TickStampStatic}] CREATE {es.Uid} {metaState.PrototypeId}");
var newEntity = (Entity)_entities.CreateEntity(metaState.PrototypeId, es.Uid);
var newEntity = _entities.CreateEntity(metaState.PrototypeId, es.Uid);
toApply.Add(newEntity, (es, null));
toInitialize.Add(newEntity);
created.Add(newEntity.Uid);
uid = newEntity.Uid;
}
if(es.Hide)
toHide.Add(uid);
}
foreach (var es in nextEntStates)
@@ -471,7 +482,7 @@ namespace Robust.Client.GameStates
foreach (var kvStates in toApply)
{
var ent = kvStates.Key;
var entity = (Entity) ent;
var entity = ent;
HandleEntityState(entity, _entities.EventBus, kvStates.Value.Item1,
kvStates.Value.Item2);
}
@@ -483,7 +494,7 @@ namespace Robust.Client.GameStates
}
#if EXCEPTION_TOLERANCE
HashSet<Entity> brokenEnts = new HashSet<Entity>();
HashSet<IEntity> brokenEnts = new HashSet<IEntity>();
#endif
foreach (var entity in toInitialize)
@@ -523,13 +534,6 @@ namespace Robust.Client.GameStates
#endif
}
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if (brokenEnts.Contains(entity))
continue;
#endif
}
#if EXCEPTION_TOLERANCE
foreach (var entity in brokenEnts)
{
@@ -537,6 +541,21 @@ namespace Robust.Client.GameStates
}
#endif
foreach (var entityUid in toHide)
{
if(_entityManager.HasComponent<MapGridComponent>(entityUid)) continue;
var xform = _entityManager.GetComponent<TransformComponent>(entityUid);
_hiddenEntities.Add(entityUid, xform.MapID);
xform.ChangeMapId(MapId.Nullspace);
}
foreach (var entityUid in toShow)
{
_entityManager.GetComponent<TransformComponent>(entityUid).ChangeMapId(_hiddenEntities[entityUid]);
_hiddenEntities.Remove(entityUid);
}
return created;
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenToolkit.Audio.OpenAL;
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
using OpenToolkit.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
private void _flushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
_openALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
_openALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer((int) handle);
_checkAlError();
}
}
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
private void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}
}

View File

@@ -10,436 +10,20 @@ using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
using OpenToolkit.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Robust.Client.Graphics.Clyde
namespace Robust.Client.Graphics.Audio
{
internal partial class Clyde
internal partial class ClydeAudio
{
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
private ISawmill _openALSawmill = default!;
private void _initializeAudio()
{
_openALSawmill = Logger.GetSawmill("clyde.oal");
_audioOpenDevice();
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
}
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
_openALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
_openALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
_openALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private void _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
_openALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
throw new InvalidOperationException($"Unable to open OpenAL device! {ALC.GetError(ALDevice.Null)}");
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
}
private void _shutdownAudio()
{
foreach (var source in _audioSources.Values.ToArray())
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
foreach (var source in _bufferedAudioSources.Values.ToArray())
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
if (_openALContext != ALContext.Null)
{
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
private void _updateAudio()
{
var eye = _eyeManager.CurrentEye;
var (x, y) = eye.Position.Position;
AL.Listener(ALListener3f.Position, x, y, -5);
var rot2d = eye.Rotation.ToVec();
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var (rotX, rotY) = eye.Rotation.ToVec();
var at = new Vector3(0f, 0f, -1f);
var up = new Vector3(rotY, rotX, 0f);
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
_openALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
_openALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer((int) handle);
_checkAlError();
}
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public void SetAudioAttenuation(int value)
{
var attenuation = (Attenuation) value;
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.Default:
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
_openALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
return audioSource;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
_openALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
private void _checkAlError([CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
_openALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = _readOggVorbis(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = _readWav(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
private void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
private sealed class AudioSource : IClydeAudioSource
{
private int SourceHandle;
private readonly Clyde _master;
private readonly ClydeAudio _master;
private readonly AudioStream _sourceStream;
private int FilterHandle;
#if DEBUG
@@ -450,7 +34,7 @@ namespace Robust.Client.Graphics.Clyde
private bool IsEfxSupported => _master.IsEfxSupported;
public AudioSource(Clyde master, int sourceHandle, AudioStream sourceStream)
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
{
_master = master;
SourceHandle = sourceHandle;
@@ -700,7 +284,7 @@ namespace Robust.Client.Graphics.Clyde
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly Clyde _master;
private readonly ClydeAudio _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
@@ -711,7 +295,7 @@ namespace Robust.Client.Graphics.Clyde
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(Clyde master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
{
_master = master;
SourceHandle = sourceHandle;

View File

@@ -1,9 +1,9 @@
using System;
using System.IO;
namespace Robust.Client.Graphics.Clyde
namespace Robust.Client.Graphics.Audio
{
internal partial class Clyde
internal partial class ClydeAudio
{
private OggVorbisData _readOggVorbis(Stream stream)
{

View File

@@ -3,9 +3,9 @@ using System.IO;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde
namespace Robust.Client.Graphics.Audio
{
internal partial class Clyde
internal partial class ClydeAudio
{
/// <summary>
/// Load up a WAVE file.

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenToolkit.Audio.OpenAL;
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
using OpenToolkit.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
private Thread? _gameThread;
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
return _initializeAudio();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
}
public void Shutdown()
{
_shutdownAudio();
}
private bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
}
}

View File

@@ -0,0 +1,399 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenToolkit.Audio.OpenAL;
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
using OpenToolkit.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
{
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
private ISawmill _openALSawmill = default!;
private bool _initializeAudio()
{
_openALSawmill = Logger.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return false;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
return true;
}
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
_openALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
_openALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
_openALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
_openALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
_openALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
private void _shutdownAudio()
{
foreach (var source in _audioSources.Values.ToArray())
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
foreach (var source in _bufferedAudioSources.Values.ToArray())
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
if (_openALContext != ALContext.Null)
{
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
private void _updateAudio()
{
var eye = _eyeManager.CurrentEye;
var (x, y) = eye.Position.Position;
AL.Listener(ALListener3f.Position, x, y, -5);
var rot2d = eye.Rotation.ToVec();
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var (rotX, rotY) = eye.Rotation.ToVec();
var at = new Vector3(0f, 0f, -1f);
var up = new Vector3(rotY, rotX, 0f);
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
_flushALDisposeQueues();
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public void SetAudioAttenuation(int value)
{
var attenuation = (Attenuation) value;
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.Default:
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
_openALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
return audioSource;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
_openALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
private void _checkAlError([CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
_openALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = _readOggVorbis(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = _readWav(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio's evil twin brother!
/// </summary>
[UsedImplicitly]
internal sealed class ClydeAudioHeadless : IClydeAudio, IClydeAudioInternal
{
public bool InitializePostWindowing()
{
return true;
}
public void FrameProcess(FrameEventArgs eventArgs)
{
}
public void Shutdown()
{
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, channels, name);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
public void SetMasterVolume(float newVolume)
{
// Nada.
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio.AudioSource's evil twin brother!
/// </summary>
internal class DummyAudioSource : IClydeAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public bool IsPlaying => default;
public bool IsLooping { get; set; }
public void Dispose()
{
// Nada.
}
public void StartPlaying()
{
// Nada.
}
public void StopPlaying()
{
// Nada.
}
public bool SetPosition(Vector2 position)
{
return true;
}
public void SetPitch(float pitch)
{
// Nada.
}
public void SetGlobal()
{
// Nada.
}
public void SetVolume(float decibels)
{
// Nada.
}
public void SetVolumeDirect(float scale)
{
// Nada.
}
public void SetMaxDistance(float maxDistance)
{
// Nada.
}
public void SetRolloffFactor(float rolloffFactor)
{
// Nada.
}
public void SetReferenceDistance(float refDistance)
{
// Nada.
}
public void SetOcclusion(float blocks)
{
// Nada.
}
public void SetPlaybackPosition(float seconds)
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio.BufferedAudioSource's evil twin brother!
/// </summary>
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
{
public new static DummyBufferedAudioSource Instance { get; } = new();
public int SampleRate { get; set; } = 0;
public void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
// Nada.
}
public void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
// Nada.
}
public void QueueBuffers(ReadOnlySpan<int> handles)
{
// Nada.
}
public void EmptyBuffers()
{
// Nada.
}
public void GetBuffersProcessed(Span<int> handles)
{
// Nada.
}
public int GetNumberOfBuffersProcessed()
{
return 0;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal sealed class FallbackProxyClydeAudio : ProxyClydeAudio
{
public override bool InitializePostWindowing()
{
// Deliberate lack of base call here (see base implementation for comments as to why there even is a base)
ActualImplementation = new ClydeAudio();
IoCManager.InjectDependencies(ActualImplementation);
if (ActualImplementation.InitializePostWindowing())
return true;
// If we get here, that failed, so use the fallback
ActualImplementation = new ClydeAudioHeadless();
IoCManager.InjectDependencies(ActualImplementation);
return ActualImplementation.InitializePostWindowing();
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal abstract class ProxyClydeAudio : IClydeAudio, IClydeAudioInternal
{
protected IClydeAudioInternal ActualImplementation = default!;
public virtual bool InitializePostWindowing()
{
// This particular implementation exists to be overridden because removing this method causes C# to complain
return ActualImplementation.InitializePostWindowing();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
ActualImplementation.FrameProcess(eventArgs);
}
public void Shutdown()
{
ActualImplementation.Shutdown();
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioOggVorbis(stream, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioWav(stream, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
return ActualImplementation.LoadAudioRaw(samples, channels, sampleRate, name);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return ActualImplementation.CreateAudioSource(stream);
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return ActualImplementation.CreateBufferedAudioSource(buffers, floatAudio);
}
public void SetMasterVolume(float newVolume)
{
ActualImplementation.SetMasterVolume(newVolume);
}
}
}

View File

@@ -15,17 +15,11 @@ namespace Robust.Client.Graphics.Clyde
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
{
try
{
// We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops.
// This is 100x easier than nvidia's documented approach of NvOptimusEnablement,
// and works while developing.
NativeLibrary.Load("nvapi64.dll");
}
catch (Exception)
{
// If this fails whatever.
}
// We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops.
// This is 100x easier than nvidia's documented approach of NvOptimusEnablement,
// and works while developing.
NativeLibrary.TryLoad("nvapi64.dll", out _);
// If this fails whatever.
}
if (OperatingSystem.IsWindows())

View File

@@ -74,7 +74,8 @@ namespace Robust.Client.Graphics.Clyde
case DEventWindowFocus(var args):
OnWindowFocused?.Invoke(args);
break;
case DEventWindowResized(var args):
case DEventWindowResized(var reg, var args):
reg.Resized?.Invoke(args);
OnWindowResized?.Invoke(args);
break;
}
@@ -112,7 +113,7 @@ namespace Robust.Client.Graphics.Clyde
reg.FramebufferSize,
reg.Handle);
_eventDispatchQueue.Enqueue(new DEventWindowResized(eventArgs));
_eventDispatchQueue.Enqueue(new DEventWindowResized(reg, eventArgs));
}
private void SendWindowContentScaleChanged(WindowContentScaleEventArgs ev)
@@ -151,7 +152,7 @@ namespace Robust.Client.Graphics.Clyde
private sealed record DEventWindowClosed(WindowReg Reg, WindowRequestClosedEventArgs Args) : DEventBase;
private sealed record DEventWindowResized(WindowResizedEventArgs Args) : DEventBase;
private sealed record DEventWindowResized(WindowReg Reg, WindowResizedEventArgs Args) : DEventBase;
private sealed record DEventWindowContentScaleChanged(WindowContentScaleEventArgs Args) : DEventBase;

View File

@@ -16,7 +16,6 @@ namespace Robust.Client.Graphics.Clyde
// Advanced GL contexts currently disabled due to lack of testing etc.
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngle))
{
/*
if (_cfg.GetCVar(CVars.DisplayAngleCustomSwapChain))
{
_sawmillOgl.Debug("Trying custom swap chain ANGLE.");
@@ -31,7 +30,6 @@ namespace Robust.Client.Graphics.Clyde
return;
}
}
*/
/*
if (_cfg.GetCVar(CVars.DisplayEgl))

View File

@@ -927,10 +927,10 @@ namespace Robust.Client.Graphics.Clyde
//
// Calculate delta positions from camera.
var (dTlX, dTlY) = eyeTransform.Transform(tl);
var (dTrX, dTrY) = eyeTransform.Transform(tr);
var (dBlX, dBlY) = eyeTransform.Transform(bl);
var (dBrX, dBrY) = eyeTransform.Transform(br);
var dTl = eyeTransform.Transform(tl);
var dTr = eyeTransform.Transform(tr);
var dBl = eyeTransform.Transform(bl);
var dBr = eyeTransform.Transform(br);
// Get which neighbors are occluding.
var no = (occluder.Occluding & OccluderDir.North) != 0;
@@ -939,10 +939,26 @@ namespace Robust.Client.Graphics.Clyde
var wo = (occluder.Occluding & OccluderDir.West) != 0;
// Do visibility tests for occluders (described above).
var tlV = dTlX > 0 && !wo || dTlY < 0 && !no;
var trV = dTrX < 0 && !eo || dTrY < 0 && !no;
var blV = dBlX > 0 && !wo || dBlY > 0 && !so;
var brV = dBrX < 0 && !eo || dBrY > 0 && !so;
bool CheckFaceEyeVis(Vector2 a, Vector2 b)
{
// get normal
var alongNormal = b - a;
var normal = alongNormal.Rotated90DegreesAnticlockwiseWorld.Normalized;
// determine which side of the plane the face is on
// the plane is at the origin of this coordinate system, which is also the eye
// the normal of the plane is that of the face
// therefore, if the dot <= 0, the face is facing the camera
// I don't like this, but rotated occluders started happening
return Vector2.Dot(normal, a) <= 0;
}
var nV = ((!no) && CheckFaceEyeVis(dTl, dTr));
var sV = ((!so) && CheckFaceEyeVis(dBr, dBl));
var eV = ((!eo) && CheckFaceEyeVis(dTr, dBr));
var wV = ((!wo) && CheckFaceEyeVis(dBl, dTl));
var tlV = nV || wV;
var trV = nV || eV;
var blV = sV || wV;
var brV = sV || eV;
// Handle faces, rules described above.
// Note that "from above" it should be clockwise.

View File

@@ -155,10 +155,6 @@ namespace Robust.Client.Graphics.Clyde
{
isActuallySrgb = loadParams.Srgb;
}
else if (pixelType == typeof(Bgra32))
{
isActuallySrgb = loadParams.Srgb;
}
else if (pixelType == typeof(A8))
{
DebugTools.Assert(_hasGLTextureSwizzle);
@@ -264,7 +260,6 @@ namespace Robust.Client.Graphics.Clyde
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
// Shaders are expected to compensate for this
Rgba32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Rgba, PT.UnsignedByte),
Bgra32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Bgra, PT.UnsignedByte),
A8 or L8 => (PIF.R8, PF.Red, PT.UnsignedByte),
_ => throw new NotSupportedException("Unsupported pixel type."),
};
@@ -445,7 +440,7 @@ namespace Robust.Client.Graphics.Clyde
{
return default(T) switch
{
Rgba32 or Bgra32 => TexturePixelType.Rgba32,
Rgba32 => TexturePixelType.Rgba32,
L8 => TexturePixelType.L8,
A8 => TexturePixelType.A8,
_ => throw new NotSupportedException("Unsupported pixel type."),

View File

@@ -218,7 +218,8 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
InitOpenGL();
if (!_earlyGLInit)
InitOpenGL();
_sawmillOgl.Debug("Setting viewport and rendering splash...");
@@ -458,6 +459,7 @@ namespace Robust.Client.Graphics.Clyde
public RenderWindow RenderTarget = default!;
public Action<WindowRequestClosedEventArgs>? RequestClosed;
public Action<WindowDestroyedEventArgs>? Closed;
public Action<WindowResizedEventArgs>? Resized;
}
private sealed class WindowHandle : IClydeWindowInternal
@@ -523,6 +525,12 @@ namespace Robust.Client.Graphics.Clyde
remove => Reg.Closed -= value;
}
public event Action<WindowResizedEventArgs>? Resized
{
add => Reg.Resized += value;
remove => Reg.Resized -= value;
}
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
}

View File

@@ -22,7 +22,7 @@ namespace Robust.Client.Graphics.Clyde
/// <summary>
/// Responsible for most things rendering on OpenGL mode.
/// </summary>
internal sealed partial class Clyde : IClydeInternal, IClydeAudio, IPostInjectInit
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit
{
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -66,6 +66,7 @@ namespace Robust.Client.Graphics.Clyde
private ISawmill _sawmillOgl = default!;
private IBindingsContext _glBindingsContext = default!;
private bool _earlyGLInit;
public Clyde()
{
@@ -97,7 +98,6 @@ namespace Robust.Client.Graphics.Clyde
if (!InitMainWindowAndRenderer())
return false;
_initializeAudio();
return true;
}
@@ -115,8 +115,6 @@ namespace Robust.Client.Graphics.Clyde
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
_windowing?.FlushDispose();
FlushShaderInstanceDispose();
FlushRenderTargetDispose();
@@ -509,7 +507,6 @@ namespace Robust.Client.Graphics.Clyde
{
_glContext?.Shutdown();
ShutdownWindowing();
_shutdownAudio();
}
private bool IsMainThread()

View File

@@ -19,7 +19,7 @@ namespace Robust.Client.Graphics.Clyde
/// Hey look, it's Clyde's evil twin brother!
/// </summary>
[UsedImplicitly]
internal sealed class ClydeHeadless : IClydeInternal, IClydeAudio
internal sealed class ClydeHeadless : IClydeInternal
{
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
public IClydeWindow MainWindow { get; }
@@ -236,34 +236,6 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, channels, name);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
public Task<string> GetText()
{
return Task.FromResult(string.Empty);
@@ -274,11 +246,6 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void SetMasterVolume(float newVolume)
{
// Nada.
}
private class DummyCursor : ICursor
{
public void Dispose()
@@ -627,6 +594,7 @@ namespace Robust.Client.Graphics.Clyde
public bool DisposeOnClose { get; set; }
public event Action<WindowRequestClosedEventArgs>? RequestClosed { add { } remove { } }
public event Action<WindowDestroyedEventArgs>? Destroyed;
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
public void MaximizeOnMonitor(IClydeMonitor monitor)
{

View File

@@ -1,6 +1,4 @@
// Commented out because I can't be bothered to figure out trimming for TerraFX.
/*
using System;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
@@ -10,13 +8,18 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TerraFX.Interop;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static Robust.Client.Graphics.Clyde.Egl;
using static TerraFX.Interop.D3D_DRIVER_TYPE;
using static TerraFX.Interop.D3D_FEATURE_LEVEL;
using static TerraFX.Interop.DXGI_FORMAT;
using static TerraFX.Interop.DXGI_SWAP_EFFECT;
using static TerraFX.Interop.Windows;
using static TerraFX.Interop.DirectX.D3D_DRIVER_TYPE;
using static TerraFX.Interop.DirectX.D3D_FEATURE_LEVEL;
using static TerraFX.Interop.DirectX.DXGI_FORMAT;
using static TerraFX.Interop.DirectX.DXGI_SWAP_EFFECT;
using static TerraFX.Interop.Windows.Windows;
using static TerraFX.Interop.DirectX.DirectX;
using static TerraFX.Interop.DirectX.D3D11;
using static TerraFX.Interop.DirectX.DXGI;
using GL = OpenToolkit.Graphics.OpenGL4.GL;
namespace Robust.Client.Graphics.Clyde
@@ -77,7 +80,7 @@ namespace Robust.Client.Graphics.Clyde
};
_windowData[reg.Id] = data;
var hWnd = Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
var hWnd = (HWND) Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
// todo: exception management.
CreateSwapChain1(hWnd, data);
@@ -98,7 +101,8 @@ namespace Robust.Client.Graphics.Clyde
{
if (data.EglBackbuffer != null)
{
eglMakeCurrent(_eglDisplay, null, null, null);
if (data.Reg.IsMainWindow)
eglMakeCurrent(_eglDisplay, null, null, null);
eglDestroySurface(_eglDisplay, data.EglBackbuffer);
data.EglBackbuffer = null;
@@ -115,8 +119,7 @@ namespace Robust.Client.Graphics.Clyde
fixed (ID3D11Texture2D** texPtr = &data.Backbuffer)
{
var iid = IID_ID3D11Texture2D;
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, &iid, (void**) texPtr));
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, __uuidof<ID3D11Texture2D>(), (void**) texPtr));
}
var attributes = stackalloc int[]
@@ -134,7 +137,7 @@ namespace Robust.Client.Graphics.Clyde
attributes);
}
private void CreateSwapChain1(nint hWnd, WindowData data)
private void CreateSwapChain1(HWND hWnd, WindowData data)
{
var desc = new DXGI_SWAP_CHAIN_DESC
{
@@ -199,6 +202,7 @@ namespace Robust.Client.Graphics.Clyde
// and so that we can know _hasGLSrgb in window creation.
eglMakeCurrent(_eglDisplay, null, null, _eglContext);
Clyde.InitOpenGL();
Clyde._earlyGLInit = true;
}
private void TryInitializeCore()
@@ -294,11 +298,9 @@ namespace Robust.Client.Graphics.Clyde
try
{
var iid = IID_IDXGIFactory1;
fixed (IDXGIFactory1** ptr = &_factory)
{
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(&iid, (void**) ptr));
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) ptr));
}
// Try to find the correct adapter if specified.
@@ -317,6 +319,38 @@ namespace Robust.Client.Graphics.Clyde
Logger.DebugS("clyde.ogl.angle", $"Found display adapter with name: {adapterName}");
}
#pragma warning disable CA1416
IDXGIFactory6* factory6;
if (_adapter == null && _factory->QueryInterface(__uuidof<IDXGIFactory6>(), (void**) &factory6) == 0)
{
var gpuPref = (DXGI_GPU_PREFERENCE) Clyde._cfg.GetCVar(CVars.DisplayGpuPreference);
IDXGIAdapter1* adapter;
for (var adapterIndex = 0u;
factory6->EnumAdapterByGpuPreference(
adapterIndex,
gpuPref,
__uuidof<IDXGIAdapter1>(),
(void**)&adapter) != DXGI_ERROR_NOT_FOUND;
adapterIndex++)
{
/*
DXGI_ADAPTER_DESC1 aDesc;
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&aDesc));
var aDescName = new ReadOnlySpan<char>(aDesc.Description, 128);
Logger.DebugS("clyde.ogl.angle", aDescName.ToString());
adapter->Release();
*/
_adapter = adapter;
break;
}
factory6->Release();
}
#pragma warning restore CA1416
Span<D3D_FEATURE_LEVEL> featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
// 11_0 can do GLES3
@@ -335,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
ThrowIfFailed("D3D11CreateDevice", D3D11CreateDevice(
(IDXGIAdapter*) _adapter,
_adapter == null ? D3D_DRIVER_TYPE_HARDWARE : D3D_DRIVER_TYPE_UNKNOWN,
IntPtr.Zero,
HMODULE.NULL,
0,
fl,
(uint) featureLevels.Length,
@@ -348,13 +382,11 @@ namespace Robust.Client.Graphics.Clyde
// Get adapter from the device.
iid = IID_IDXGIDevice1;
ThrowIfFailed("QueryInterface", _device->QueryInterface(&iid, (void**) &dxgiDevice));
ThrowIfFailed("QueryInterface", _device->QueryInterface(__uuidof<IDXGIDevice1>(), (void**) &dxgiDevice));
fixed (IDXGIAdapter1** ptrAdapter = &_adapter)
{
iid = IID_IDXGIAdapter1;
ThrowIfFailed("GetParent", dxgiDevice->GetParent(&iid, (void**) ptrAdapter));
ThrowIfFailed("GetParent", dxgiDevice->GetParent(__uuidof<IDXGIAdapter1>(), (void**) ptrAdapter));
}
_deviceFl = _device->GetFeatureLevel();
@@ -527,4 +559,3 @@ namespace Robust.Client.Graphics.Clyde
}
}
}
*/

View File

@@ -1,28 +1,32 @@
/*
using System;
using System;
using System.Runtime.InteropServices;
using Robust.Shared.Console;
using Robust.Shared.Utility;
using TerraFX.Interop;
using static TerraFX.Interop.D3D_DRIVER_TYPE;
using static TerraFX.Interop.D3D_FEATURE_LEVEL;
using static TerraFX.Interop.DXGI_MEMORY_SEGMENT_GROUP;
using static TerraFX.Interop.DXGI_SWAP_EFFECT;
using static TerraFX.Interop.Windows;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.DirectX.DXGI_MEMORY_SEGMENT_GROUP;
using static TerraFX.Interop.Windows.Windows;
using static TerraFX.Interop.DirectX.DirectX;
using static TerraFX.Interop.DirectX.DXGI;
namespace Robust.Client.Graphics.Clyde
{
public sealed class VramCommand : IConsoleCommand
{
public string Command => "vram";
public string Description => "Checks vram";
public string Description => "Displays video memory usage statics by the game.";
public string Help => "Usage: vram";
public unsafe void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!OperatingSystem.IsWindows())
{
shell.WriteError("This command is only supported on Windows.");
return;
}
IDXGIFactory1* dxgiFactory;
var iid = IID_IDXGIFactory1;
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(&iid, (void**) &dxgiFactory));
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) &dxgiFactory));
uint idx = 0;
IDXGIAdapter* adapter;
@@ -30,8 +34,8 @@ namespace Robust.Client.Graphics.Clyde
{
DXGI_ADAPTER_DESC2 desc;
IDXGIAdapter3* adapter3;
iid = IID_IDXGIAdapter3;
adapter->QueryInterface(&iid, (void**) &adapter3);
adapter->QueryInterface(__uuidof<IDXGIAdapter3>(), (void**) &adapter3);
adapter->Release();
ThrowIfFailed("GetDesc", adapter3->GetDesc2(&desc));
var descString = new ReadOnlySpan<char>(desc.Description, 128).TrimEnd('\0');
@@ -46,6 +50,8 @@ namespace Robust.Client.Graphics.Clyde
shell.WriteLine($"Usage (non local): {ByteHelpers.FormatBytes((long) memInfo.CurrentUsage)}");
idx += 1;
adapter3->Release();
}
}
@@ -58,4 +64,3 @@ namespace Robust.Client.Graphics.Clyde
}
}
}
*/

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared;
using System.Threading;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
@@ -8,6 +9,8 @@ using GlfwKey = OpenToolkit.GraphicsLibraryFramework.Keys;
using GlfwButton = OpenToolkit.GraphicsLibraryFramework.MouseButton;
using static Robust.Client.Input.Mouse;
using static Robust.Client.Input.Keyboard;
using Robust.Shared.IoC;
using Robust.Shared.Configuration;
namespace Robust.Client.Graphics.Clyde
{
@@ -22,6 +25,7 @@ namespace Robust.Client.Graphics.Clyde
private void InitKeyMap()
{
_printableKeyNameMap.Clear();
// From GLFW's source code: this is the actual list of "printable" keys
// that GetKeyName returns something for.
CacheKey(Keys.KeyPadEqual);
@@ -41,8 +45,18 @@ namespace Robust.Client.Graphics.Clyde
if (rKey == Key.Unknown)
return;
var name = GLFW.GetKeyName(key, 0);
if (name != null)
string name;
if (!_clyde._cfg.GetCVar(CVars.DisplayUSQWERTYHotkeys))
{
name = GLFW.GetKeyName(key, 0);
}
else
{
name = key.ToString();
}
if (!string.IsNullOrEmpty(name))
_printableKeyNameMap.Add(rKey, name);
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@@ -268,7 +268,7 @@ namespace Robust.Client.Graphics.Clyde
}
var (desc, errCode) = errorResult!.Value;
return (null, $"[{errCode}]: {desc}");
return (null, (string)$"[{errCode}]: {desc}");
}
public void WindowDestroy(WindowReg window)

View File

@@ -1,6 +1,7 @@
using System;
using System.Runtime.Serialization;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
@@ -16,6 +17,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private readonly Clyde _clyde;
@@ -39,6 +41,7 @@ namespace Robust.Client.Graphics.Clyde
#if DEBUG
_cfg.OnValueChanged(CVars.DisplayWin32Experience, b => _win32Experience = b, true);
#endif
_cfg.OnValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap);
InitChannels();
@@ -60,6 +63,7 @@ namespace Robust.Client.Graphics.Clyde
if (_glfwInitialized)
{
_sawmill.Debug("Terminating GLFW.");
_cfg.UnsubValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap);
GLFW.Terminate();
}
}
@@ -69,6 +73,12 @@ namespace Robust.Client.Graphics.Clyde
// Not currently used
}
private void ReInitKeyMap(bool onValueChanged)
{
InitKeyMap();
_inputManager.InputModeChanged();
}
private bool InitGlfw()
{
StoreCallbacks();

View File

@@ -52,6 +52,11 @@ namespace Robust.Client.Graphics
public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad,
Color? modulate = null, UIBox2? subRegion = null);
private Box2 GetQuad(Texture texture, Vector2 position)
{
return Box2.FromDimensions(position, texture.Size / (float)Ppm);
}
/// <summary>
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
@@ -67,7 +72,28 @@ namespace Robust.Client.Graphics
{
CheckDisposed();
DrawTextureRect(texture, Box2.FromDimensions(position, texture.Size / (float) Ppm), modulate);
DrawTextureRect(texture, GetQuad(texture, position), modulate);
}
/// <summary>
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="position">The coordinates of the quad in object space (or world if the transform is identity.).</param>
/// <param name="angle">The angle of the quad in object space.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <remarks>
/// The sprite will have it's local dimensions calculated so that it has <see cref="EyeManager.PixelsPerMeter"/> texels per meter in the world.
/// </remarks>
public void DrawTexture(Texture texture, Vector2 position, Angle angle, Color? modulate = null)
{
CheckDisposed();
var quad = GetQuad(texture, position);
DrawTextureRect(texture, new Box2Rotated(quad, angle, quad.Center), modulate);
}
/// <summary>

View File

@@ -0,0 +1,16 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
internal interface IClydeAudioInternal : IClydeAudio
{
bool InitializePostWindowing();
void FrameProcess(FrameEventArgs eventArgs);
void Shutdown();
}
}

View File

@@ -35,6 +35,12 @@ namespace Robust.Client.Graphics
/// This means the window must not be used anymore (it is disposed).
/// </summary>
event Action<WindowDestroyedEventArgs> Destroyed;
/// <summary>
/// Raised when the window has been definitively closed.
/// This means the window must not be used anymore (it is disposed).
/// </summary>
event Action<WindowResizedEventArgs> Resized;
}
public interface IClydeWindowInternal : IClydeWindow

View File

@@ -111,6 +111,7 @@ namespace Robust.Client.Input
event Action<IKeyBinding> OnKeyBindingAdded;
event Action<IKeyBinding> OnKeyBindingRemoved;
event Action OnInputModeChanged;
/// <summary>
/// Gets all the keybinds bound to a specific function.
@@ -131,5 +132,7 @@ namespace Robust.Client.Input
bool IsKeyFunctionModified(BoundKeyFunction function);
bool IsKeyDown(Keyboard.Key key);
void InputModeChanged();
}
}

View File

@@ -6,6 +6,7 @@ namespace Robust.Client.Input
{
BoundKeyState State { get; }
BoundKeyFunction Function { get; }
string FunctionCommand { get; }
KeyBindingType BindingType { get; }
Keyboard.Key BaseKey { get; }

View File

@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Input;
@@ -43,6 +44,7 @@ namespace Robust.Client.Input
[Dependency] private readonly IResourceManager _resourceMan = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
[Dependency] private readonly IConsoleHost _console = default!;
private bool _currentlyFindingViewport;
@@ -76,7 +78,7 @@ namespace Robust.Client.Input
public IEnumerable<BoundKeyFunction> DownKeyFunctions => _bindings
.Where(x => x.State == BoundKeyState.Down)
.Select(x => x.Function)
.Select(x => (BoundKeyFunction)x.Function)
.ToList();
public virtual string GetKeyName(Key key)
@@ -98,6 +100,7 @@ namespace Robust.Client.Input
public event KeyEventAction? FirstChanceOnKeyEvent;
public event Action<IKeyBinding>? OnKeyBindingAdded;
public event Action<IKeyBinding>? OnKeyBindingRemoved;
public event Action? OnInputModeChanged;
/// <inheritdoc />
public void Initialize()
@@ -131,7 +134,7 @@ namespace Robust.Client.Input
.SelectMany(p => p)
.Select(p => new KeyBindingRegistration
{
Function = p.Function,
Function = p.Function.FunctionName,
BaseKey = p.BaseKey,
Mod1 = p.Mod1,
Mod2 = p.Mod2,
@@ -213,7 +216,7 @@ namespace Robust.Client.Input
foreach (var binding in _bindings)
{
// check if our binding is even in the active context
if (!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
if (binding.BindingType != KeyBindingType.Command && !Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
continue;
if (PackedMatchesPressedState(binding.PackedKeyCombo))
@@ -359,6 +362,12 @@ namespace Robust.Client.Input
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
{
if (binding.BindingType == KeyBindingType.Command && state == BoundKeyState.Down)
{
_console.ExecuteCommand(binding.FunctionCommand);
return true;
}
// christ this crap *is* re-entrant thanks to PlacementManager and
// I honestly have no idea what the best solution here is.
// note from the future: context switches won't cause re-entrancy anymore because InputContextContainer defers context switches
@@ -492,10 +501,10 @@ namespace Robust.Client.Input
foreach (var reg in baseKeyRegs)
{
if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName))
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
{
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", file,
reg.Function.FunctionName);
reg.Function);
continue;
}
@@ -531,6 +540,17 @@ namespace Robust.Client.Input
/// <inheritdoc />
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
{
var binding = new KeyBinding(this, function.FunctionName, bindingType, baseKey, false, false, false,
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
RegisterBinding(binding);
return binding;
}
public IKeyBinding RegisterBinding(string function, KeyBindingType bindingType,
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
{
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false,
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
@@ -542,7 +562,7 @@ namespace Robust.Client.Input
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
{
var binding = new KeyBinding(this, reg.Function, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
var binding = new KeyBinding(this, reg.Function.FunctionName, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
RegisterBinding(binding, markModified);
@@ -569,6 +589,8 @@ namespace Robust.Client.Input
OnKeyBindingRemoved?.Invoke(binding);
}
public void InputModeChanged() => OnInputModeChanged?.Invoke();
private void RegisterBinding(KeyBinding binding, bool markModified = true)
{
// we sort larger combos first so they take priority over smaller (single key) combos,
@@ -684,6 +706,7 @@ namespace Robust.Client.Input
[ViewVariables] public BoundKeyState State { get; set; }
public PackedKeyCombo PackedKeyCombo { get; }
[ViewVariables] public BoundKeyFunction Function { get; }
[ViewVariables] public string FunctionCommand => Function.FunctionName;
[ViewVariables] public KeyBindingType BindingType { get; }
[ViewVariables] public Key BaseKey => PackedKeyCombo.BaseKey;
@@ -711,7 +734,9 @@ namespace Robust.Client.Input
[ViewVariables] public int Priority { get; internal set; }
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
public KeyBinding(
InputManager inputManager,
string function,
KeyBindingType bindingType,
Key baseKey,
bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown,
@@ -771,7 +796,7 @@ namespace Robust.Client.Input
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendFormat("{0}: {1}", Function.FunctionName, BaseKey);
sb.AppendFormat("{0}: {1}", Function, BaseKey);
if (Mod1 != Key.Unknown)
{
sb.AppendFormat("+{0}", Mod1);
@@ -868,6 +893,10 @@ namespace Robust.Client.Input
Unknown = 0,
State,
Toggle,
/// <summary>
/// This keybind does not execute a real key function but instead causes a console command to be executed.
/// </summary>
Command,
}
public enum CommandState : byte
@@ -922,7 +951,7 @@ namespace Robust.Client.Input
var registration = new KeyBindingRegistration
{
Function = new BoundKeyFunction(inputCommand),
Function = inputCommand,
BaseKey = keyId,
Type = keyMode
};

View File

@@ -8,6 +8,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Client</OutputPath>
<NoWarn>NU1701</NoWarn>
<RobustILLink>true</RobustILLink>
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
@@ -22,7 +23,7 @@
<PackageReference Include="OpenToolkit.OpenAL" Version="4.0.0-pre9.1" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" />
<PackageReference Include="Robust.Natives" Version="0.1.0" />
<!--<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-beta1" />-->
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" />
</ItemGroup>
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.8.0" />
@@ -44,10 +45,16 @@
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
</ItemGroup>
<ItemGroup>
<RobustLinkRoots Include="Robust.Client" />
<RobustLinkAssemblies Include="TerraFX.Interop.Windows" />
<RobustLinkAssemblies Include="OpenToolkit.Graphics" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
<PropertyGroup>
<RobustToolsPath>../Tools</RobustToolsPath>
</PropertyGroup>
<Target Name="RobustAfterBuild" AfterTargets="Build" />
<Import Project="..\MSBuild\XamlIL.targets" />
<Import Project="..\MSBuild\Robust.Trimming.targets" />
</Project>

View File

@@ -134,7 +134,7 @@ namespace Robust.Client.UserInterface
return false;
}
if (parent == UserInterfaceManager.RootControl)
if (parent is UIRoot)
{
return true;
}

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.UserInterface.Controls
/// <seealso cref="CheckBox"/>
public abstract class BaseButton : Control
{
private bool _attemptingPress;
private int _attemptingPress;
private bool _beingHovered;
private bool _disabled;
private bool _pressed;
@@ -151,11 +151,11 @@ namespace Robust.Client.UserInterface.Controls
{
return DrawModeEnum.Disabled;
}
else if (Pressed || _attemptingPress)
else if (Pressed || (_attemptingPress > 0 && IsHovered))
{
return DrawModeEnum.Pressed;
}
else if (IsHovered)
else if (IsHovered || _attemptingPress > 0)
{
return DrawModeEnum.Hover;
}
@@ -210,7 +210,8 @@ namespace Robust.Client.UserInterface.Controls
var drawMode = DrawMode;
if (Mode == ActionMode.Release)
{
_attemptingPress = true;
UserInterfaceManager.ControlFocused = this;
_attemptingPress += 1;
}
else
{
@@ -227,7 +228,8 @@ namespace Robust.Client.UserInterface.Controls
}
else
{
_attemptingPress = true;
UserInterfaceManager.ControlFocused = this;
_attemptingPress += 1;
OnPressed?.Invoke(buttonEventArgs);
}
}
@@ -251,7 +253,7 @@ namespace Robust.Client.UserInterface.Controls
OnButtonUp?.Invoke(buttonEventArgs);
var drawMode = DrawMode;
if (Mode == ActionMode.Release && _attemptingPress && HasPoint((args.PointerLocation.Position - GlobalPixelPosition) / UIScale))
if (Mode == ActionMode.Release && _attemptingPress > 0 && HasPoint((args.PointerLocation.Position - GlobalPixelPosition) / UIScale))
{
// Can't un press a radio button directly.
if (Group == null || !Pressed)
@@ -270,7 +272,22 @@ namespace Robust.Client.UserInterface.Controls
}
}
_attemptingPress = false;
if (_attemptingPress > 0)
_attemptingPress -= 1;
if (_attemptingPress <= 0 && UserInterfaceManager.ControlFocused == this)
UserInterfaceManager.ControlFocused = null;
if (drawMode != DrawMode)
{
DrawModeChanged();
}
}
protected internal override void ControlFocusExited()
{
base.ControlFocusExited();
var drawMode = DrawMode;
_attemptingPress = 0;
if (drawMode != DrawMode)
{
DrawModeChanged();

View File

@@ -0,0 +1,136 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
public class Collapsible : BoxContainer
{
public BaseButton? Heading { get; private set; }
public Control? Body { get; private set; }
private bool _initialized = false;
private bool _bodyVisible;
public bool BodyVisible
{
get => _bodyVisible;
set
{
_bodyVisible = value;
if (Heading != null && Body != null)
{
Heading.Pressed = value;
Body.Visible = value;
}
}
}
public Collapsible()
{}
public Collapsible(CollapsibleHeading header, CollapsibleBody body)
{
AddChild(header);
AddChild(body);
Initialize();
}
public Collapsible(string title, CollapsibleBody body)
{
AddChild(new CollapsibleHeading(title));
AddChild(body);
Initialize();
}
protected internal override void Draw(DrawingHandleScreen handle)
{
if (!_initialized) Initialize();
base.Draw(handle);
}
private void Initialize()
{
var enumerator = Children.GetEnumerator();
enumerator.MoveNext();
// downcast
if (enumerator.Current is not BaseButton heading
|| !heading.ToggleMode)
throw new ArgumentException("No toggle button defined in Collapsible, or title is missing");
Heading = heading;
if (!enumerator.MoveNext())
throw new ArgumentException("Not enough children in Collapsible");
Body = enumerator.Current;
BodyVisible = _bodyVisible;
Heading.Pressed = _bodyVisible;
if (enumerator.MoveNext())
throw new ArgumentException("Too many children in Collapsible");
Heading.OnToggled += args => BodyVisible = args.Pressed;
_initialized = true;
}
}
public class CollapsibleHeading : ContainerButton
{
private TextureRect _chevron = new TextureRect
{
StyleClasses = { OptionButton.StyleClassOptionTriangle },
Margin = new Thickness(2, 0),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
public bool ChevronVisible
{
get => _chevron.Visible;
set => _chevron.Visible = value;
}
public Thickness ChevronMargin
{
get => _chevron.Margin;
set => _chevron.Margin = value;
}
private Label _title = new();
public string? Title
{
get => _title.Text;
set => _title.Text = value;
}
public CollapsibleHeading()
{
ToggleMode = true;
var box = new BoxContainer();
AddChild(box);
box.AddChild(_chevron);
_title = new Label();
box.AddChild(_title);
}
public CollapsibleHeading(string title) : this()
{
Title = title;
}
}
public class CollapsibleBody : Container
{
public CollapsibleBody()
{
this.Visible = false;
}
}
}

View File

@@ -129,6 +129,7 @@ namespace Robust.Client.UserInterface.Controls
ClydeWindow = _clyde.CreateWindow(parameters);
ClydeWindow.RequestClosed += OnWindowRequestClosed;
ClydeWindow.Destroyed += OnWindowDestroyed;
ClydeWindow.Resized += OnWindowResized;
_root = UserInterfaceManager.CreateWindowRoot(ClydeWindow);
_root.AddChild(this);
@@ -167,6 +168,11 @@ namespace Robust.Client.UserInterface.Controls
RealClosed();
}
private void OnWindowResized(WindowResizedEventArgs obj)
{
SetSize = obj.NewSize;
}
private void RealClosed()
{
Orphan();

View File

@@ -153,6 +153,9 @@ namespace Robust.Client.UserInterface.CustomControls
this.placementManager.PlacementChanged += OnPlacementCanceled;
this.placementManager.DirectionChanged += OnDirectionChanged;
UpdateDirectionLabel();
OnClose += OnWindowClosed;
SearchBar.GrabKeyboardFocus();
}
@@ -162,7 +165,7 @@ namespace Robust.Client.UserInterface.CustomControls
if (!disposing) return;
if(EraseButton.Pressed)
if (EraseButton.Pressed)
placementManager.Clear();
placementManager.PlacementChanged -= OnPlacementCanceled;
@@ -171,6 +174,7 @@ namespace Robust.Client.UserInterface.CustomControls
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
{
placementManager.Clear();
BuildEntityList(args.Text);
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
}
@@ -196,13 +200,17 @@ namespace Robust.Client.UserInterface.CustomControls
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
placementManager.Clear();
SearchBar.Clear();
BuildEntityList("");
}
private void OnEraseButtonToggled(BaseButton.ButtonToggledEventArgs args)
{
placementManager.Clear();
placementManager.ToggleEraser();
// clearing will toggle the erase button off...
args.Button.Pressed = args.Pressed;
OverrideMenu.Disabled = args.Pressed;
}
@@ -505,6 +513,16 @@ namespace Robust.Client.UserInterface.CustomControls
}
}
private void OnWindowClosed()
{
if (SelectedButton != null)
{
SelectedButton.ActualButton.Pressed = false;
SelectedButton = null;
}
placementManager.Clear();
}
private void OnPlacementCanceled(object? sender, EventArgs e)
{
if (SelectedButton != null)

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Timing;
namespace Robust.Client.UserInterface.CustomControls
{
internal sealed class FpsCounter : Label
public sealed class FpsCounter : Label
{
private readonly IGameTiming _gameTiming;

View File

@@ -18,6 +18,9 @@ namespace Robust.Client.UserInterface.CustomControls
public const string StyleClassWindowHeader = "windowHeader";
public const string StyleClassWindowCloseButton = "windowCloseButton";
private string? _headerClass;
private string? _titleClass;
public SS14Window()
{
RobustXamlLoader.Load(this);
@@ -31,6 +34,42 @@ namespace Robust.Client.UserInterface.CustomControls
XamlChildren = new SS14ContentCollection(this);
}
public string? HeaderClass
{
get => _headerClass;
set
{
if (_headerClass == value)
return;
if (_headerClass != null)
WindowHeader.RemoveStyleClass(_headerClass);
if (value != null)
WindowHeader.AddStyleClass(value);
_headerClass = value;
}
}
public string? TitleClass
{
get => _titleClass;
set
{
if (_titleClass == value)
return;
if (_titleClass != null)
TitleLabel.RemoveStyleClass(_titleClass);
if (value != null)
TitleLabel.AddStyleClass(value);
_titleClass = value;
}
}
public Control Contents { get; private set; }
private const int DRAG_MARGIN_SIZE = 7;

View File

@@ -62,6 +62,8 @@ namespace Robust.Client.UserInterface.CustomControls
_placementManager.PlacementChanged += OnPlacementCanceled;
OnClose += OnWindowClosed;
Title = "Place Tiles";
SearchBar.GrabKeyboardFocus();
@@ -80,6 +82,8 @@ namespace Robust.Client.UserInterface.CustomControls
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
TileList.ClearSelected();
_placementManager.Clear();
SearchBar.Clear();
BuildTileList("");
ClearButton.Disabled = true;
@@ -87,6 +91,8 @@ namespace Robust.Client.UserInterface.CustomControls
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
{
TileList.ClearSelected();
_placementManager.Clear();
BuildTileList(args.Text);
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
}
@@ -120,6 +126,12 @@ namespace Robust.Client.UserInterface.CustomControls
}
}
private void OnWindowClosed()
{
TileList.ClearSelected();
_placementManager.Clear();
}
private void OnPlacementCanceled(object? sender, EventArgs e)
{
_clearingSelections = true;

View File

@@ -32,7 +32,7 @@ namespace Robust.Client.UserInterface
/// (such as by pressing a different mouse button down over a different control) or when the keyup event
/// happens. When focus is lost on a control, it always fires Control.ControlFocusExited.
/// </summary>
Control? ControlFocused { get; }
Control? ControlFocused { get; set; }
ViewportContainer MainViewport { get; }
@@ -53,8 +53,7 @@ namespace Robust.Client.UserInterface
float DefaultUIScale { get; }
/// <summary>
/// The "root" control to which all other controls are parented,
/// potentially indirectly.
/// The root control for the main game window.
/// </summary>
WindowRoot RootControl { get; }

View File

@@ -1,4 +1,4 @@
using Robust.Client.State;
using Robust.Client.State;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
@@ -14,11 +14,20 @@ namespace Robust.Client.UserInterface
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var reflection = IoCManager.Resolve<IReflectionManager>();
var type = reflection.LooseGetType(args[0]);
var types = reflection.GetAllChildren(typeof(State.State));
var stateMan = IoCManager.Resolve<IStateManager>();
foreach (var tryType in types)
{
if (tryType.FullName!.EndsWith(args[0]))
{
var stateMan = IoCManager.Resolve<IStateManager>();
stateMan.RequestStateChange(tryType);
shell.WriteLine($"Switching to scene {tryType.FullName}");
return;
}
}
stateMan.RequestStateChange(type);
shell.WriteError($"No scene child class type ends with {args[0]}");
}
}
}

View File

@@ -54,7 +54,19 @@ namespace Robust.Client.UserInterface
[ViewVariables] public Control? KeyboardFocused { get; private set; }
[ViewVariables] public Control? ControlFocused { get; private set; }
private Control? _controlFocused;
[ViewVariables]
public Control? ControlFocused
{
get => _controlFocused;
set
{
if (_controlFocused == value)
return;
_controlFocused?.ControlFocusExited();
_controlFocused = value;
}
}
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
@@ -348,7 +360,6 @@ namespace Robust.Client.UserInterface
RemoveModal(top);
else
{
ControlFocused?.ControlFocusExited();
ControlFocused = top;
hitData = null;
return false; // prevent anything besides the top modal control from receiving input
@@ -370,7 +381,6 @@ namespace Robust.Client.UserInterface
var (control, rel) = hit.Value;
ControlFocused?.ControlFocusExited();
ControlFocused = control;
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
@@ -384,7 +394,6 @@ namespace Robust.Client.UserInterface
public void HandleCanFocusUp()
{
ControlFocused?.ControlFocusExited();
ControlFocused = null;
}
@@ -646,7 +655,6 @@ namespace Robust.Client.UserInterface
}
if (control != ControlFocused) return;
ControlFocused?.ControlFocusExited();
ControlFocused = null;
}

View File

@@ -80,9 +80,11 @@ namespace Robust.Server
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
[Dependency] private readonly IScriptHost _scriptHost = default!;
[Dependency] private readonly IMetricsManager _metricsManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
[Dependency] private readonly INetConfigurationManager _netCfgMan = default!;
[Dependency] private readonly IServerConsoleHost _consoleHost = default!;
private readonly Stopwatch _uptimeStopwatch = new();
@@ -103,11 +105,12 @@ namespace Robust.Server
/// <inheritdoc />
public string ServerName => _config.GetCVar(CVars.GameHostName);
public bool ContentStart { get; set; }
/// <inheritdoc />
public void Restart()
{
Logger.InfoS("srv", "Restarting Server...");
// FIXME: This explodes very violently.
Cleanup();
Start(Options, _logHandlerFactory);
}
@@ -139,10 +142,6 @@ namespace Robust.Server
public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
Options = options;
var profilePath = Path.Join(Environment.CurrentDirectory, "AAAAAAAA");
ProfileOptimization.SetProfileRoot(profilePath);
ProfileOptimization.StartProfile("AAAAAAAAAA");
_config.Initialize(true);
if (Options.LoadConfigAndUserData)
@@ -337,21 +336,12 @@ namespace Robust.Server
prototypeManager.LoadDirectory(Options.PrototypeDirectory);
prototypeManager.Resync();
IoCManager.Resolve<IServerConsoleHost>().Initialize();
_consoleHost.Initialize();
_entityManager.Startup();
_mapManager.Startup();
IoCManager.Resolve<IEntityLookup>().Startup();
_stateManager.Initialize();
// sometime after content init
{
var reg = _entityManager.ComponentFactory.GetRegistration<TransformComponent>();
if (!reg.NetID.HasValue)
throw new InvalidOperationException("TransformComponent does not have a NetId.");
_stateManager.SetTransformNetId(reg.NetID.Value);
}
_scriptHost.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
@@ -372,6 +362,8 @@ namespace Robust.Server
GC.Collect();
ProgramShared.RunExecCommands(_consoleHost, _commandLineArgs?.ExecCommands);
return false;
}
@@ -423,12 +415,12 @@ namespace Robust.Server
return false;
}
LokiCredentials credentials;
if (string.IsNullOrWhiteSpace(username))
LokiSinkConfiguration cfg = new()
{
credentials = new NoAuthCredentials(address);
}
else
LokiUrl = address
};
if (!string.IsNullOrWhiteSpace(username))
{
if (string.IsNullOrWhiteSpace(password))
{
@@ -436,13 +428,16 @@ namespace Robust.Server
return false;
}
credentials = new BasicAuthCredentials(address, username, password);
cfg.LokiUsername = username;
cfg.LokiPassword = password;
}
cfg.LogLabelProvider = new LogLabelProvider(serverName);
Logger.DebugS("loki", "Loki enabled for server {ServerName} loki address {LokiAddress}.", serverName,
address);
var handler = new LokiLogHandler(serverName, credentials);
var handler = new LokiLogHandler(cfg);
_log.RootSawmill.AddHandler(handler);
return true;
}
@@ -507,8 +502,6 @@ namespace Robust.Server
FinishMainLoop();
}
public bool ContentStart { get; set; }
public void OverrideMainLoop(IGameLoop gameLoop)
{
_mainLoop = gameLoop;
@@ -538,17 +531,18 @@ namespace Robust.Server
}
// called right before main loop returns, do all saving/cleanup in here
private void Cleanup()
public void Cleanup()
{
_modLoader.Shutdown();
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_playerManager.Shutdown();
// shut down networking, kicking all players.
_network.Shutdown($"Server shutting down: {_shutdownReason}");
// shutdown entities
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_entityManager.Cleanup();
if (_config.GetCVar(CVars.LogRuntimeLog))
{
@@ -569,6 +563,8 @@ namespace Robust.Server
{
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
_config.Shutdown();
}
private void Input(FrameEventArgs args)

View File

@@ -4,130 +4,138 @@ using Robust.Shared;
using Robust.Shared.Utility;
using C = System.Console;
namespace Robust.Server
namespace Robust.Server;
internal sealed class CommandLineArgs
{
internal sealed class CommandLineArgs
public MountOptions MountOptions { get; }
public string? ConfigFile { get; }
public string? DataDir { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
public IReadOnlyList<string> ExecCommands { get; set; }
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
{
public MountOptions MountOptions { get; }
public string? ConfigFile { get; }
public string? DataDir { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
parsed = null;
string? configFile = null;
string? dataDir = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
var execCommands = new List<string>();
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
{
parsed = null;
string? configFile = null;
string? dataDir = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
var arg = enumerator.Current;
if (arg == "--config-file")
{
var arg = enumerator.Current;
if (arg == "--config-file")
if (!enumerator.MoveNext())
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing config file.");
return false;
}
configFile = enumerator.Current;
}
else if (arg == "--data-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing data directory.");
return false;
}
dataDir = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
C.WriteLine("Missing config file.");
return false;
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
configFile = enumerator.Current;
}
else if (arg == "--data-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing data directory.");
return false;
}
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions);
return true;
dataDir = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
return false;
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg.StartsWith("+"))
{
execCommands.Add(arg[1..]);
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
}
private static void PrintHelp()
{
C.WriteLine(@"
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions, execCommands);
return true;
}
private static void PrintHelp()
{
C.WriteLine(@"
Usage: Robust.Server [options] [+command [+command]]
Options:
--config-file Path to the config file to read from.
--data-dir Path to the data directory to read/write from/to.
@@ -136,16 +144,25 @@ Options:
--mount-dir Resource directory to mount.
--mount-zip Resource zip to mount.
--help Display this help text and exit.
");
}
private CommandLineArgs(string? configFile, string? dataDir, IReadOnlyCollection<(string, string)> cVars, IReadOnlyCollection<(string, string)> logLevels, MountOptions mountOptions)
{
ConfigFile = configFile;
DataDir = dataDir;
CVars = cVars;
LogLevels = logLevels;
MountOptions = mountOptions;
}
+command: You can pass a set of commands, prefixed by +,
to be executed in the console in order after the game has finished initializing.
");
}
private CommandLineArgs(
string? configFile,
string? dataDir,
IReadOnlyCollection<(string, string)> cVars,
IReadOnlyCollection<(string, string)> logLevels,
MountOptions mountOptions,
IReadOnlyList<string> execCommands)
{
ConfigFile = configFile;
DataDir = dataDir;
CVars = cVars;
LogLevels = logLevels;
MountOptions = mountOptions;
ExecCommands = execCommands;
}
}

View File

@@ -158,7 +158,7 @@ namespace Robust.Server.Console.Commands
var sb = new StringBuilder();
var players = IoCManager.Resolve<IPlayerManager>().GetAllPlayers();
var players = IoCManager.Resolve<IPlayerManager>().ServerSessions;
sb.AppendLine($"{"Player Name",20} {"Status",12} {"Playing Time",14} {"Ping",9} {"IP EndPoint",20}");
sb.AppendLine("-------------------------------------------------------------------------------");
@@ -188,7 +188,7 @@ namespace Robust.Server.Console.Commands
if (args.Length < 1)
{
var player = shell.Player as IPlayerSession;
var toKickPlayer = player ?? players.GetAllPlayers().FirstOrDefault();
var toKickPlayer = player ?? players.ServerSessions.FirstOrDefault();
if (toKickPlayer == null)
{
shell.WriteLine("You need to provide a player to kick.");

View File

@@ -0,0 +1,98 @@
using System;
using Robust.Server.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
namespace Robust.Server.Console.Commands;
public sealed class ScaleCommand : IConsoleCommand
{
public string Command => "scale";
public string Description => "Increases or decreases an entity's size naively";
public string Help => $"{Command} <entityUid> <float>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError($"Insufficient number of args supplied: expected 2 and received {args.Length}");
return;
}
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteError($"Unable to find entity {args[0]}");
return;
}
if (!float.TryParse(args[1], out var scale))
{
shell.WriteError($"Invalid scale supplied of {args[0]}");
return;
}
if (scale < 0f)
{
shell.WriteError($"Invalid scale supplied that is negative!");
return;
}
// Event for content to use
// We'll just set engine stuff here
var @event = new ScaleEntityEvent();
var entManager = IoCManager.Resolve<IEntityManager>();
entManager.EventBus.RaiseLocalEvent(uid, ref @event);
if (entManager.TryGetComponent(uid, out SpriteComponent? spriteComponent))
{
spriteComponent.Scale *= scale;
}
if (entManager.TryGetComponent(uid, out FixturesComponent? manager))
{
foreach (var (_, fixture) in manager.Fixtures)
{
// TODO: May be worthwhile to swap to density like box2d? Either way mass is unchanged for now.
switch (fixture.Shape)
{
case EdgeShape edge:
edge.Vertex0 *= scale;
edge.Vertex1 *= scale;
edge.Vertex2 *= scale;
edge.Vertex3 *= scale;
break;
case PhysShapeCircle circle:
circle.Position *= scale;
circle.Radius *= scale;
break;
case PolygonShape poly:
var verts = poly.Vertices;
for (var i = 0; i < verts.Length; i++)
{
verts[i] *= scale;
}
poly.SetVertices(verts);
break;
default:
throw new NotImplementedException();
}
}
EntitySystem.Get<FixtureSystem>().FixtureUpdate(manager);
}
}
public readonly struct ScaleEntityEvent
{
public readonly EntityUid Uid;
public ScaleEntityEvent(EntityUid uid)
{
Uid = uid;
}
}
}

View File

@@ -130,7 +130,7 @@ namespace Robust.Server.Console.Commands
Hard = true
};
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
var broadphase = EntitySystem.Get<FixtureSystem>();
broadphase.CreateFixture(ground, horizontalFixture);
@@ -194,7 +194,7 @@ namespace Robust.Server.Console.Commands
Hard = true
};
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
var broadphase = EntitySystem.Get<FixtureSystem>();
broadphase.CreateFixture(ground, horizontalFixture);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
@@ -258,7 +258,7 @@ namespace Robust.Server.Console.Commands
Hard = true
};
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
var broadphase = EntitySystem.Get<FixtureSystem>();
broadphase.CreateFixture(ground, horizontalFixture);
// Setup boxes
@@ -296,7 +296,7 @@ namespace Robust.Server.Console.Commands
private void CreateTumbler(MapId mapId)
{
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
var broadphaseSystem = EntitySystem.Get<FixtureSystem>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var groundUid = entityManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId)).Uid;

View File

@@ -0,0 +1,88 @@
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Players;
namespace Robust.Server.Console.Commands
{
public sealed class AddViewSubscriberCommand : IConsoleCommand
{
public string Command => "addview";
public string Description => $"Allows you to subscribe to an entity's view for debugging purposes";
public string Help => $"{Command} <entityUid>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var session = shell.Player;
if (session is not IPlayerSession playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;
}
if (args.Length != 1)
{
shell.WriteError($"Only 1 arg valid for {Command}, received {args.Length}");
return;
}
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteError($"Unable to parse {args[0]} as a {nameof(EntityUid)}");
return;
}
var entManager = IoCManager.Resolve<IEntityManager>();
if (!entManager.EntityExists(uid))
{
shell.WriteError($"Unable to find entity {uid}");
return;
}
EntitySystem.Get<ViewSubscriberSystem>().AddViewSubscriber(uid, playerSession);
}
public sealed class RemoveViewSubscriberCommand : IConsoleCommand
{
public string Command => "removeview";
public string Description => $"Allows you to unsubscribe to an entity's view for debugging purposes";
public string Help => $"{Command} <entityUid>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var session = shell.Player;
if (session is not IPlayerSession playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;
}
if (args.Length != 1)
{
shell.WriteError($"Only 1 arg valid for {Command}, received {args.Length}");
return;
}
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteError($"Unable to parse {args[0]} as a {nameof(EntityUid)}");
return;
}
var entManager = IoCManager.Resolve<IEntityManager>();
if (!entManager.EntityExists(uid))
{
shell.WriteError($"Unable to find entity {uid}");
return;
}
EntitySystem.Get<ViewSubscriberSystem>().RemoveViewSubscriber(uid, playerSession);
}
}
}
}

View File

@@ -1,71 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Players;
namespace Robust.Server.GameObjects
{
[ComponentReference(typeof(SharedAppearanceComponent))]
public sealed class AppearanceComponent : SharedAppearanceComponent
{
[ViewVariables]
readonly Dictionary<object, object> data = new();
public override void SetData(string key, object value)
{
if (data.TryGetValue(key, out var existing) && existing.Equals(value))
return;
data[key] = value;
Dirty();
}
public override void SetData(Enum key, object value)
{
if (data.TryGetValue(key, out var existing) && existing.Equals(value))
return;
data[key] = value;
Dirty();
}
public override T GetData<T>(string key)
{
return (T)data[key];
}
public override T GetData<T>(Enum key)
{
return (T)data[key];
}
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
private bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
{
if (this.data.TryGetValue(key, out var dat))
{
data = (T)dat;
return true;
}
data = default!;
return false;
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new AppearanceComponentState(data);
}
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects;
/// <summary>
/// This is the server instance of <see cref="AppearanceComponent"/>.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(AppearanceComponent))]
public sealed class ServerAppearanceComponent : AppearanceComponent { }

View File

@@ -85,7 +85,7 @@ namespace Robust.Server.GameObjects
}
}
public override ComponentState GetComponentState(ICommonSession player)
public override ComponentState GetComponentState()
{
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation, VisibilityMask);
}

View File

@@ -427,7 +427,7 @@ namespace Robust.Server.GameObjects
Dirty();
}
public override ComponentState GetComponentState(ICommonSession player)
public override ComponentState GetComponentState()
{
return new SpriteComponentState(Visible, DrawDepth, Scale, Rotation, Offset, Color,
BaseRSIPath, Layers, RenderOrder);

View File

@@ -15,8 +15,6 @@ namespace Robust.Server.GameObjects
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private const int AudioDistanceRange = 25;
private uint _streamIndex;
private class AudioSourceServer : IPlayingAudioStream
@@ -66,9 +64,6 @@ namespace Robust.Server.GameObjects
return unchecked(_streamIndex++);
}
/// <inheritdoc />
public int DefaultSoundRange => AudioDistanceRange;
/// <inheritdoc />
public int OcclusionCollisionMask { get; set; }
@@ -97,7 +92,7 @@ namespace Robust.Server.GameObjects
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityUid uid, AudioParams? audioParams = null)
{
//TODO: Calculate this from PAS
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? DefaultSoundRange : audioParams.Value.MaxDistance;
if(!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
return null;
@@ -129,7 +124,7 @@ namespace Robust.Server.GameObjects
public IPlayingAudioStream Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
//TODO: Calculate this from PAS
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? DefaultSoundRange : audioParams.Value.MaxDistance;
var id = CacheIdentifier();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
@@ -19,7 +20,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Priority queue sorted by how soon the effect will die, we remove messages from the front of the queue during update until caught up
/// </summary>
private readonly PriorityQueue<EffectSystemMessage> _CurrentEffects = new(new EffectMessageComparer());
private readonly PriorityQueue<EffectSystemMessage> _currentEffects = new(new EffectMessageComparer());
/// <summary>
/// Creates a particle effect and sends it to clients.
@@ -28,11 +29,11 @@ namespace Robust.Server.GameObjects
/// <param name="excludedSession">Session to be excluded for prediction</param>
public void CreateParticle(EffectSystemMessage effect, IPlayerSession? excludedSession = null)
{
_CurrentEffects.Add(effect);
_currentEffects.Add(effect);
//For now we will use this which sends to ALL clients
//TODO: Client bubbling
foreach (var player in _playerManager.GetAllPlayers())
foreach (var player in _playerManager.ServerSessions)
{
if (player.Status != SessionStatus.InGame || player == excludedSession)
continue;
@@ -44,9 +45,9 @@ namespace Robust.Server.GameObjects
public override void Update(float frameTime)
{
//Take elements from front of priority queue until they are old
while (_CurrentEffects.Count != 0 && _CurrentEffects.Peek().DeathTime < _timing.CurTime)
while (_currentEffects.Count != 0 && _currentEffects.Peek().DeathTime < _timing.CurTime)
{
_CurrentEffects.Take();
_currentEffects.Take();
}
}

View File

@@ -26,7 +26,6 @@ namespace Robust.Server.GameObjects
RegisterClass<OccluderComponent>();
RegisterClass<OccluderTreeComponent>();
RegisterClass<SpriteComponent>();
RegisterClass<AppearanceComponent>();
RegisterClass<ServerUserInterfaceComponent>();
RegisterClass<TimerComponent>();
RegisterClass<MapSaveIdComponent>();

View File

@@ -50,24 +50,24 @@ namespace Robust.Server.GameObjects
void IServerEntityManagerInternal.FinishEntityLoad(IEntity entity, IEntityLoadContext? context)
{
LoadEntity((Entity) entity, context);
LoadEntity(entity, context);
}
void IServerEntityManagerInternal.FinishEntityInitialization(IEntity entity)
{
InitializeEntity((Entity) entity);
InitializeEntity(entity);
}
void IServerEntityManagerInternal.FinishEntityStartup(IEntity entity)
{
StartEntity((Entity) entity);
StartEntity(entity);
}
private protected override Entity CreateEntity(string? prototypeName, EntityUid? uid = null)
private protected override IEntity CreateEntity(string? prototypeName, EntityUid? uid = null)
{
var entity = base.CreateEntity(prototypeName, uid);
if (prototypeName != null)
if (!string.IsNullOrWhiteSpace(prototypeName))
{
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
@@ -87,6 +87,13 @@ namespace Robust.Server.GameObjects
return entity;
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
{
TryGetComponent(uid, out ActorComponent? actor);
return base.ToPrettyString(uid) with { Session = actor?.PlayerSession };
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;

View File

@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Maths;
namespace Robust.Server.GameStates;
public struct ChunkIndicesEnumerator
{
private Vector2i _topLeft;
private Vector2i _bottomRight;
private int _x;
private int _y;
public ChunkIndicesEnumerator(Box2 viewBox, float chunkSize)
{
_topLeft = (viewBox.TopLeft / chunkSize).Floored();
_bottomRight = (viewBox.BottomRight / chunkSize).Floored();
_x = _topLeft.X;
_y = _bottomRight.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? chunkIndices)
{
if (_y > _topLeft.Y)
{
_x++;
_y = _bottomRight.Y;
}
if (_x > _bottomRight.X)
{
chunkIndices = null;
return false;
}
chunkIndices = new Vector2i(_x, _y);
_y++;
return true;
}
}

View File

@@ -14,9 +14,5 @@ namespace Robust.Server.GameStates
/// Create and dispatch game states to all connected sessions.
/// </summary>
void SendGameStateUpdate();
bool PvsEnabled { get; set; }
float PvsRange { get; set; }
void SetTransformNetId(ushort netId);
}
}

View File

@@ -0,0 +1,419 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates;
public interface IPVSCollection
{
/// <summary>
/// Processes all previous additions, removals and updates of indices.
/// </summary>
public void Process();
public void AddPlayer(ICommonSession session);
public void AddGrid(GridId gridId);
public void AddMap(MapId mapId);
public void RemovePlayer(ICommonSession session);
public void RemoveGrid(GridId gridId);
public void RemoveMap(MapId mapId);
/// <summary>
/// Remove all deletions up to a <see cref="GameTick"/>.
/// </summary>
/// <param name="tick">The <see cref="GameTick"/> before which all deletions should be removed.</param>
public void CullDeletionHistoryUntil(GameTick tick);
}
public class PVSCollection<TIndex, TElement> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
{
[Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!;
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2i GetChunkIndices(Vector2 coordinates)
{
return (coordinates / PVSSystem.ChunkSize).Floored();
}
/// <summary>
/// A delegate to retrieve elements
/// </summary>
private readonly Func<TIndex, TElement> _getElementDelegate;
/// <summary>
/// Index of which <see cref="TIndex"/> are contained in which mapchunk, indexed by <see cref="Vector2i"/>.
/// </summary>
private readonly Dictionary<MapId, Dictionary<Vector2i, HashSet<TIndex>>> _mapChunkContents = new();
/// <summary>
/// Index of which <see cref="TIndex"/> are contained in which gridchunk, indexed by <see cref="Vector2i"/>.
/// </summary>
private readonly Dictionary<GridId, Dictionary<Vector2i, HashSet<TIndex>>> _gridChunkContents = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent.
/// </summary>
private readonly HashSet<TIndex> _globalOverrides = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent.
/// </summary>
public IReadOnlySet<TIndex> GlobalOverrides => _globalOverrides;
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent to a certain <see cref="ICommonSession"/>.
/// </summary>
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _localOverrides = new();
/// <summary>
/// Which <see cref="TIndex"/> where last seen/sent to a certain <see cref="ICommonSession"/>.
/// </summary>
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _lastSeen = new();
/// <summary>
/// History of deletion-tuples, containing the <see cref="GameTick"/> of the deletion, as well as the <see cref="TIndex"/> of the object which was deleted.
/// </summary>
private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new();
/// <summary>
/// An index containing the <see cref="IndexLocation"/>s of all <see cref="TIndex"/>.
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _indexLocations = new();
/// <summary>
/// Buffer of all locationchanges since the last process call
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _locationChangeBuffer = new();
/// <summary>
/// Buffer of all indexremovals since the last process call
/// </summary>
private readonly Dictionary<TIndex, GameTick> _removalBuffer = new();
public PVSCollection(Func<TIndex, TElement> getElementDelegate)
{
_getElementDelegate = getElementDelegate;
IoCManager.InjectDependencies(this);
}
public void Process()
{
var changedIndices = new HashSet<TIndex>(_locationChangeBuffer.Keys);
var changedChunkLocations = new HashSet<IndexLocation>();
foreach (var (index, tick) in _removalBuffer)
{
//changes dont need to be computed if we are removing the index anyways
if (changedIndices.Remove(index) && !_indexLocations.ContainsKey(index))
{
//this index wasnt added yet, so we can safely just skip the deletion
continue;
}
var location = RemoveIndexInternal(index);
if(location is GridChunkLocation or MapChunkLocation)
changedChunkLocations.Add(location);
_deletionHistory.Add((tick, index));
}
// remove empty chunk-subsets
foreach (var chunkLocation in changedChunkLocations)
{
switch (chunkLocation)
{
case GridChunkLocation gridChunkLocation:
if (_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Count == 0)
_gridChunkContents[gridChunkLocation.GridId].Remove(gridChunkLocation.ChunkIndices);
break;
case MapChunkLocation mapChunkLocation:
if (_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Count == 0)
_mapChunkContents[mapChunkLocation.MapId].Remove(mapChunkLocation.ChunkIndices);
break;
}
}
foreach (var index in changedIndices)
{
RemoveIndexInternal(index);
AddIndexInternal(index, _locationChangeBuffer[index]);
}
_locationChangeBuffer.Clear();
_removalBuffer.Clear();
}
public bool TryGetChunk(MapId mapId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
_mapChunkContents[mapId].TryGetValue(chunkIndices, out indices);
public bool TryGetChunk(GridId gridId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
_gridChunkContents[gridId].TryGetValue(chunkIndices, out indices);
public IReadOnlySet<TIndex> GetElementsForSession(ICommonSession session) => _localOverrides[session];
private void AddIndexInternal(TIndex index, IndexLocation location)
{
switch (location)
{
case GlobalOverride _:
_globalOverrides.Add(index);
break;
case GridChunkLocation gridChunkLocation:
// might be gone due to grid-deletions
if(!_gridChunkContents.ContainsKey(gridChunkLocation.GridId)) return;
if(!_gridChunkContents[gridChunkLocation.GridId].ContainsKey(gridChunkLocation.ChunkIndices))
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices] = new();
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Add(index);
break;
case LocalOverride localOverride:
// might be gone due to disconnects
if(!_localOverrides.ContainsKey(localOverride.Session)) return;
_localOverrides[localOverride.Session].Add(index);
break;
case MapChunkLocation mapChunkLocation:
// might be gone due to map-deletions
if(!_mapChunkContents.ContainsKey(mapChunkLocation.MapId)) return;
if(!_mapChunkContents[mapChunkLocation.MapId].ContainsKey(mapChunkLocation.ChunkIndices))
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices] = new();
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Add(index);
break;
}
// we want this to throw if there is already an entry because if that happens we fucked up somewhere
_indexLocations.Add(index, location);
}
private IndexLocation? RemoveIndexInternal(TIndex index)
{
// the index might be gone due to disconnects/grid-/map-deletions
if (!_indexLocations.TryGetValue(index, out var location))
return null;
// since we can find the index, we can assume the dicts will be there too & dont need to do any checks. gaming.
switch (location)
{
case GlobalOverride _:
_globalOverrides.Remove(index);
break;
case GridChunkLocation gridChunkLocation:
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
break;
case LocalOverride localOverride:
_localOverrides[localOverride.Session].Remove(index);
break;
case MapChunkLocation mapChunkLocation:
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index);
break;
}
_indexLocations.Remove(index);
return location;
}
#region Init Functions
/// <inheritdoc />
public void AddPlayer(ICommonSession session)
{
_localOverrides[session] = new();
_lastSeen[session] = new();
}
/// <inheritdoc />
public void AddGrid(GridId gridId) => _gridChunkContents[gridId] = new();
/// <inheritdoc />
public void AddMap(MapId mapId) => _mapChunkContents[mapId] = new();
#endregion
#region ShutdownFunctions
/// <inheritdoc />
public void RemovePlayer(ICommonSession session)
{
foreach (var index in _localOverrides[session])
{
_indexLocations.Remove(index);
}
_localOverrides.Remove(session);
_lastSeen.Remove(session);
}
/// <inheritdoc />
public void RemoveGrid(GridId gridId)
{
foreach (var (_, indices) in _gridChunkContents[gridId])
{
foreach (var index in indices)
{
_indexLocations.Remove(index);
}
}
_gridChunkContents.Remove(gridId);
}
/// <inheritdoc />
public void RemoveMap(MapId mapId)
{
foreach (var (_, indices) in _mapChunkContents[mapId])
{
foreach (var index in indices)
{
_indexLocations.Remove(index);
}
}
_mapChunkContents.Remove(mapId);
}
#endregion
#region DeletionHistory & RemoveIndex
/// <summary>
/// Registers a deletion of an <see cref="TIndex"/> on a <see cref="GameTick"/>. WARNING: this also clears the index out of the internal cache!!!
/// </summary>
/// <param name="tick">The <see cref="GameTick"/> at which the deletion took place.</param>
/// <param name="index">The <see cref="TIndex"/> of the removed object.</param>
public void RemoveIndex(GameTick tick, TIndex index)
{
_removalBuffer[index] = tick;
}
/// <inheritdoc />
public void CullDeletionHistoryUntil(GameTick tick) => _deletionHistory.RemoveAll(hist => hist.tick < tick);
public List<TIndex> GetDeletedIndices(GameTick fromTick)
{
var list = new List<TIndex>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick >= fromTick) list.Add(id);
}
return list;
}
#endregion
#region UpdateIndex
private bool IsOverride(TIndex index)
{
if (_locationChangeBuffer.TryGetValue(index, out var change) &&
change is GlobalOverride or LocalOverride) return true;
if (_indexLocations.TryGetValue(index, out var indexLoc) &&
indexLoc is GlobalOverride or LocalOverride) return true;
return false;
}
/// <summary>
/// Updates an <see cref="TIndex"/> to be sent to all players at all times.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
return;
RegisterUpdate(index, new GlobalOverride());
}
/// <summary>
/// Updates an <see cref="TIndex"/> to be sent to a specific <see cref="ICommonSession"/> at all times.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="session">The <see cref="ICommonSession"/> receiving the object.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, ICommonSession session, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
return;
RegisterUpdate(index, new LocalOverride(session));
}
/// <summary>
/// Updates an <see cref="TIndex"/> with the location based on the provided <see cref="EntityCoordinates"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="coordinates">The <see cref="EntityCoordinates"/> to use when adding the <see cref="TIndex"/> to the internal cache.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, EntityCoordinates coordinates, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
return;
var gridId = coordinates.GetGridId(_entityManager);
if (gridId != GridId.Invalid)
{
var gridIndices = GetChunkIndices(_mapManager.GetGrid(gridId).LocalToGrid(coordinates));
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
return;
}
var mapId = coordinates.GetMapId(_entityManager);
var mapIndices = GetChunkIndices(coordinates.ToMapPos(_entityManager));
UpdateIndex(index, mapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
/// <summary>
/// Updates an <see cref="TIndex"/> using the provided <see cref="gridId"/> and <see cref="chunkIndices"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="gridId">The id of the grid.</param>
/// <param name="chunkIndices">The indices of the chunk.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, GridId gridId, Vector2i chunkIndices, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
return;
RegisterUpdate(index, new GridChunkLocation(gridId, chunkIndices));
}
/// <summary>
/// Updates an <see cref="TIndex"/> using the provided <see cref="mapId"/> and <see cref="chunkIndices"/>.
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="mapId">The id of the map.</param>
/// <param name="chunkIndices">The indices of the mapchunk.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, MapId mapId, Vector2i chunkIndices, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
return;
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
}
private void RegisterUpdate(TIndex index, IndexLocation location)
{
if(_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation == location) return;
_locationChangeBuffer[index] = location;
}
#endregion
#region IndexLocations
private abstract record IndexLocation;
private record MapChunkLocation(MapId MapId, Vector2i ChunkIndices) : IndexLocation;
private record GridChunkLocation(GridId GridId, Vector2i ChunkIndices) : IndexLocation;
private record GlobalOverride : IndexLocation;
private record LocalOverride(ICommonSession Session) : IndexLocation;
#endregion
}

View File

@@ -0,0 +1,21 @@
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
namespace Robust.Server.GameStates;
public struct PVSEntityPacket
{
public readonly TransformComponent TransformComponent;
public readonly MetaDataComponent MetaDataComponent;
public readonly VisibilityComponent? VisibilityComponent;
public readonly ContainerManagerComponent? ContainerManagerComponent;
public PVSEntityPacket(IEntityManager entityManager, EntityUid uid)
{
TransformComponent = entityManager.GetComponent<TransformComponent>(uid);
MetaDataComponent = entityManager.GetComponent<MetaDataComponent>(uid);
entityManager.TryGetComponent(uid, out VisibilityComponent);
entityManager.TryGetComponent(uid, out ContainerManagerComponent);
}
}

View File

@@ -0,0 +1,8 @@
namespace Robust.Server.GameStates;
public enum PVSEntityVisiblity : byte
{
Entered,
StayedUnchanged,
StayedChanged
}

View File

@@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates
{
/// <summary>
/// Caching for dirty bodies
/// </summary>
internal partial class PVSSystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private const int DirtyBufferSize = 4;
/// <summary>
/// if it's a new entity we need to GetEntityState from tick 0.
/// </summary>
private HashSet<EntityUid>[] _addEntities = new HashSet<EntityUid>[DirtyBufferSize];
private HashSet<EntityUid>[] _dirtyEntities = new HashSet<EntityUid>[DirtyBufferSize];
private int _currentIndex = 1;
private void InitializeDirty()
{
for (var i = 0; i < DirtyBufferSize; i++)
{
_addEntities[i] = new HashSet<EntityUid>(32);
_dirtyEntities[i] = new HashSet<EntityUid>(32);
}
EntityManager.EntityAdded += OnEntityAdd;
SubscribeLocalEvent<EntityDirtyEvent>(OnDirty);
}
private void ShutdownDirty()
{
EntityManager.EntityAdded -= OnEntityAdd;
}
private void OnEntityAdd(object? sender, EntityUid e)
{
DebugTools.Assert(_currentIndex == _gameTiming.CurTick.Value % DirtyBufferSize);
_addEntities[_currentIndex].Add(e);
}
private void OnDirty(ref EntityDirtyEvent ev)
{
if (_addEntities[_currentIndex].Contains(ev.Uid) ||
EntityManager.GetComponent<MetaDataComponent>(ev.Uid).EntityLifeStage < EntityLifeStage.Initialized) return;
_dirtyEntities[_currentIndex].Add(ev.Uid);
}
private void CleanupDirty(IEnumerable<IPlayerSession> sessions)
{
// Just in case we desync somehow
_currentIndex = ((int) _gameTiming.CurTick.Value + 1) % DirtyBufferSize;
_addEntities[_currentIndex].Clear();
_dirtyEntities[_currentIndex].Clear();
}
private bool TryGetTick(GameTick tick, [NotNullWhen(true)] out HashSet<EntityUid>? addEntities, [NotNullWhen(true)] out HashSet<EntityUid>? dirtyEntities)
{
var currentTick = _gameTiming.CurTick;
if (currentTick.Value - tick.Value >= DirtyBufferSize)
{
addEntities = null;
dirtyEntities = null;
return false;
}
var index = tick.Value % DirtyBufferSize;
if (index > _dirtyEntities.Length - 1)
{
addEntities = null;
dirtyEntities = null;
return false;
}
addEntities = _addEntities[index];
dirtyEntities = _dirtyEntities[index];
return true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -15,7 +16,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -42,23 +42,6 @@ namespace Robust.Server.GameStates
private ISawmill _logger = default!;
public bool PvsEnabled
{
get => _configurationManager.GetCVar(CVars.NetPVS);
set => _configurationManager.SetCVar(CVars.NetPVS, value);
}
public float PvsRange
{
get => _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
set => _configurationManager.SetCVar(CVars.NetMaxUpdateRange, value);
}
public void SetTransformNetId(ushort netId)
{
_pvs.SetTransformNetId(netId);
}
public void PostInject()
{
_logger = Logger.GetSawmill("PVS");
@@ -121,15 +104,13 @@ namespace Robust.Server.GameStates
{
DebugTools.Assert(_networkManager.IsServer);
_pvs.ViewSize = PvsRange * 2;
_pvs.CullingEnabled = PvsEnabled;
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
_entityManager.CullDeletionHistory(GameTick.MaxValue);
_pvs.CullDeletionHistory(GameTick.MaxValue);
_mapManager.CullDeletionHistory(GameTick.MaxValue);
_pvs.Cleanup(_playerManager.ServerSessions);
return;
}
@@ -190,7 +171,9 @@ namespace Robust.Server.GameStates
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
Parallel.ForEach(_playerManager.GetAllPlayers(), session =>
_pvs.ProcessCollections();
Parallel.ForEach(_playerManager.ServerSessions, session =>
{
try
{
@@ -202,6 +185,7 @@ namespace Robust.Server.GameStates
}
});
_pvs.Cleanup(_playerManager.ServerSessions);
var oldestAck = new GameTick(oldestAckValue);
// keep the deletion history buffers clean
@@ -213,78 +197,5 @@ namespace Robust.Server.GameStates
_mapManager.CullDeletionHistory(oldestAck);
}
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="entMan">EntityManager that contains the entity.</param>
/// <param name="player">The player to generate this state for.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <returns>New entity State for the given entity.</returns>
internal static EntityState GetEntityState(IServerEntityManager entMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
{
var bus = entMan.EventBus;
var changed = new List<ComponentChange>();
foreach (var (netId, component) in entMan.GetNetComponents(entityUid))
{
DebugTools.Assert(component.Initialized);
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
// i.e. when the client spawns the entity and loads the entity prototype,
// the data it deserializes from the prototype SHOULD be equal
// to what the component state / ComponentChange would send.
// As such, we can avoid sending this data in this case since the client "already has it".
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
{
ComponentState? state = null;
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
state = entMan.GetComponentState(bus, component, player);
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Added(netId, state));
}
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
{
changed.Add(ComponentChange.Changed(netId, entMan.GetComponentState(bus, component, player)));
}
}
foreach (var netId in entMan.GetDeletedComponents(entityUid, fromTick))
{
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray());
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
internal static List<EntityState>? GetAllEntityStates(IServerEntityManager entityMan, ICommonSession player, GameTick fromTick)
{
var stateEntities = new List<EntityState>();
foreach (var entity in entityMan.GetEntities())
{
if (entity.Deleted)
{
continue;
}
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(entityMan, player, entity.Uid, fromTick));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
}
}
}

View File

@@ -13,10 +13,10 @@ namespace Robust.Server.Log
{
private readonly SLogger _sLogger;
public LokiLogHandler(string serverName, LokiCredentials credentials)
public LokiLogHandler(LokiSinkConfiguration configuration)
{
_sLogger = new LoggerConfiguration()
.WriteTo.LokiHttp(credentials, new LogLabelProvider(serverName))
.WriteTo.LokiHttp(() => configuration)
.MinimumLevel.Debug()
.CreateLogger();
}
@@ -37,24 +37,28 @@ namespace Robust.Server.Log
{
_sLogger.Dispose();
}
}
private sealed class LogLabelProvider : ILogLabelProvider
public sealed class LogLabelProvider : ILogLabelProvider
{
private readonly string _serverName;
public LogLabelProvider(string serverName)
{
private readonly string _serverName;
public LogLabelProvider(string serverName)
{
_serverName = serverName;
}
public IList<LokiLabel> GetLabels()
{
return new[]
{
new LokiLabel("App", "Robust.Server"),
new LokiLabel("Server", _serverName),
};
}
_serverName = serverName;
}
public IList<LokiLabel> GetLabels()
{
return new[]
{
new LokiLabel("App", "Robust.Server"),
new LokiLabel("Server", _serverName),
};
}
public IList<string> PropertiesAsLabels => new[] {"level"};
public IList<string> PropertiesToAppend => Array.Empty<string>();
public LokiFormatterStrategy FormatterStrategy => LokiFormatterStrategy.SpecificPropertiesAsLabelsAndRestAppended;
}
}

View File

@@ -51,11 +51,11 @@ namespace Robust.Server.Map
// TODO: Like MapManager injecting this is a PITA so need to work out an easy way to do it.
// Maybe just add like a PostInject method that gets called way later?
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
foreach (var fixture in chunk.Fixtures)
{
broadphaseSystem.DestroyFixture(body, fixture);
fixtureSystem.DestroyFixture(body, fixture);
}
}

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Physics;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -440,19 +441,21 @@ namespace Robust.Server.Maps
{
var entManager = IoCManager.Resolve<IEntityManager>();
var gridFixtures = EntitySystem.Get<GridFixtureSystem>();
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
foreach (var grid in Grids)
{
var gridInternal = (IMapGridInternal) grid;
var body = entManager.EnsureComponent<PhysicsComponent>(entManager.GetEntity(grid.GridEntityId));
var body = entManager.EnsureComponent<PhysicsComponent>(grid.GridEntityId);
body.Broadphase = _mapManager.GetMapEntity(grid.ParentMapId).GetComponent<BroadphaseComponent>();
var fixtures = entManager.EnsureComponent<FixturesComponent>(grid.GridEntityId);
gridFixtures.ProcessGrid(gridInternal);
// Need to go through and double-check we don't have any hanging-on fixtures that
// no longer apply (e.g. due to an update in GridFixtureSystem)
var toRemove = new List<Fixture>();
var toRemove = new RemQueue<Fixture>();
foreach (var fixture in body.Fixtures)
foreach (var (_, fixture) in fixtures.Fixtures)
{
var found = false;
@@ -476,8 +479,10 @@ namespace Robust.Server.Maps
foreach (var fixture in toRemove)
{
broadphaseSystem.DestroyFixture(fixture);
fixtureSystem.DestroyFixture(body, fixture, false, fixtures);
}
fixtureSystem.FixtureUpdate(fixtures, body);
}
}
@@ -499,12 +504,14 @@ namespace Robust.Server.Maps
private void FixMapEntities()
{
var pvs = EntitySystem.Get<PVSSystem>();
foreach (var entity in Entities)
{
if (entity.TryGetComponent(out IMapGridComponent? grid))
{
var castGrid = (MapGrid) grid.Grid;
castGrid.GridEntityId = entity.Uid;
pvs?.EntityPVSCollection.UpdateIndex(entity.Uid);
}
}
}

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