Compare commits

..

175 Commits

Author SHA1 Message Date
Pieter-Jan Briers
d49f27a3bb Version: 174.0.3 2024-08-11 18:04:18 +02:00
Pieter-Jan Briers
7ebd4e39be Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
(cherry picked from commit db8ba83866c523e08e4fba0b80cd954f4f190613)
(cherry picked from commit 5ea7aa07c26a499a2fb9930a09ef8d13f85494c0)
(cherry picked from commit 229b60b6af8c36b5f741de840d37e6cd95d5760d)
2024-08-11 18:04:18 +02:00
Pieter-Jan Briers
ef1fd698a0 Version: 174.0.2 2024-03-10 21:28:11 +01:00
Pieter-Jan Briers
a253c18e01 global.json force .NET 7
(cherry picked from commit 6211cf2e03)
2024-03-10 21:28:04 +01:00
Pieter-Jan Briers
4641634011 Version: 174.0.1 2024-03-10 20:50:30 +01:00
Pieter-Jan Briers
5d93618442 Backport 859f150404
(cherry picked from commit 24d5c26fa6)
(cherry picked from commit 688efac67b634c613539b783a9fb6e679948cd53)
2024-03-10 20:50:30 +01:00
ElectroJr
f5874ea402 Version: 174.0.0 2023-10-28 13:26:49 -04:00
metalgearsloth
b486ef885c Add NextAngle for System.Random (#4522) 2023-10-29 04:22:32 +11:00
metalgearsloth
9d55d77e48 Sprite GetFrame (#4528) 2023-10-29 04:21:52 +11:00
Leon Friedrich
5af3cb969c Move ActorComponent to shared (#4527) 2023-10-29 04:21:09 +11:00
metalgearsloth
429bc806dc Version: 173.1.0 2023-10-28 15:36:26 +11:00
metalgearsloth
81484699a8 Add chain shapes (#4523)
* Add chain shapes

* rar only

* that too

* weh

* a

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: Moony <moony@hellomouse.net>

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: Moony <moony@hellomouse.net>

---------

Co-authored-by: Moony <moony@hellomouse.net>
2023-10-28 15:29:30 +11:00
metalgearsloth
7cad8d5ba3 Version: 173.0.0 2023-10-28 14:02:06 +11:00
Leon Friedrich
3aa04a3c86 Fix grid chunk bugs (#4525)
* Fix grid rendering

* Use TileChangedEvent

* Other empty chunk fixes

* Remove assert

Good ol integration tests at it again, adding invalid components
2023-10-28 13:57:54 +11:00
metalgearsloth
9750b113c8 Version: 172.0.0 2023-10-24 20:22:31 +11:00
Leon Friedrich
5a6c4220fc IPlayerManager refactor (#4518) 2023-10-24 20:18:58 +11:00
Leon Friedrich
b2d389f184 Remove TryLifestage() helpers (#4519) 2023-10-24 18:46:46 +11:00
Leon Friedrich
ad0cb05dd6 Add EnsureComponent(ref Entity<T?>) (#4516) 2023-10-24 17:19:38 +11:00
Leon Friedrich
ad134d9e4e Fix game state logging spam (#4517) 2023-10-24 14:09:55 +11:00
Leon Friedrich
be33bc2219 Re-add force ack threshold (#4423) and fix bugs. (#4438)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-10-22 23:27:15 +11:00
metalgearsloth
aa2fd2107d Add mgs to physics codeowners (#4510)
Please ping me for this is creates more work if you do not ping me.
2023-10-22 05:03:47 -07:00
metalgearsloth
554e0777b1 Version: 171.0.0 2023-10-22 16:58:48 +11:00
metalgearsloth
21b7c5f93e Cleanup relays on joint deletion (#4497) 2023-10-22 16:53:10 +11:00
Leon Friedrich
9e5c1e9c95 Change place-next-to helper methods (#4506) 2023-10-22 16:52:58 +11:00
Leon Friedrich
6825f09fb9 Set EntityLastModifiedTick when an entity spawns (#4509) 2023-10-22 16:51:40 +11:00
DrSmugleaf
58e3a4eb4a Version: 170.0.0 2023-10-21 14:31:08 -07:00
DrSmugleaf
9a342f0d11 Fix double delete entity command, fix not being able to delete individual entities (#4508) 2023-10-21 14:19:34 -07:00
DrSmugleaf
f754ddb96d Remove all usages of obsolete Dirty method, remove some obsoleted methods (#4500) 2023-10-21 14:19:07 -07:00
Leon Friedrich
7feede0d95 Fix duplicate command error (#4507) 2023-10-21 14:18:50 -07:00
Jordan Dominion
ea152366e3 Allow deletion of FileLogHandler logs while engine is running (#4501) 2023-10-21 15:07:10 +02:00
DrSmugleaf
ab47d4e009 Version: 169.0.1 2023-10-21 03:55:02 -07:00
DrSmugleaf
81b2a3825e Fix help command, let the client know about server toolshed commands (#4502) 2023-10-21 03:54:17 -07:00
DrSmugleaf
56d850f389 Version: 169.0.0 2023-10-19 12:27:27 -07:00
DrSmugleaf
b737ecf9b3 Add generic EntityUid, remove some usages of .Owner (#4498) 2023-10-19 12:23:48 -07:00
DrSmugleaf
ed5223b592 Remove by-refness subscription test (#4499) 2023-10-19 02:04:32 -07:00
DrSmugleaf
f87012e681 Allow handling by-value events by ref (#4373) 2023-10-18 18:37:43 -07:00
wixoa
54529fdbe3 Respect the manifest's assemblyPrefix value on the server (#4492) 2023-10-18 20:29:28 +02:00
DrSmugleaf
1745a12e5a Remove casts to Component (#4495) 2023-10-17 20:45:21 -07:00
DrSmugleaf
d201d787b7 Remove obsoletion from localized and console commands (#4496) 2023-10-17 20:18:30 -07:00
DrSmugleaf
904ddea274 Version: 168.0.0 2023-10-17 19:38:56 -07:00
DrSmugleaf
6b6ec844e8 Replace all T : Component constraints with T : IComponent (#4494) 2023-10-17 19:37:46 -07:00
Jordan Dominion
f24d18f470 Allow for ushort CVars (#4493) 2023-10-17 16:44:18 -07:00
metalgearsloth
77654a1628 Version: 167.0.0 2023-10-17 23:51:47 +11:00
Leon Friedrich
f3af813b57 Transform interpolation fixes (#4488) 2023-10-17 23:47:45 +11:00
Leon Friedrich
0623baedcf Fix PVS bug and add new test (#4444)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-10-17 23:33:42 +11:00
Leon Friedrich
2ade6c04c5 Adds more joint debug asserts (#4490) 2023-10-15 11:04:27 +11:00
Kara
a9df9097c1 Kill ContainerHelpers (#4486) 2023-10-15 03:58:25 +11:00
Kara
755dac719f Kill ComponentExt (#4485) 2023-10-15 03:58:04 +11:00
ElectroJr
7095a58685 Version: 166.0.0 2023-10-10 22:23:51 -04:00
Leon Friedrich
16e68a4351 Make PVS session overrides recursive (#4480) 2023-10-11 13:16:00 +11:00
DrSmugleaf
0152f9d1d8 Move Component lifecycle methods to EntityManager (#4483) 2023-10-10 17:34:40 -07:00
DrSmugleaf
16d916796a Add doc clarification to CopyByRef inheritance for the future coder (#4476) 2023-10-10 17:34:24 -07:00
Leon Friedrich
e865157432 Make AssertOwner() accept nullable components (#4475) 2023-10-10 04:41:26 +11:00
metalgearsloth
24d5ce4bd4 Make AddCompUninit obsolete (#4470) 2023-10-10 04:41:10 +11:00
deltanedas
662195e4ff add SortedSetSerializer (#4473) 2023-10-10 04:29:42 +11:00
DrSmugleaf
d00fd6f736 Make AnimationPlayerSystem only log a warning for networked components when the property is automatically networked (#4477) 2023-10-10 04:08:56 +11:00
Leon Friedrich
2d58c1071d Make ExpandPvsEvent lists nullable (#4479) 2023-10-10 04:07:59 +11:00
Leon Friedrich
a9db89d023 Fix nullable NetEntity conversion (#4481) 2023-10-10 04:07:30 +11:00
Leon Friedrich
684cabf3e6 Use metadata query in ToPrettyString() (#4478) 2023-10-09 19:07:31 +11:00
ElectroJr
a4f51f0cd9 Fix release notes 2023-10-08 12:41:45 -04:00
ElectroJr
a8ddd837c8 Version: 165.0.0 2023-10-08 12:39:03 -04:00
Leon Friedrich
82aace7997 Fix SplitContainer.MinDraggableWidth not working with mouse-blocking children. (#4439) 2023-10-09 03:28:23 +11:00
Leon Friedrich
01ce244b7b Validate default values of ProtoId and EntProtoId fields (#4462) 2023-10-02 03:49:45 +11:00
metalgearsloth
58aa6e5c75 Version: 164.0.0 2023-09-30 15:19:43 +10:00
DrSmugleaf
4818c3aab4 Make auto comp states infer when data should be cloned (#4461) 2023-09-30 15:14:10 +10:00
Leon Friedrich
3b6adeb5ff Reduce transform resolves in RecursiveDeleteEntity() (#4468) 2023-09-30 15:12:17 +10:00
metalgearsloth
889b8351be Version: 163.0.0 2023-09-30 14:40:55 +10:00
metalgearsloth
ac37b0a131 Move TimedDespawn to engine (#4448) 2023-09-30 14:35:28 +10:00
Leon Friedrich
f6f1fc425a Use ToPrettyString() in component resolve errors (#4467) 2023-09-30 14:31:40 +10:00
Leon Friedrich
7476628840 Give map and grid entities a default name (#4464) 2023-09-30 14:09:37 +10:00
Leon Friedrich
668cdbe76b Allow adding/removing of widgets in sub-controls (#4443) 2023-09-30 14:08:52 +10:00
Leon Friedrich
a0a6e9b111 Fix render error spam (#4463) 2023-09-30 14:07:27 +10:00
Leon Friedrich
06d28f04e6 Add ExecuteCommand() (#4466) 2023-09-30 14:07:07 +10:00
Leon Friedrich
57897161d0 Fix console backspace exception (#4465) 2023-09-30 14:05:29 +10:00
metalgearsloth
c4c528478e Use gamestate fields to avoid heap allocs (#4452) 2023-09-29 15:03:50 +10:00
DrSmugleaf
a6c295b89c Version: 162.2.1 2023-09-28 17:55:17 -07:00
DrSmugleaf
165913a4de Add IComparable to ProtoId, EntProtoId and LocId (#4460) 2023-09-28 17:53:23 -07:00
metalgearsloth
675dfdaabd Fix scroll containers invalidating on first scroll (#4449) 2023-09-28 16:58:18 -07:00
Kara
fab172d6f6 Allow force submitting line edits (#4455) 2023-09-28 11:07:55 -07:00
metalgearsloth
e75c1659f6 Version: 162.2.0 2023-09-28 20:25:52 +10:00
DrSmugleaf
0c440a8fc9 Localize "View Variables" (#4454) 2023-09-28 20:22:54 +10:00
DrSmugleaf
0c2c8f352a Add LocId and LocIdSerializer (#4456) 2023-09-28 20:22:17 +10:00
DrSmugleaf
0a4a2b7a36 Add nullable conversion operators to ProtoId and EntProtoId (#4447) 2023-09-28 20:18:45 +10:00
metalgearsloth
b5b59c1d2f Use CollectionsMarshal in clientgamestatemanager (#4453) 2023-09-28 20:13:38 +10:00
metalgearsloth
f4f0967fdc Fix double contact deletion throwing (#4450) 2023-09-28 20:12:45 +10:00
metalgearsloth
3d69766112 Use CollectionsMarshal for mergeimplicitdata (#4451) 2023-09-28 20:12:22 +10:00
DrSmugleaf
d1eb3438d5 Add support for automatically networking entity lists and sets (#4446) 2023-09-25 18:02:53 +10:00
ElectroJr
8f6b189d29 Version: 162.1.1 2023-09-19 17:59:16 -04:00
Leon Friedrich
ef8b278b47 Fix HideSpawnMenu (#4437) 2023-09-20 07:57:50 +10:00
metalgearsloth
c53ce2c907 Version: 162.1.0 2023-09-19 23:19:39 +10:00
Leon Friedrich
f063aa3ea1 Use CollectionsMarshal in RobustTree (#4429) 2023-09-19 23:17:27 +10:00
Leon Friedrich
30f63254ef Mark ProtoId<T> as serializable (#4430) 2023-09-19 23:13:14 +10:00
Leon Friedrich
30a5b6152c Slightly improve AddToChunkSetRecursively() (#4432) 2023-09-19 23:13:00 +10:00
Leon Friedrich
910a7f8bff Fix visibility layers not updating on children (#4431) 2023-09-19 23:12:52 +10:00
Leon Friedrich
526a88293e Use CollectionsMarshal in AddComponentInternal() (#4435) 2023-09-19 23:09:41 +10:00
metalgearsloth
22cd840b83 Revert "Add force ack threshold (#4423)" (#4436) 2023-09-19 18:36:41 +10:00
metalgearsloth
415c518bc7 Version: 162.0.0 2023-09-19 17:40:46 +10:00
Leon Friedrich
2417dbb0e0 Use CollectionsMarshal in DynamicTree (#4433) 2023-09-19 15:58:02 +10:00
Leon Friedrich
005673a957 Add force ack threshold (#4423) 2023-09-19 15:16:01 +10:00
Leon Friedrich
942db3120c Make entity system proxy methods use Metadata & Transform queries (#4434) 2023-09-19 15:11:21 +10:00
Leon Friedrich
c0a5fab19e Add missing EntitySystem - EntityManager proxy method (#4427) 2023-09-18 12:17:46 +10:00
Leon Friedrich
d16c62b132 Use CollectionsMarshal in PVS (#4428) 2023-09-18 12:17:32 +10:00
Leon Friedrich
7da22557fe Add entity categories (#4356) 2023-09-18 12:14:26 +10:00
metalgearsloth
92f47c0f20 Version: 161.1.0 2023-09-18 11:48:03 +10:00
Leon Friedrich
9576d0739f Add more DebugTools assert variants (#4425) 2023-09-18 11:18:35 +10:00
Leon Friedrich
10f25faabf Try fix oldest ack issues (#4426) 2023-09-18 11:17:49 +10:00
Leon Friedrich
74831a177e Don't attempt to insert entities into deleted containers (#4424) 2023-09-18 10:57:14 +10:00
ElectroJr
88d3168913 Version: 161.0.0 2023-09-17 13:56:34 -04:00
Leon Friedrich
902519093c Add PVS debug command (#4422) 2023-09-18 03:49:57 +10:00
Leon Friedrich
366266a8ae Fix light animations not working (#4413) 2023-09-18 03:49:47 +10:00
Leon Friedrich
a22cce7783 Fix metadata assert (#4419) 2023-09-17 17:18:44 +10:00
metalgearsloth
e5e738b8cd Maybe fix heisentest (#4418) 2023-09-17 16:13:37 +10:00
metalgearsloth
b8f6e83473 Use stackalloc Span<Vector2> in ComputeHull (#4417) 2023-09-17 12:41:53 +10:00
Artur
c5fb186c57 Add missing InvariantCulture to AngleTypeParser (#4411) 2023-09-17 11:32:01 +10:00
Leon Friedrich
131d7f5422 Add string formatting for client-side NetEntity ids (#4415) 2023-09-17 11:29:54 +10:00
Leon Friedrich
217996f1ed Use ToPrettyString() in state error logs. (#4416) 2023-09-17 11:29:02 +10:00
DrSmugleaf
fc718d68a5 Fix IClydeWindow resized xmldoc (#4414) 2023-09-17 11:28:43 +10:00
Leon Friedrich
d7d9578803 Mark EntPrototId as NetSerializable (#4412) 2023-09-17 11:25:40 +10:00
metalgearsloth
9bbeb54569 Version: 160.1.0 2023-09-17 11:20:03 +10:00
metalgearsloth
1ea7071ffb Backport some arch comp net changes (#4408)
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-09-17 11:03:11 +10:00
ElectroJr
196028b619 Version: 160.0.2 2023-09-16 14:50:34 -04:00
ElectroJr
c102da052f Make VV fields private 2023-09-16 14:47:27 -04:00
metalgearsloth
5d46cdcfa4 Add transform parent + container VVs (#4407) 2023-09-16 17:08:08 +10:00
metalgearsloth
cd646d3b07 Version: 160.0.0 2023-09-16 15:26:56 +10:00
Leon Friedrich
922165fa19 Include sensors in default entity lookups (#4406) 2023-09-16 15:22:54 +10:00
metalgearsloth
4879252e99 Remove comprefs entirely (#4367) 2023-09-15 21:52:47 +10:00
DrSmugleaf
0e21f5727a Version: 159.1.0 2023-09-15 03:41:17 -07:00
Leon Friedrich
3ce8a00389 Store metadata component in NetEntity lookup dictionary (#4400)
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
2023-09-15 02:01:44 -07:00
Leon Friedrich
9a283fe541 PVS NetEntity related changes (#4399) 2023-09-15 01:47:35 -07:00
Leon Friedrich
f3e3e64db3 Re-add entity check to fixture equality (#4397) 2023-09-15 18:44:24 +10:00
Leon Friedrich
4a4a135089 Make ToPrettyString() take in nullable EntityUids (#4396) 2023-09-15 01:43:39 -07:00
metalgearsloth
e323a67806 Remove 1 pvs getcomp (#4398) 2023-09-15 15:28:16 +10:00
Leon Friedrich
3a328ffdd5 More NetEntity VV fixes (#4395) 2023-09-15 13:37:18 +10:00
Leon Friedrich
bc5107e297 Misc NetEntity fixes for VV (#4390) 2023-09-15 10:11:28 +10:00
metalgearsloth
2abf33c9be Queue LightTree update on light states (#4385)
Might fix some more issues.
2023-09-15 06:03:25 +12:00
metalgearsloth
71c46828c2 Minor re-apply net state change (#4392)
Slightly faster than checking deleted first, at least for now.
2023-09-15 05:56:51 +12:00
DrSmugleaf
814d6fe2d0 Add ProtoId, EntProtoId and serializers (#4387) 2023-09-12 23:19:40 -07:00
DrSmugleaf
77b98b8308 Add ProtoId, EntProtoId and serializers (#4386) 2023-09-13 14:32:01 +10:00
metalgearsloth
34b0a7fc6d Fix tooltip bounds (#4384) 2023-09-13 13:23:34 +10:00
DrSmugleaf
1b6123c79f Make data field tags optional (#4382) 2023-09-13 12:58:46 +10:00
DrSmugleaf
1476f9d462 Make QueueDel work with nullable entity uids (#4381) 2023-09-13 12:58:03 +10:00
Leon Friedrich
d62efe7301 Add UnknownEntityDeleteTest (#4380) 2023-09-13 12:57:50 +10:00
metalgearsloth
6af0c88f27 Fix IEye not updating always (#4368) 2023-09-13 11:51:11 +10:00
metalgearsloth
5f05b0aa2a Version: 159.0.3 2023-09-13 11:43:34 +10:00
metalgearsloth
e6c335b6cd Fix nent deleted entity handling (#4379) 2023-09-13 11:22:21 +10:00
metalgearsloth
5cd8e8276e Version: 159.0.2 2023-09-13 00:33:43 +10:00
metalgearsloth
22aeec45f9 Minor resolve light update (#4366) 2023-09-13 00:32:02 +10:00
metalgearsloth
aed53fb63d Fix lights not updating (#4377) 2023-09-13 00:31:45 +10:00
metalgearsloth
5ebe97aec1 Version: 159.0.1 2023-09-12 13:30:52 +10:00
metalgearsloth
3ff374a4af Remove unnecessary meta getcomp (#4376) 2023-09-12 13:30:15 +10:00
metalgearsloth
c5bcf853ac Some netstate fixes (#4375) 2023-09-12 13:05:20 +10:00
metalgearsloth
0624ac36cd Remove PhysicsComponent ref from fixtures (#4374) 2023-09-12 12:49:33 +10:00
metalgearsloth
dd906e9b01 Version: 159.0.0 2023-09-11 21:26:25 +10:00
DrSmugleaf
7f99b44e5c Remove inactive reviewers from CODEOWNERS (#4365) 2023-09-11 20:51:55 +10:00
metalgearsloth
268eb862ea Refactor UI a bit more to shared (#4343) 2023-09-11 20:21:23 +10:00
Leon Friedrich
467f518421 Make entity-deletion take in nullables (#4363) 2023-09-11 20:19:04 +10:00
DrSmugleaf
4666a87aa5 Add obsoletion warnings to serialization source generated methods, xmldocs (#4364) 2023-09-11 20:15:07 +10:00
metalgearsloth
25007a743f Remove lights component reference (#4316) 2023-09-11 19:17:28 +10:00
metalgearsloth
8ce3a03136 Version: 158.0.0 2023-09-11 17:41:27 +10:00
metalgearsloth
c4d6690a71 Remove SharedEyeComponent (#4309) 2023-09-11 16:15:08 +10:00
Kara
5486bc7686 Add tile overlay edge priority (#4341) 2023-09-11 15:30:49 +10:00
ElectroJr
cdf44ef3d9 Version: 157.1.0 2023-09-11 01:27:55 -04:00
metalgearsloth
49ec5b9ca3 Use RichTextLabel for tooltips (#4331) 2023-09-11 13:55:13 +10:00
ElectroJr
8b53b89423 Version: 157.0.0 2023-09-10 19:46:20 -04:00
metalgearsloth
3fd731d917 Network entity ids (#4252) 2023-09-11 09:42:55 +10:00
metalgearsloth
cb1d4ae843 Version: 156.0.0 2023-09-10 21:48:19 +10:00
metalgearsloth
039b70f502 Revert "Remove IContainer and move some functions to the system (#4351)" (#4361) 2023-09-10 21:46:17 +10:00
metalgearsloth
7892cc895f Tooltip QoL (#4330) 2023-09-10 20:23:52 +10:00
ElectroJr
77108284b8 Version: 155.0.0 2023-09-09 22:26:10 -04:00
Leon Friedrich
5e21dbdd7f Remove IContainer and move some functions to the system (#4351) 2023-09-10 12:17:00 +10:00
ShadowCommander
8274623edb Add a command to hide replay UI (#4355) 2023-09-10 10:49:34 +10:00
Leon Friedrich
e923d69083 Miscellaneous replay related changes (#4354) 2023-09-10 10:48:42 +10:00
PrPleGoo
6e8ab5ce78 Ignore deleted components while raising events. (#4311) 2023-09-10 10:48:00 +10:00
metalgearsloth
f905ea631b Raise MapInitEvent on components added after spawn (#4290) 2023-09-10 10:47:18 +10:00
Pieter-Jan Briers
be54c41891 Fix localization file error formatting. 2023-09-09 20:10:55 +02:00
489 changed files with 12913 additions and 7962 deletions

19
.github/CODEOWNERS vendored
View File

@@ -1,23 +1,12 @@
# Last match in file takes precedence.
# Ping for all PRs
* @Acruid @PJB3005 @ZoldorfTheWizard
/Robust.Client.NameGenerator @PaulRitter
/Robust.Client.Injectors @PaulRitter
/Robust.Generators @PaulRitter
/Robust.Analyzers @PaulRitter
/Robust.*/GameStates @PaulRitter
/Robust.Shared/Analyzers @PaulRitter
/Robust.*/Serialization @PaulRitter @DrSmugleaf
/Robust.*/Prototypes @PaulRitter
/Robust.Shared/GameObjects/ComponentDependencies @PaulRitter
/Robust.*/Containers @PaulRitter
# Be they Fluent translations or Freemarker templates, I know them both!
*.ftl @RemieRichards
* @PJB3005 @DrSmugleaf
# commands commands commands commands
**/Toolshed/** @moonheart08
*Command.cs @moonheart08
*Commands.cs @moonheart08
# Physics
**/Robust.Shared/Physics/** @metalgearsloth

View File

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

View File

@@ -54,8 +54,426 @@ END TEMPLATE-->
*None yet*
## 174.0.3
## 174.0.2
## 174.0.1
## 174.0.0
### Breaking changes
* ActorComponent has been moved to `Robust.Shared.Player` (namespace changed).
### New features
* Added `SpriteSystem.GetFrame()` method, which takes in an animated RSI and a time and returns a frame/texture.
* Added `IRobustRandom.NextAngle()`
## 173.1.0
### New features
* Add physics chain shapes from Box2D.
## 173.0.0
### Breaking changes
* Remove GridModifiedEvent in favor of TileChangedEvent.
### Bugfixes
* Fix some grid rendering bugs where chunks don't get destroyed correctly.
## 172.0.0
### Breaking changes
* Remove TryLifestage helper methods.
* Refactor IPlayerManager to remove more IPlayerSession, changed PlayerAttachedEvent etc on client to have the Local prefix, and shuffled namespaces around.
### New features
* Add EnsureComponent(ref Entity<\T?>)
### Bugfixes
* Re-add force ask threshold and fix other PVS bugs.
## 171.0.0
### Breaking changes
* Change PlaceNextTo method names to be more descriptive.
* Rename RefreshRelay for joints to SetRelay to match its behaviour.
### Bugfixes
* Fix PVS error spam for joint relays not being cleaned up.
### Other
* Set EntityLastModifiedTick on entity spawn.
## 170.0.0
### Breaking changes
* Removed obsolete methods and properties in VisibilitySystem, SharedContainerSystem and MetaDataComponent.
### Bugfixes
* Fixed duplicate command error.
* Fixed not being able to delete individual entities with the delete command.
### Other
* FileLogHandler logs can now be deleted while the engine is running.
## 169.0.1
### Other
* The client now knows about registered server-side toolshed commands.
## 169.0.0
### Breaking changes
* Entity<T> has been introduced to hold a component and its owning entity. Some methods that returned and accepted components directly have been removed or obsoleted to reflect this.
### Other
* By-value events may now be subscribed to by-ref.
* The manifest's assemblyPrefix value is now respected on the server.
## 168.0.0
### Breaking changes
* The Component.OnRemove method has been removed. Use SubscribeLocalEvent<TComp, ComponentRemove>(OnRemove) from an EntitySystem instead.
## 167.0.0
### Breaking changes
* Remove ComponentExtensions.
* Remove ContainerHelpers.
* Change some TransformSystem methods to fix clientside lerping.
### Bugfixes
* Fixed PVS bugs from dropped entity states.
### Other
* Add more joint debug asserts.
## 166.0.0
### Breaking changes
* EntityUid-NetEntity conversion methods now return null when given a null value, rather than returning an invalid id.
* ExpandPvsEvent now defaults to using null lists to reduce allocations.
* Various component lifestage related methods have been moved from the `Component` class to `EntityManager`.
* Session/client specific PVS overrides are now always recursive, which means that all children of the overriden entity will also get sent.
### New features
* Added a SortedSet yaml serializer.
### Other
* AddComponentUninitialized is now marked as obsolete and will be removed in the future.
* DebugTools.AssertOwner() now accepts null components.
## 165.0.0
### Breaking changes
* The arguments of `SplitContainer`s resize-finished event have changed.
### New features
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
### Bugfixes
* The minimum draggable area of split containers now blocks mouse inputs.
## 164.0.0
### Breaking changes
* Make automatic component states infer cloneData.
* Removed cloneData from AutoNetworkedFieldAttribute. This is now automatically inferred.
### Internal
* Reduce Transform GetComponents in RecursiveDeleteEntity.
## 163.0.0
### Breaking changes
* Moved TimedDespawn to engine for a component that deletes the attached entity after a timer has elapsed.
### New features
* Add ExecuteCommand for integration tests.
* Allow adding / removing widgets of cub-controls.
* Give maps / grids a default name to help with debugging.
* Use ToPrettyString in component resolve errors to help with debugging.
### Bugfixes
* Fix console backspace exception.
* Fix rendering invalid maps spamming exceptions every frame.
### Internal
* Move ClientGameStatemanager local variables to fields to avoid re-allocating every tick.
## 162.2.1
## 162.2.0
### New features
* Add support for automatically networking entity lists and sets.
* Add nullable conversion operators for ProtoIds.
* Add LocId serializer for validation.
### Bugfixes
* Fix deleting a contact inside of collision events throwing.
* Localize VV.
### Internal
* Use CollectionsMarshal in GameStateManager.
## 162.1.1
### Bugfixes
* Fixes "NoSpawn" entities appearing in the spawn menu.
## 162.1.0
### New features
* Mark ProtoId as NetSerializable.
### Bugfixes
* Temporarily revert NetForceAckThreshold change as it can lead to client stalling.
* Fix eye visibility layers not updating on children when a parent changes.
### Internal
* Use CollectionsMarshal in RobustTree and AddComponentInternal.
## 162.0.0
### New features
* Add entity categories for prototypes and deprecate the `noSpawn` tag.
* Add missing proxy method for `TryGetEntityData`.
* Add NetForceAckThreshold cvar to forcibly update acks for late clients.
### Internal
* Use CollectionMarshals in PVS and DynamicTree.
* Make the proxy methods use MetaQuery / TransformQuery.
## 161.1.0
### New features
* Add more DebugTools assert variations.
### Bugfixes
* Don't attempt to insert entities into deleted containers.
* Try to fix oldestAck not being set correctly leading to deletion history getting bloated for pvs.
## 161.0.0
### Breaking changes
* Point light animations now need to use different component fields in order to animate the lights. `Enabled` should be replaced with `AnimatedEnable` and `Radius` should be replaced with `AnimatedRadius`
### New features
* EntProtoId is now net-serializable
* Added print_pvs_ack command to debug PVS issues.
### Bugfixes
* Fixes AngleTypeParser not using InvariantCulture
* Fixed a bug that was causing `MetaDataComponent.LastComponentRemoved` to be updated improperly.
### Other
* The string representation of client-side entities now looks nicer and simply uses a 'c' prefix.
## 160.1.0
### New features
* Add optional MetaDataComponent args to Entitymanager methods.
### Internal
* Move _netComponents onto MetaDataComponent.
* Remove some component resolves internally on adding / removing components.
## 160.0.2
### Other
* Transform component and containers have new convenience fields to make using VIewVariables easier.
## 160.0.0
### Breaking changes
* ComponentReference has now been entirely removed.
* Sensor / non-hard physics bodies are now included in EntityLookup by default.
## 159.1.0
## 159.0.3
### Bugfixes
* Fix potentially deleted entities having states re-applied when NetEntities come in.
## 159.0.2
### Bugfixes
* Fix PointLight state handling not queueing ComponentTree updates.
## 159.0.1
### Bugfixes
* Fix pending entity states not being removed when coming in (only on entity deletion).
### Internal
* Remove PhysicsComponent ref from Fixture.
## 159.0.0
### Breaking changes
* Remove ComponentReference from PointLights.
* Move more of UserInterfaceSystem to shared.
* Mark some EntitySystem proxy methods as protected instead of public.
### New features
* Make entity deletion take in a nullable EntityUid.
* Added a method to send predicted messages via BUIs.
### Other
* Add Obsoletions to more sourcegen serv4 methods.
* Remove inactive reviewers from CODEOWNERs.
## 158.0.0
### Breaking changes
* Remove SharedEyeComponent.
* Add Tile Overlay edge priority.
## 157.1.0
### New features
* UI tooltips now use rich text labels.
## 157.0.0
### Breaking changes
* Unrevert container changes from 155.0.0.
* Added server-client EntityUid separation. A given EntityUid will no longer refer to the same entity on the server & client.
* EntityUid is no longer net-serializable, use NetEntity instead, EntityManager & entity systems have helper methods for converting between the two,
## 156.0.0
### Breaking changes
* Revert container changes from 155.0.0.
## 155.0.0
### Breaking changes
* MapInitEvent now gets raised for components that get added to entities that have already been map-initialized.
### New features
* VirtualWritableDirProvider now supports file renaming/moving.
* Added a new command for toggling the replay UI (`replay_toggleui`).
### Bugfixes
* Fixed formatting of localization file errors.
* Directed event subscriptions will no longer error if the corresponding component is queued for deletion.
## 154.2.0
### New features
* Added support for advertising to multiple hubs simultaneously.
* Added new functions to ContainerSystem that recursively look for a component on a contained entity's parents.
### Bugfixes
* Fix Direction.TurnCw/TurnCcw to South returning Invalid.

View File

@@ -1,7 +1,7 @@
- type: entity
id: debugRotation
abstract: true
suffix: DEBUG
categories: [ debug ]
components:
- type: Sprite
netsync: false

View File

@@ -0,0 +1,17 @@
# debug related entities
- type: entityCategory
id: debug
name: entity-category-name-debug
description: entity-category-desc-debug
# entities that spawn other entities
- type: entityCategory
id: spawner
name: entity-category-name-spawner
description: entity-category-desc-spawner
# entities that should be hidden from the spawn menu
- type: entityCategory
id: hideSpawnMenu
name: entity-category-name-hide
description: entity-category-desc-hide

View File

@@ -17,15 +17,15 @@ cmd-error-dir-not-found = Could not find directory: {$dir}.
cmd-failure-no-attached-entity = There is no entity attached to this shell.
## 'help' command
cmd-oldhelp-desc = Display general help or help text for a specific command
cmd-oldhelp-help = Usage: help [command name]
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
cmd-oldhelp-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-oldhelp-unknown = Unknown command: { $command }
cmd-oldhelp-top = { $command } - { $description }
cmd-oldhelp-invalid-args = Invalid amount of arguments.
cmd-oldhelp-arg-cmdname = [command name]
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-help-unknown = Unknown command: { $command }
cmd-help-top = { $command } - { $description }
cmd-help-invalid-args = Invalid amount of arguments.
cmd-help-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.

View File

@@ -0,0 +1,8 @@
entity-category-name-debug = Debug
entity-category-desc-debug = Entity prototypes intended for debugging & testing.
entity-category-name-spawner = Spawner
entity-category-desc-spawner = Entity prototypes that spawn other entities.
entity-category-name-hide = Hidden
entity-category-desc-hide = Entity prototypes that should be hidden from the spawn menu

View File

@@ -1,5 +1,6 @@
## ViewVariablesInstanceEntity
view-variables = View Variables
view-variable-instance-entity-server-components-add-component-button-placeholder = Add Component
view-variable-instance-entity-client-variables-tab-title = Client Variables
view-variable-instance-entity-client-components-tab-title = Client Components
@@ -8,4 +9,4 @@ view-variable-instance-entity-server-components-tab-title = Server Components
view-variable-instance-entity-client-components-search-bar-placeholder = Search
view-variable-instance-entity-server-components-search-bar-placeholder = Search
view-variable-instance-entity-add-window-server-components = Add Component [S]
view-variable-instance-entity-add-window-client-components = Add Component [C]
view-variable-instance-entity-add-window-client-components = Add Component [C]

View File

@@ -23,16 +23,6 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
"Make sure that methods subscribing to a ref event have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByValueEventSubscribedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event subscribed to by-ref",
"Tried to subscribe to a value event '{0}' by-ref.",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure that methods subscribing to value events do not have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
@@ -55,7 +45,6 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
ByRefEventSubscribedByValueRule,
ByValueEventSubscribedByRefRule,
ByRefEventRaisedByValueRule,
ByValueEventRaisedByRefRule
);
@@ -64,71 +53,9 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckEventSubscription, OperationKind.Invocation);
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
}
private void CheckEventSubscription(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)
return;
var subscribeMethods = context.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
.GetMembers()
.Where(m => m.Name.Contains("SubscribeLocalEvent"))
.Cast<IMethodSymbol>();
if (subscribeMethods == null)
return;
if (!subscribeMethods.Any(m => m.Equals(operation.TargetMethod.OriginalDefinition, Default)))
return;
var typeArguments = operation.TargetMethod.TypeArguments;
if (typeArguments.Length < 1 || typeArguments.Length > 2)
return;
if (operation.Arguments.First().Value is not IDelegateCreationOperation delegateCreation)
return;
if (delegateCreation.Target is not IMethodReferenceOperation methodReference)
return;
var eventParameter = methodReference.Method.Parameters.LastOrDefault();
if (eventParameter == null)
return;
ITypeSymbol eventArgument;
switch (typeArguments.Length)
{
case 1:
eventArgument = typeArguments[0];
break;
case 2:
eventArgument = typeArguments[1];
break;
default:
return;
}
var byRefAttribute = context.Compilation.GetTypeByMetadataName(ByRefAttribute);
if (byRefAttribute == null)
return;
var isByRefEventType = eventArgument
.GetAttributes()
.Any(attribute => attribute.AttributeClass?.Equals(byRefAttribute, Default) ?? false);
var parameterIsRef = eventParameter.RefKind == RefKind.Ref;
if (isByRefEventType != parameterIsRef)
{
var descriptor = isByRefEventType ? ByRefEventSubscribedByValueRule : ByValueEventSubscribedByRefRule;
var diagnostic = Diagnostic.Create(descriptor, operation.Syntax.GetLocation(), eventArgument);
context.ReportDiagnostic(diagnostic);
}
}
private void CheckEventRaise(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)

View File

@@ -18,7 +18,6 @@ public static class Diagnostics
public const string IdInvalidNotNullableFlagType = "RA0011";
public const string IdNotNullableFlagValueType = "RA0012";
public const string IdByRefEventSubscribedByValue = "RA0013";
public const string IdValueEventSubscribedByRef = "RA0014";
public const string IdByRefEventRaisedByValue = "RA0015";
public const string IdValueEventRaisedByRef = "RA0016";
public const string IdDataDefinitionPartial = "RA0017";

View File

@@ -54,7 +54,7 @@ public class RecursiveMoveBenchmark
var mapSys = _entMan.System<SharedMapSystem>();
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGrid(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_mapCoords = new EntityCoordinates(map, 100, 100);

View File

@@ -1,8 +1,6 @@
using System;
using System.Linq;
using System.Net;
using Robust.Client.Configuration;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -10,13 +8,12 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
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;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -65,12 +62,12 @@ namespace Robust.Client
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize();
_playMan.Initialize(0);
_playMan.PlayerListUpdated += OnPlayerListUpdated;
Reset();
}
private void OnPlayerListUpdated(object? sender, EventArgs e)
private void OnPlayerListUpdated()
{
var serverPlayers = _playMan.PlayerCount;
if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected)
@@ -130,9 +127,10 @@ namespace Robust.Client
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
DebugTools.Assert(!_net.IsConnected);
_playMan.Startup();
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
_playMan.SetupSinglePlayer(name);
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
_playMan.JoinGame(_playMan.LocalSession!);
GameStartedSetup();
}
@@ -173,22 +171,14 @@ namespace Robust.Client
info.ServerName = serverName;
}
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
info.ServerMaxPlayers = maxPlayers;
var userName = _net.ServerChannel!.UserName;
var userId = _net.ServerChannel.UserId;
var channel = _net.ServerChannel!;
// start up player management
_playMan.Startup();
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
_playMan.SetupMultiplayer(channel);
_playMan.PlayerStatusChanged += OnStatusChanged;
var serverPlayers = _playMan.PlayerCount;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
_discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
}
@@ -221,6 +211,8 @@ namespace Robust.Client
private void Reset()
{
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
_playMan.PlayerStatusChanged -= OnStatusChanged;
_configManager.ClearReceivedInitialNwVars();
OnRunLevelChanged(ClientRunLevel.Initialize);
}
@@ -263,19 +255,17 @@ namespace Robust.Client
Reset();
}
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session != _playMan.LocalSession)
return;
// player finished fully connecting to the server.
// OldStatus is used here because it can go from connecting-> connected or connecting-> ingame
if (eventArgs.OldStatus == SessionStatus.Connecting)
{
OnPlayerJoinedServer(_playMan.LocalPlayer!.Session);
}
if (eventArgs.NewStatus == SessionStatus.InGame)
{
OnPlayerJoinedGame(_playMan.LocalPlayer!.Session);
}
if (e.OldStatus == SessionStatus.Connecting)
OnPlayerJoinedServer(e.Session);
else if (e.NewStatus == SessionStatus.InGame)
OnPlayerJoinedGame(e.Session);
}
private void OnRunLevelChanged(ClientRunLevel newRunLevel)

View File

@@ -37,7 +37,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;

View File

@@ -2,11 +2,13 @@ using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.ViewVariables;
namespace Robust.Client.ComponentTrees;
[RegisterComponent]
public sealed partial class LightTreeComponent: Component, IComponentTreeComponent<PointLightComponent>
{
[ViewVariables]
public DynamicTree<ComponentTreeEntry<PointLightComponent>> Tree { get; set; } = default!;
}

View File

@@ -13,7 +13,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;

View File

@@ -22,13 +22,11 @@ namespace Robust.Client.Console.Commands
return;
}
var entity = EntityUid.Parse(args[0]);
var netEntity = NetEntity.Parse(args[0]);
var entity = _entityManager.GetEntity(netEntity);
var componentName = args[1];
var component = (Component) _componentFactory.GetComponent(componentName);
component.Owner = entity;
var component = _componentFactory.GetComponent(componentName);
_entityManager.AddComponent(entity, component);
}
}
@@ -49,7 +47,8 @@ namespace Robust.Client.Console.Commands
return;
}
var entityUid = EntityUid.Parse(args[0]);
var netEntity = NetEntity.Parse(args[0]);
var entityUid = _entityManager.GetEntity(netEntity);
var componentName = args[1];
var registration = _componentFactory.GetRegistration(componentName);

View File

@@ -78,14 +78,7 @@ namespace Robust.Client.Console.Commands
message.Append($"net ID: {registration.NetID}");
}
message.Append($", References:");
shell.WriteLine(message.ToString());
foreach (var type in registration.References)
{
shell.WriteLine($" {type}");
}
}
catch (UnknownComponentException)
{
@@ -296,6 +289,7 @@ namespace Robust.Client.Console.Commands
internal sealed class SnapGridGetCell : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _map = default!;
public override string Command => "sggcell";
@@ -310,7 +304,7 @@ namespace Robust.Client.Console.Commands
string indices = args[1];
if (!EntityUid.TryParse(args[0], out var gridUid))
if (!NetEntity.TryParse(args[0], out var gridNet))
{
shell.WriteError($"{args[0]} is not a valid entity UID.");
return;
@@ -322,7 +316,7 @@ namespace Robust.Client.Console.Commands
return;
}
if (_map.TryGetGrid(gridUid, out var grid))
if (_map.TryGetGrid(_entManager.GetEntity(gridNet), out var grid))
{
foreach (var entity in grid.GetAnchoredEntities(new Vector2i(
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
@@ -430,6 +424,7 @@ namespace Robust.Client.Console.Commands
internal sealed class GridTileCount : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _map = default!;
public override string Command => "gridtc";
@@ -442,7 +437,8 @@ namespace Robust.Client.Console.Commands
return;
}
if (!EntityUid.TryParse(args[0], out var gridUid))
if (!NetEntity.TryParse(args[0], out var gridUidNet) ||
!_entManager.TryGetEntity(gridUidNet, out var gridUid))
{
shell.WriteLine($"{args[0]} is not a valid entity UID.");
return;

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var type = Type.GetType(args[0]);
var type = GetType(args[0]);
if (type == null)
{
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
shell.WriteLine(sig);
}
}
private Type? GetType(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetType(name) is { } type)
return type;
}
return null;
}
}
#endif
}

View File

@@ -1,6 +1,7 @@
#if DEBUG
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
@@ -8,7 +9,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Debugging
@@ -19,6 +19,7 @@ namespace Robust.Client.Debugging
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
private Label? _label;
@@ -70,7 +71,7 @@ namespace Robust.Client.Debugging
return;
}
var tile = grid.GetTileRef(spot);
var tile = _mapSystem.GetTileRef(gridUid, grid, spot);
_label.Position = mouseSpot.Position + new Vector2(32, 0);
if (_hovered?.GridId == gridUid && _hovered?.Tile == tile) return;
@@ -79,7 +80,7 @@ namespace Robust.Client.Debugging
var text = new StringBuilder();
foreach (var ent in grid.GetAnchoredEntities(spot))
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
{
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
{

View File

@@ -46,7 +46,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -207,6 +206,7 @@ namespace Robust.Client.Debugging
private readonly Font _font;
private HashSet<Joint> _drawnJoints = new();
private List<Entity<MapGridComponent>> _grids = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
{
@@ -231,32 +231,33 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
var xform = _physicsSystem.GetPhysicsTransform(physBody);
var comp = physBody.Comp;
const float AlphaModifier = 0.2f;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
{
// Invalid shape - Box2D doesn't check for IsSensor but we will for sanity.
if (physBody.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
if (comp.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
{
DrawShape(worldHandle, fixture, xform, Color.Red.WithAlpha(AlphaModifier));
}
else if (!physBody.CanCollide)
else if (!comp.CanCollide)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.3f).WithAlpha(AlphaModifier));
}
else if (physBody.BodyType == BodyType.Static)
else if (comp.BodyType == BodyType.Static)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.9f, 0.5f).WithAlpha(AlphaModifier));
}
else if ((physBody.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
else if ((comp.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.9f).WithAlpha(AlphaModifier));
}
else if (!physBody.Awake)
else if (!comp.Awake)
{
DrawShape(worldHandle, fixture, xform, new Color(0.6f, 0.6f, 0.6f).WithAlpha(AlphaModifier));
}
@@ -275,15 +276,18 @@ namespace Robust.Client.Debugging
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
var color = Color.Purple.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
var transform = _physicsSystem.GetPhysicsTransform(physBody);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.Comp.LocalCenter), 0.2f, color);
}
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids);
foreach (var grid in _grids)
{
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.Owner);
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid);
var color = Color.Orange.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(grid.Owner);
var transform = _physicsSystem.GetPhysicsTransform(grid);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
}
}
@@ -292,14 +296,14 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
var xform = _physicsSystem.GetPhysicsTransform(physBody);
const float AlphaModifier = 0.2f;
Box2? aabb = null;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
@@ -318,10 +322,11 @@ namespace Robust.Client.Debugging
{
_drawnJoints.Clear();
foreach (var jointComponent in _entityManager.EntityQuery<JointComponent>(true))
var query = _entityManager.AllEntityQueryEnumerator<JointComponent>();
while (query.MoveNext(out var uid, out var jointComponent))
{
if (jointComponent.JointCount == 0 ||
!_entityManager.TryGetComponent(jointComponent.Owner, out TransformComponent? xf1) ||
!_entityManager.TryGetComponent(uid, out TransformComponent? xf1) ||
!viewAABB.Contains(xf1.WorldPosition)) continue;
foreach (var (_, joint) in jointComponent.Joints)
@@ -361,6 +366,9 @@ namespace Robust.Client.Debugging
_debugPhysicsSystem.PointCount = 0;
}
worldHandle.UseShader(null);
worldHandle.SetTransform(Matrix3.Identity);
}
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
@@ -370,28 +378,31 @@ namespace Robust.Client.Debugging
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
{
var hoverBodies = new List<PhysicsComponent>();
var hoverBodies = new List<Entity<PhysicsComponent>>();
var bounds = Box2.UnitCentered.Translated(_eyeManager.PixelToMap(mousePos.Position).Position);
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, bounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
hoverBodies.Add(physBody);
var uid = physBody.Owner;
if (_entityManager.HasComponent<MapGridComponent>(uid)) continue;
hoverBodies.Add((uid, physBody));
}
var lineHeight = _font.GetLineHeight(1f);
var drawPos = mousePos.Position + new Vector2(20, 0) + new Vector2(0, -(hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
foreach (var body in hoverBodies)
foreach (var bodyEnt in hoverBodies)
{
if (body != hoverBodies[0])
if (bodyEnt != hoverBodies[0])
{
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
var body = bodyEnt.Comp;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
@@ -430,6 +441,9 @@ namespace Robust.Client.Debugging
}
}
}
screenHandle.UseShader(null);
screenHandle.SetTransform(Matrix3.Identity);
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -451,11 +465,26 @@ namespace Robust.Client.Debugging
{
switch (fixture.Shape)
{
case ChainShape cShape:
{
var count = cShape.Count;
var vertices = cShape.Vertices;
var v1 = Transform.Mul(xform, vertices[0]);
for (var i = 1; i < count; ++i)
{
var v2 = Transform.Mul(xform, vertices[i]);
worldHandle.DrawLine(v1, v2, color);
v1 = v2;
}
}
break;
case PhysShapeCircle circle:
var center = Transform.Mul(xform, circle.Position);
worldHandle.DrawCircle(center, circle.Radius, color);
break;
case EdgeShape edge:
{
var v1 = Transform.Mul(xform, edge.Vertex1);
var v2 = Transform.Mul(xform, edge.Vertex2);
worldHandle.DrawLine(v1, v2, color);
@@ -465,6 +494,7 @@ namespace Robust.Client.Debugging
worldHandle.DrawCircle(v1, 0.1f, color);
worldHandle.DrawCircle(v2, 0.1f, color);
}
}
break;
case PolygonShape poly:

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Robust.Client.WebViewHook;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;

View File

@@ -287,78 +287,6 @@ namespace Robust.Client
return true;
}
private ResourceManifestData LoadResourceManifest()
{
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
return ResourceManifestData.Default;
var yamlStream = new YamlStream();
using (stream)
{
using var streamReader = new StreamReader(stream, EncodingHelpers.UTF8);
yamlStream.Load(streamReader);
}
if (yamlStream.Documents.Count == 0)
return ResourceManifestData.Default;
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
{
throw new InvalidOperationException(
"Expected a single YAML document with root mapping for /manifest.yml");
}
var modules = ReadStringArray(mapping, "modules") ?? Array.Empty<string>();
string? assemblyPrefix = null;
if (mapping.TryGetNode("assemblyPrefix", out var prefixNode))
assemblyPrefix = prefixNode.AsString();
string? defaultWindowTitle = null;
if (mapping.TryGetNode("defaultWindowTitle", out var winTitleNode))
defaultWindowTitle = winTitleNode.AsString();
string? windowIconSet = null;
if (mapping.TryGetNode("windowIconSet", out var iconSetNode))
windowIconSet = iconSetNode.AsString();
string? splashLogo = null;
if (mapping.TryGetNode("splashLogo", out var splashNode))
splashLogo = splashNode.AsString();
bool autoConnect = true;
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
autoConnect = autoConnectNode.AsBool();
var clientAssemblies = ReadStringArray(mapping, "clientAssemblies");
return new ResourceManifestData(
modules,
assemblyPrefix,
defaultWindowTitle,
windowIconSet,
splashLogo,
autoConnect,
clientAssemblies
);
static string[]? ReadStringArray(YamlMappingNode mapping, string key)
{
if (!mapping.TryGetNode(key, out var node))
return null;
var sequence = (YamlSequenceNode)node;
var array = new string[sequence.Children.Count];
for (var i = 0; i < array.Length; i++)
{
array[i] = sequence[i].AsString();
}
return array;
}
}
internal bool StartupSystemSplash(
GameControllerOptions options,
Func<ILogHandler>? logHandlerFactory,
@@ -457,7 +385,7 @@ namespace Robust.Client
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = LoadResourceManifest();
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -704,7 +632,6 @@ namespace Robust.Client
logManager.GetSawmill("ogl.debug.other").Level = LogLevel.Warning;
logManager.GetSawmill("gdparse").Level = LogLevel.Error;
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Warning;
@@ -786,20 +713,6 @@ namespace Robust.Client
_clydeAudio.Shutdown();
}
private sealed record ResourceManifestData(
string[] Modules,
string? AssemblyPrefix,
string? DefaultWindowTitle,
string? WindowIconSet,
string? SplashLogo,
bool AutoConnect,
string[]? ClientAssemblies
)
{
public static readonly ResourceManifestData Default =
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true, null);
}
public event Action<FrameEventArgs>? TickUpdateOverride;
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class ClientEntityManager
{
protected override NetEntity GenerateNetEntity() => new(NextNetworkId++ | NetEntity.ClientEntity);
/// <summary>
/// If the client fails to resolve a NetEntity then during component state handling or the likes we
/// flag that comp state as requiring re-running if that NetEntity comes in.
/// </summary>
/// <returns></returns>
internal readonly Dictionary<NetEntity, List<(Type type, EntityUid Owner)>> PendingNetEntityStates = new();
public override bool IsClientSide(EntityUid uid, MetaDataComponent? metadata = null)
{
// Can't log false because some content code relies on invalid UIDs.
if (!MetaQuery.Resolve(uid, ref metadata, false))
return false;
return metadata.NetEntity.IsClientSide();
}
public override EntityUid EnsureEntity<T>(NetEntity nEntity, EntityUid callerEntity)
{
if (!nEntity.Valid)
{
return EntityUid.Invalid;
}
if (NetEntityLookup.TryGetValue(nEntity, out var entity))
{
return entity.Item1;
}
// Flag the callerEntity to have their state potentially re-run later.
var pending = PendingNetEntityStates.GetOrNew(nEntity);
pending.Add((typeof(T), callerEntity));
return entity.Item1;
}
public override EntityCoordinates EnsureCoordinates<T>(NetCoordinates netCoordinates, EntityUid callerEntity)
{
var entity = EnsureEntity<T>(netCoordinates.NetEntity, callerEntity);
return new EntityCoordinates(entity, netCoordinates.Position);
}
}

View File

@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Replays;
@@ -18,7 +16,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
public sealed class ClientEntityManager : EntityManager, IClientEntityManagerInternal
public sealed partial class ClientEntityManager : EntityManager, IClientEntityManagerInternal
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
@@ -27,8 +25,6 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
public override void Initialize()
{
SetupNetworking();
@@ -39,13 +35,16 @@ namespace Robust.Client.GameObjects
public override void FlushEntities()
{
// Server doesn't network deletions on client shutdown so we need to
// manually clear these out or risk stale data getting used.
PendingNetEntityStates.Clear();
using var _ = _gameTiming.StartStateApplicationArea();
base.FlushEntities();
}
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, EntityUid uid)
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
{
return base.CreateEntity(prototypeName, uid);
return base.CreateEntity(prototypeName, out metadata);
}
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
@@ -66,9 +65,12 @@ namespace Robust.Client.GameObjects
base.DirtyEntity(uid, meta);
}
public override void QueueDeleteEntity(EntityUid uid)
public override void QueueDeleteEntity(EntityUid? uid)
{
if (uid.IsClientSide())
if (uid == null)
return;
if (IsClientSide(uid.Value))
{
base.QueueDeleteEntity(uid);
return;
@@ -79,23 +81,29 @@ namespace Robust.Client.GameObjects
// Client-side entity deletion is not supported and will cause errors.
if (_client.RunLevel == ClientRunLevel.Connected || _client.RunLevel == ClientRunLevel.InGame)
LogManager.RootSawmill.Error($"Predicting the queued deletion of a networked entity: {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
LogManager.RootSawmill.Error($"Predicting the queued deletion of a networked entity: {ToPrettyString(uid.Value)}. Trace: {Environment.StackTrace}");
}
/// <inheritdoc />
public override void Dirty(EntityUid uid, Component component, MetaDataComponent? meta = null)
public override void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
{
Dirty(new Entity<IComponent>(uid, component), meta);
}
/// <inheritdoc />
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(uid, component, meta);
base.Dirty(ent, meta);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
{
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
else
return base.ToPrettyString(uid);
return base.ToPrettyString(uid);
}
public override void RaisePredictiveEvent<T>(T msg)
@@ -162,7 +170,7 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
{
throw new NotSupportedException();
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using static Robust.Client.Animations.AnimationPlaybackShared;
namespace Robust.Client.GameObjects
@@ -21,42 +19,5 @@ namespace Robust.Client.GameObjects
= new();
internal bool HasPlayingAnimation = false;
/// <summary>
/// Start playing an animation.
/// </summary>
/// <param name="animation">The animation to play.</param>
/// <param name="key">
/// The key for this animation play. This key can be used to stop playback short later.
/// </param>
[Obsolete("Use AnimationPlayerSystem.Play() instead")]
public void Play(Animation animation, string key)
{
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AnimationPlayerSystem>().AddComponent(this);
var playback = new AnimationPlayback(animation);
PlayingAnimations.Add(key, playback);
}
[Obsolete("Use AnimationPlayerSystem.HasRunningAnimation() instead")]
public bool HasRunningAnimation(string key)
{
return PlayingAnimations.ContainsKey(key);
}
[Obsolete("Use AnimationPlayerSystem.Stop() instead")]
public void Stop(string key)
{
PlayingAnimations.Remove(key);
}
[Obsolete("Temporary method until the event is replaced with eventbus")]
internal void AnimationComplete(string key)
{
AnimationCompleted?.Invoke(key);
}
[Obsolete("Use AnimationCompletedEvent instead")]
public event Action<string>? AnimationCompleted;
}
}

View File

@@ -1,136 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[RegisterComponent, ComponentReference(typeof(SharedEyeComponent))]
public sealed partial class EyeComponent : SharedEyeComponent
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] internal Eye? _eye = default!;
// Horrible hack to get around ordering issues.
internal bool _setCurrentOnInitialize;
[DataField("drawFov")] internal bool _setDrawFovOnInitialize = true;
[DataField("zoom")] internal Vector2 _setZoomOnInitialize = Vector2.One;
/// <summary>
/// If not null, this entity is used to update the eye's position instead of just using the component's owner.
/// </summary>
/// <remarks>
/// This is useful for things like vehicles that effectively need to hijack the eye. This allows them to do
/// that without messing with the main viewport's eye. This is important as there are some overlays that are
/// only be drawn if that viewport's eye belongs to the currently controlled entity.
/// </remarks>
[ViewVariables]
public EntityUid? Target;
public IEye? Eye => _eye;
[ViewVariables(VVAccess.ReadWrite)]
public bool Current
{
get => _eyeManager.CurrentEye == _eye;
set
{
if (_eye == null)
{
_setCurrentOnInitialize = value;
return;
}
if (_eyeManager.CurrentEye == _eye == value)
return;
if (value)
{
_eyeManager.CurrentEye = _eye;
}
else
{
_eyeManager.ClearCurrentEye();
}
}
}
public override Vector2 Zoom
{
get => _eye?.Zoom ?? _setZoomOnInitialize;
set
{
if (_eye == null)
{
_setZoomOnInitialize = value;
}
else
{
_eye.Zoom = value;
}
}
}
public override Angle Rotation
{
get => _eye?.Rotation ?? Angle.Zero;
set
{
if (_eye != null)
_eye.Rotation = value;
}
}
public override Vector2 Offset
{
get => _eye?.Offset ?? default;
set
{
if (_eye != null)
_eye.Offset = value;
}
}
public override bool DrawFov
{
get => _eye?.DrawFov ?? _setDrawFovOnInitialize;
set
{
if (_eye == null)
{
_setDrawFovOnInitialize = value;
}
else
{
_eye.DrawFov = value;
}
}
}
[ViewVariables]
public MapCoordinates? Position => _eye?.Position;
/// <summary>
/// Updates the Eye of this entity with the transform position. This has to be called every frame to
/// keep the view following the entity.
/// </summary>
public void UpdateEyePosition()
{
if (_eye == null) return;
if (!_entityManager.TryGetComponent(Target, out TransformComponent? xform))
{
xform = _entityManager.GetComponent<TransformComponent>(Owner);
Target = null;
}
_eye.Position = xform.MapPosition;
}
}
}

View File

@@ -1,85 +0,0 @@
using Robust.Client.Graphics;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[RegisterComponent]
[ComponentReference(typeof(SharedPointLightComponent))]
public sealed partial class PointLightComponent : SharedPointLightComponent, IComponentTreeEntry<PointLightComponent>
{
public EntityUid? TreeUid { get; set; }
public DynamicTree<ComponentTreeEntry<PointLightComponent>>? Tree { get; set; }
public bool AddToTree => Enabled && !ContainerOccluded;
public bool TreeUpdateQueued { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public override Color Color
{
get => _color;
set => base.Color = value;
}
[Access(typeof(PointLightSystem))]
public bool ContainerOccluded;
/// <summary>
/// Determines if the light mask should automatically rotate with the entity. (like a flashlight)
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool MaskAutoRotate
{
get => _maskAutoRotate;
set => _maskAutoRotate = value;
}
/// <summary>
/// Local rotation of the light mask around the center origin
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public Angle Rotation
{
get => _rotation;
set => _rotation = value;
}
/// <summary>
/// The resource path to the mask texture the light will use.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public string? MaskPath
{
get => _maskPath;
set
{
if (_maskPath?.Equals(value) != false) return;
_maskPath = value;
EntitySystem.Get<PointLightSystem>().UpdateMask(this);
}
}
/// <summary>
/// Set a mask texture that will be applied to the light while rendering.
/// The mask's red channel will be linearly multiplied.p
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Texture? Mask { get; set; }
[DataField("autoRot")]
private bool _maskAutoRotate;
private Angle _rotation;
[DataField("mask")]
internal string? _maskPath;
}
}

View File

@@ -0,0 +1,38 @@
using Robust.Client.Graphics;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects;
[RegisterComponent]
public sealed partial class PointLightComponent : SharedPointLightComponent, IComponentTreeEntry<PointLightComponent>
{
#region Component Tree
/// <inheritdoc />
[ViewVariables]
public EntityUid? TreeUid { get; set; }
/// <inheritdoc />
[ViewVariables]
public DynamicTree<ComponentTreeEntry<PointLightComponent>>? Tree { get; set; }
/// <inheritdoc />
[ViewVariables]
public bool AddToTree => Enabled && !ContainerOccluded;
/// <inheritdoc />
[ViewVariables]
public bool TreeUpdateQueued { get; set; }
#endregion
/// <summary>
/// Set a mask texture that will be applied to the light while rendering.
/// The mask's red channel will be linearly multiplied.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
internal Texture? Mask;
}

View File

@@ -1,5 +1,7 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
@@ -24,7 +26,7 @@ namespace Robust.Client.GameObjects
int AnimationFrame { get; }
bool AutoAnimated { get; set; }
RSI.State.Direction EffectiveDirection(Angle worldRotation);
RsiDirection EffectiveDirection(Angle worldRotation);
/// <summary>
/// Layer size in pixels.

View File

@@ -12,6 +12,8 @@ using Robust.Shared;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -26,8 +28,8 @@ using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using static Robust.Client.ComponentTrees.SpriteTreeSystem;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using RSIDirection = Robust.Client.Graphics.RSI.State.Direction;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
namespace Robust.Client.GameObjects
{
@@ -1347,11 +1349,11 @@ namespace Robust.Client.GameObjects
state = GetFallbackState(resourceCache);
}
return state.Directions switch
return state.RsiDirections switch
{
RSI.State.DirectionType.Dir1 => 1,
RSI.State.DirectionType.Dir4 => 4,
RSI.State.DirectionType.Dir8 => 8,
RsiDirectionType.Dir1 => 1,
RsiDirectionType.Dir4 => 4,
RsiDirectionType.Dir8 => 8,
_ => throw new ArgumentOutOfRangeException()
};
}
@@ -1389,7 +1391,7 @@ namespace Robust.Client.GameObjects
builder.AppendFormat(
"vis/depth/scl/rot/ofs/col/norot/override/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{8}/{7}\n",
Visible, DrawDepth, Scale, Rotation, Offset,
Color, NoRotation, entities.GetComponent<TransformComponent>(Owner).WorldRotation.ToRsiDirection(RSI.State.DirectionType.Dir8),
Color, NoRotation, entities.GetComponent<TransformComponent>(Owner).WorldRotation.ToRsiDirection(RsiDirectionType.Dir8),
DirectionOverride
);
@@ -1689,7 +1691,7 @@ namespace Robust.Client.GameObjects
int ISpriteLayer.AnimationFrame => AnimationFrame;
public RSIDirection EffectiveDirection(Angle worldRotation)
public RsiDirection EffectiveDirection(Angle worldRotation)
{
if (State == default)
{
@@ -1710,23 +1712,23 @@ namespace Robust.Client.GameObjects
return default;
}
public RSIDirection EffectiveDirection(RSI.State state, Angle worldRotation,
public RsiDirection EffectiveDirection(RSI.State state, Angle worldRotation,
Direction? overrideDirection)
{
if (state.Directions == RSI.State.DirectionType.Dir1)
if (state.RsiDirections == RsiDirectionType.Dir1)
{
return RSIDirection.South;
return RsiDirection.South;
}
else
{
RSIDirection dir;
RsiDirection dir;
if (overrideDirection != null)
{
dir = overrideDirection.Value.Convert(state.Directions);
dir = overrideDirection.Value.Convert(state.RsiDirections);
}
else
{
dir = worldRotation.ToRsiDirection(state.Directions);
dir = worldRotation.ToRsiDirection(state.RsiDirections);
}
return dir.OffsetRsiDir(DirOffset);
@@ -1882,20 +1884,20 @@ namespace Robust.Client.GameObjects
else if (_parent.SnapCardinals && (!_parent.GranularLayersRendering || RenderingStrategy == LayerRenderingStrategy.UseSpriteStrategy)
|| _parent.GranularLayersRendering && RenderingStrategy == LayerRenderingStrategy.SnapToCardinals)
{
DebugTools.Assert(_actualState == null || _actualState.Directions == RSI.State.DirectionType.Dir1);
DebugTools.Assert(_actualState == null || _actualState.RsiDirections == RsiDirectionType.Dir1);
size = new Vector2(longestSide, longestSide);
}
else
{
// Build the bounding box based on how many directions the sprite has
size = (_actualState?.Directions) switch
size = (_actualState?.RsiDirections) switch
{
// If we have four cardinal directions, take the longest side of our texture and square it, then turn that into our bounding box.
// This accounts for all possible rotations.
RSI.State.DirectionType.Dir4 => new Vector2(longestSide, longestSide),
RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide),
// If we have eight directions, find the maximum length of the texture (accounting for rotation), then square it to make
RSI.State.DirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
// If we have only one direction or an invalid RSI state, create a simple bounding box with the size of the texture.
_ => textureSize
@@ -1929,9 +1931,9 @@ namespace Robust.Client.GameObjects
/// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing &
/// relevant RSI direction.
/// </summary>
public void GetLayerDrawMatrix(RSIDirection dir, out Matrix3 layerDrawMatrix)
public void GetLayerDrawMatrix(RsiDirection dir, out Matrix3 layerDrawMatrix)
{
if (_parent.NoRotation || dir == RSIDirection.South)
if (_parent.NoRotation || dir == RsiDirection.South)
layerDrawMatrix = LocalMatrix;
else
{
@@ -1956,11 +1958,11 @@ namespace Robust.Client.GameObjects
/// Converts an angle (between 0 and 2pi) to an RSI direction. This will slightly bias the angle to avoid flickering for
/// 4-directional sprites.
/// </summary>
public static RSIDirection GetDirection(RSI.State.DirectionType dirType, Angle angle)
public static RsiDirection GetDirection(RsiDirectionType dirType, Angle angle)
{
if (dirType == RSI.State.DirectionType.Dir1)
return RSIDirection.South;
else if (dirType == RSI.State.DirectionType.Dir8)
if (dirType == RsiDirectionType.Dir1)
return RsiDirection.South;
else if (dirType == RsiDirectionType.Dir8)
return angle.GetDir().Convert(dirType);
// For 4-directional sprites, as entities are often moving & facing diagonally, we will slightly bias the
@@ -1973,10 +1975,10 @@ namespace Robust.Client.GameObjects
return ((int)Math.Round(modTheta / MathHelper.PiOver2) % 4) switch
{
0 => RSIDirection.South,
1 => RSIDirection.East,
2 => RSIDirection.North,
_ => RSIDirection.West,
0 => RsiDirection.South,
1 => RsiDirection.East,
2 => RsiDirection.North,
_ => RsiDirection.West,
};
}
@@ -1988,7 +1990,7 @@ namespace Robust.Client.GameObjects
if (!Visible || Blank)
return;
var dir = _actualState == null ? RSIDirection.South : GetDirection(_actualState.Directions, angle);
var dir = _actualState == null ? RsiDirection.South : GetDirection(_actualState.RsiDirections, angle);
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
@@ -1998,7 +2000,7 @@ namespace Robust.Client.GameObjects
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
if (overrideDirection != null && _actualState != null)
dir = overrideDirection.Value.Convert(_actualState.Directions);
dir = overrideDirection.Value.Convert(_actualState.RsiDirections);
dir = dir.OffsetRsiDir(DirOffset);
// Get the correct directional texture from the state, and draw it!
@@ -2021,7 +2023,7 @@ namespace Robust.Client.GameObjects
drawingHandle.UseShader(null);
}
private Texture GetRenderTexture(RSI.State? state, RSIDirection dir)
private Texture GetRenderTexture(RSI.State? state, RsiDirection dir)
{
if (state == null)
return Texture ?? _parent.resourceCache.GetFallback<TextureResource>().Texture;

View File

@@ -1,23 +1,19 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using TerraFX.Interop.Windows;
namespace Robust.Client.GameObjects
{
public sealed class AnimationPlayerSystem : EntitySystem, IPostInjectInit
public sealed class AnimationPlayerSystem : EntitySystem
{
private readonly List<AnimationPlayerComponent> _activeAnimations = new();
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
private EntityQuery<MetaDataComponent> _metaQuery;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
@@ -39,22 +35,22 @@ namespace Robust.Client.GameObjects
continue;
}
if (!Update(uid, anim, frameTime))
if (!Update(uid, anim.Comp, frameTime))
{
continue;
}
_activeAnimations.RemoveSwap(i);
i--;
anim.HasPlayingAnimation = false;
anim.Comp.HasPlayingAnimation = false;
}
}
internal void AddComponent(AnimationPlayerComponent component)
internal void AddComponent(Entity<AnimationPlayerComponent> ent)
{
if (component.HasPlayingAnimation) return;
_activeAnimations.Add(component);
component.HasPlayingAnimation = true;
if (ent.Comp.HasPlayingAnimation) return;
_activeAnimations.Add(ent);
ent.Comp.HasPlayingAnimation = true;
}
private bool Update(EntityUid uid, AnimationPlayerComponent component, float frameTime)
@@ -79,7 +75,6 @@ namespace Robust.Client.GameObjects
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
component.AnimationComplete(key);
}
return false;
@@ -90,22 +85,29 @@ namespace Robust.Client.GameObjects
/// </summary>
public void Play(EntityUid uid, Animation animation, string key)
{
var component = EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(component, animation, key);
var component = EnsureComp<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(component, animation, key);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
/// <summary>
/// Start playing an animation.
/// </summary>
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(AnimationPlayerComponent component, Animation animation, string key)
{
AddComponent(component);
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
}
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
{
AddComponent(ent);
var playback = new AnimationPlaybackShared.AnimationPlayback(animation);
#if DEBUG
@@ -117,18 +119,18 @@ namespace Robust.Client.GameObjects
if (compTrack.ComponentType == null)
{
_sawmill.Error($"Attempted to play a component animation without any component specified.");
Log.Error("Attempted to play a component animation without any component specified.");
return;
}
if (!EntityManager.TryGetComponent(component.Owner, compTrack.ComponentType, out var animatedComp))
if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp))
{
_sawmill.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(component.Owner)} does not have the component to be animated: {compTrack.ComponentType}.");
Log.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}.");
return;
}
if (component.Owner.IsClientSide() || !animatedComp.NetSyncEnabled)
if (IsClientSide(ent) || !animatedComp.NetSyncEnabled)
continue;
var reg = _compFact.GetRegistration(animatedComp);
@@ -136,12 +138,18 @@ namespace Robust.Client.GameObjects
// In principle there is nothing wrong with this, as long as the property of the component being
// animated is not part of the networked state and setting it does not dirty the component. Hence only a
// warning in debug mode.
if (reg.NetID != null)
_sawmill.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(component.Owner)}");
if (reg.NetID != null && compTrack.Property != null)
{
if (animatedComp.GetType().GetProperty(compTrack.Property) is { } property &&
property.HasCustomAttribute<AutoNetworkedFieldAttribute>())
{
Log.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}");
}
}
}
#endif
component.PlayingAnimations.Add(key, playback);
ent.Comp.PlayingAnimations.Add(key, playback);
}
public bool HasRunningAnimation(EntityUid uid, string key)
@@ -170,19 +178,18 @@ namespace Robust.Client.GameObjects
public void Stop(EntityUid uid, string key)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var player)) return;
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
return;
player.PlayingAnimations.Remove(key);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
{
if (!Resolve(uid, ref component, false)) return;
component.PlayingAnimations.Remove(key);
}
if (!Resolve(uid, ref component, false))
return;
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("anim");
component.PlayingAnimations.Remove(key);
}
}

View File

@@ -17,7 +17,6 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
@@ -81,9 +80,13 @@ public sealed class AudioSystem : SharedAudioSystem
#region Event Handlers
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
{
var stream = EntityManager.EntityExists(ev.EntityUid)
? (PlayingStream?) Play(ev.FileName, ev.EntityUid, ev.FallbackCoordinates, ev.AudioParams, false)
: (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams, false);
var uid = GetEntity(ev.NetEntity);
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = EntityManager.EntityExists(uid)
? (PlayingStream?) Play(ev.FileName, uid, fallback, ev.AudioParams, false)
: (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
@@ -98,7 +101,10 @@ public sealed class AudioSystem : SharedAudioSystem
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
{
var stream = (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams, false);
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
@@ -383,8 +389,8 @@ public sealed class AudioSystem : SharedAudioSystem
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
EntityUid = entity,
FallbackCoordinates = fallbackCoordinates ?? default,
NetEntity = GetNetEntity(entity),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates) ?? default,
AudioParams = audioParams ?? AudioParams.Default
});
}
@@ -437,8 +443,8 @@ public sealed class AudioSystem : SharedAudioSystem
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = coordinates,
FallbackCoordinates = fallbackCoordinates,
Coordinates = GetNetCoordinates(coordinates),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -5,13 +6,11 @@ using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Physics.Components;
using Robust.Shared.Serialization;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -23,14 +22,20 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
[Dependency] private readonly PointLightSystem _lightSys = default!;
private EntityQuery<PointLightComponent> _pointLightQuery;
private EntityQuery<SpriteComponent> _spriteQuery;
private readonly HashSet<EntityUid> _updateQueue = new();
public readonly Dictionary<EntityUid, IContainer> ExpectedEntities = new();
public readonly Dictionary<NetEntity, BaseContainer> ExpectedEntities = new();
public override void Initialize()
{
base.Initialize();
_pointLightQuery = GetEntityQuery<PointLightComponent>();
_spriteQuery = GetEntityQuery<SpriteComponent>();
EntityManager.EntityInitialized += HandleEntityInitialized;
SubscribeLocalEvent<ContainerManagerComponent, ComponentHandleState>(HandleComponentState);
@@ -43,20 +48,18 @@ namespace Robust.Client.GameObjects
base.Shutdown();
}
protected override void ValidateMissingEntity(EntityUid uid, IContainer cont, EntityUid missing)
protected override void ValidateMissingEntity(EntityUid uid, BaseContainer cont, EntityUid missing)
{
DebugTools.Assert(ExpectedEntities.TryGetValue(missing, out var expectedContainer) && expectedContainer == cont && cont.ExpectedEntities.Contains(missing));
var netEntity = GetNetEntity(missing);
DebugTools.Assert(ExpectedEntities.TryGetValue(netEntity, out var expectedContainer) && expectedContainer == cont && cont.ExpectedEntities.Contains(netEntity));
}
private void HandleEntityInitialized(EntityUid uid)
{
if (!RemoveExpectedEntity(uid, out var container))
if (!RemoveExpectedEntity(GetNetEntity(uid), out var container))
return;
if (container.Deleted)
return;
container.Insert(uid);
container.Insert(uid, EntityManager, transform: TransformQuery.GetComponent(uid), meta: MetaQuery.GetComponent(uid));
}
private void HandleComponentState(EntityUid uid, ContainerManagerComponent component, ref ComponentHandleState args)
@@ -64,23 +67,24 @@ namespace Robust.Client.GameObjects
if (args.Current is not ContainerManagerComponentState cast)
return;
var metaQuery = GetEntityQuery<MetaDataComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var xform = xformQuery.GetComponent(uid);
var xform = TransformQuery.GetComponent(uid);
// Delete now-gone containers.
var toDelete = new ValueList<string>();
foreach (var (id, container) in component.Containers)
{
if (cast.Containers.ContainsKey(id))
{
DebugTools.Assert(cast.Containers[id].ContainerType == container.GetType().Name);
continue;
}
foreach (var entity in container.ContainedEntities.ToArray())
{
container.Remove(entity,
EntityManager,
xformQuery.GetComponent(entity),
metaQuery.GetComponent(entity),
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
force: true,
reparent: false);
@@ -98,26 +102,32 @@ namespace Robust.Client.GameObjects
// Add new containers and update existing contents.
foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.Containers.Values)
foreach (var (id, data) in cast.Containers)
{
if (!component.Containers.TryGetValue(id, out var container))
{
container = ContainerFactory(component, containerType, id);
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
container.Init(id, uid, component);
component.Containers.Add(id, container);
}
// sync show flag
container.ShowContents = showEnts;
container.OccludesLight = occludesLight;
DebugTools.Assert(container.ID == id);
container.ShowContents = data.ShowContents;
container.OccludesLight = data.OccludesLight;
// Remove gone entities.
var toRemove = new ValueList<EntityUid>();
DebugTools.Assert(!container.Contains(EntityUid.Invalid));
var stateNetEnts = data.ContainedEntities;
var stateEnts = GetEntityArray(stateNetEnts); // No need to ensure entities.
foreach (var entity in container.ContainedEntities)
{
if (!entityUids.Contains(entity))
{
if (!stateEnts.Contains(entity))
toRemove.Add(entity);
}
}
foreach (var entity in toRemove)
@@ -125,8 +135,8 @@ namespace Robust.Client.GameObjects
container.Remove(
entity,
EntityManager,
xformQuery.GetComponent(entity),
metaQuery.GetComponent(entity),
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
force: true,
reparent: false);
@@ -134,13 +144,11 @@ namespace Robust.Client.GameObjects
}
// Remove entities that were expected, but have been removed from the container.
var removedExpected = new ValueList<EntityUid>();
foreach (var entityUid in container.ExpectedEntities)
var removedExpected = new ValueList<NetEntity>();
foreach (var netEntity in container.ExpectedEntities)
{
if (!entityUids.Contains(entityUid))
{
removedExpected.Add(entityUid);
}
if (!stateNetEnts.Contains(netEntity))
removedExpected.Add(netEntity);
}
foreach (var entityUid in removedExpected)
@@ -149,14 +157,20 @@ namespace Robust.Client.GameObjects
}
// Add new entities.
foreach (var entity in entityUids)
for (var i = 0; i < stateNetEnts.Length; i++)
{
if (!EntityManager.TryGetComponent(entity, out MetaDataComponent? meta))
var entity = stateEnts[i];
var netEnt = stateNetEnts[i];
if (!entity.IsValid())
{
AddExpectedEntity(entity, container);
DebugTools.Assert(netEnt.IsValid());
AddExpectedEntity(netEnt, container);
continue;
}
var meta = MetaData(entity);
DebugTools.Assert(meta.NetEntity == netEnt);
// If an entity is currently in the shadow realm, it means we probably left PVS and are now getting
// back into range. We do not want to directly insert this entity, as IF the container and entity
// transform states did not get sent simultaneously, the entity's transform will be modified by the
@@ -166,18 +180,18 @@ namespace Robust.Client.GameObjects
// containers/players.
if ((meta.Flags & MetaDataFlags.Detached) != 0)
{
AddExpectedEntity(entity, container);
AddExpectedEntity(netEnt, container);
continue;
}
if (container.Contains(entity))
continue;
RemoveExpectedEntity(entity, out _);
RemoveExpectedEntity(netEnt, out _);
container.Insert(entity, EntityManager,
xformQuery.GetComponent(entity),
TransformQuery.GetComponent(entity),
xform,
metaQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
force: true);
DebugTools.Assert(container.Contains(entity));
@@ -198,7 +212,7 @@ namespace Robust.Client.GameObjects
if (message.OldParent != null && message.OldParent.Value.IsValid())
return;
if (!RemoveExpectedEntity(message.Entity, out var container))
if (!RemoveExpectedEntity(GetNetEntity(message.Entity), out var container))
return;
if (xform.ParentUid != container.Owner)
@@ -208,84 +222,69 @@ namespace Robust.Client.GameObjects
return;
}
if (container.Deleted)
return;
container.Insert(message.Entity, EntityManager);
}
private IContainer ContainerFactory(ContainerManagerComponent component, string containerType, string id)
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
{
var type = _serializer.FindSerializedType(typeof(IContainer), containerType);
if (type is null) throw new ArgumentException($"Container of type {containerType} for id {id} cannot be found.");
#if DEBUG
var uid = GetEntity(netEntity);
var newContainer = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type);
newContainer.ID = id;
newContainer.Manager = component;
return newContainer;
}
if (TryComp<MetaDataComponent>(uid, out var meta))
{
DebugTools.Assert((meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");
}
#endif
public void AddExpectedEntity(EntityUid uid, IContainer container)
{
DebugTools.Assert(!TryComp(uid, out MetaDataComponent? meta) ||
(meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");
if (!ExpectedEntities.TryAdd(uid, container))
if (!ExpectedEntities.TryAdd(netEntity, container))
{
// It is possible that we were expecting this entity in one container, but it has now moved to another
// container, and this entity's state is just being applied before the old container is getting updated.
var oldContainer = ExpectedEntities[uid];
ExpectedEntities[uid] = container;
DebugTools.Assert(oldContainer.ExpectedEntities.Contains(uid),
$"Entity {ToPrettyString(uid)} is expected, but not expected in the given container? Container: {oldContainer.ID} in {ToPrettyString(oldContainer.Owner)}");
oldContainer.ExpectedEntities.Remove(uid);
var oldContainer = ExpectedEntities[netEntity];
ExpectedEntities[netEntity] = container;
DebugTools.Assert(oldContainer.ExpectedEntities.Contains(netEntity),
$"Entity {netEntity} is expected, but not expected in the given container? Container: {oldContainer.ID} in {ToPrettyString(oldContainer.Owner)}");
oldContainer.ExpectedEntities.Remove(netEntity);
}
DebugTools.Assert(!container.ExpectedEntities.Contains(uid),
$"Contained entity {ToPrettyString(uid)} was not yet expected by the system, but was already expected by the container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Add(uid);
DebugTools.Assert(!container.ExpectedEntities.Contains(netEntity),
$"Contained entity {netEntity} was not yet expected by the system, but was already expected by the container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Add(netEntity);
}
public bool RemoveExpectedEntity(EntityUid uid, [NotNullWhen(true)] out IContainer? container)
public bool RemoveExpectedEntity(NetEntity netEntity, [NotNullWhen(true)] out BaseContainer? container)
{
if (!ExpectedEntities.Remove(uid, out container))
if (!ExpectedEntities.Remove(netEntity, out container))
return false;
DebugTools.Assert(container.ExpectedEntities.Contains(uid),
$"While removing expected contained entity {ToPrettyString(uid)}, the entity was missing from the container expected set. Container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Remove(uid);
DebugTools.Assert(container.ExpectedEntities.Contains(netEntity),
$"While removing expected contained entity {ToPrettyString(netEntity)}, the entity was missing from the container expected set. Container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Remove(netEntity);
return true;
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
foreach (var toUpdate in _updateQueue)
{
if (Deleted(toUpdate))
continue;
UpdateEntityRecursively(toUpdate, xformQuery, pointQuery, spriteQuery);
UpdateEntityRecursively(toUpdate);
}
_updateQueue.Clear();
}
private void UpdateEntityRecursively(
EntityUid entity,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery)
private void UpdateEntityRecursively(EntityUid entity)
{
// Recursively go up parents and containers to see whether both sprites and lights need to be occluded
// Could maybe optimise this more by checking nearest parent that has sprite / light and whether it's container
// occluded but this probably isn't a big perf issue.
var xform = xformQuery.GetComponent(entity);
var xform = TransformQuery.GetComponent(entity);
var parent = xform.ParentUid;
var child = entity;
var spriteOccluded = false;
@@ -293,7 +292,7 @@ namespace Robust.Client.GameObjects
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
{
var parentXform = xformQuery.GetComponent(parent);
var parentXform = TransformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
@@ -308,24 +307,21 @@ namespace Robust.Client.GameObjects
// This is the CBT bit.
// The issue is we need to go through the children and re-check whether they are or are not contained.
// if they are contained then the occlusion values may need updating for all those children
UpdateEntity(entity, xform, xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
UpdateEntity(entity, xform, spriteOccluded, lightOccluded);
}
private void UpdateEntity(
EntityUid entity,
TransformComponent xform,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery,
bool spriteOccluded,
bool lightOccluded)
{
if (spriteQuery.TryGetComponent(entity, out var sprite))
if (_spriteQuery.TryGetComponent(entity, out var sprite))
{
sprite.ContainerOccluded = spriteOccluded;
}
if (pointQuery.TryGetComponent(entity, out var light))
if (_pointLightQuery.TryGetComponent(entity, out var light))
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
var childEnumerator = xform.ChildEnumerator;
@@ -346,14 +342,14 @@ namespace Robust.Client.GameObjects
childLightOccluded = childLightOccluded || container.OccludesLight;
}
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, childSpriteOccluded, childLightOccluded);
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), childSpriteOccluded, childLightOccluded);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
{
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), spriteOccluded, lightOccluded);
}
}
}

View File

@@ -1,6 +1,8 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
namespace Robust.Client.GameObjects;
@@ -13,8 +15,35 @@ public sealed class EyeSystem : SharedEyeSystem
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<EyeComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<EyeComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<EyeComponent, LocalPlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, LocalPlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, AfterAutoHandleStateEvent>(OnEyeAutoState);
// Make sure this runs *after* entities have been moved by interpolation and movement.
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
}
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateEye(component);
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
{
_eyeManager.CurrentEye = component._eye;
}
var ev = new EyeAttachedEvent(uid, component);
RaiseLocalEvent(uid, ref ev, true);
}
private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDetachedEvent args)
{
_eyeManager.ClearCurrentEye();
}
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
@@ -22,39 +51,35 @@ public sealed class EyeSystem : SharedEyeSystem
component._eye = new Eye
{
Position = Transform(uid).MapPosition,
Zoom = component._setZoomOnInitialize,
DrawFov = component._setDrawFovOnInitialize
Zoom = component.Zoom,
DrawFov = component.DrawFov,
Rotation = component.Rotation,
};
if ((_eyeManager.CurrentEye == component._eye) != component._setCurrentOnInitialize)
{
if (component._setCurrentOnInitialize)
{
_eyeManager.ClearCurrentEye();
}
else
{
_eyeManager.CurrentEye = component._eye;
}
}
}
private void OnRemove(EntityUid uid, EyeComponent component, ComponentRemove args)
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
component.Current = false;
}
var query = AllEntityQuery<EyeComponent>();
private void OnHandleState(EntityUid uid, EyeComponent component, ref ComponentHandleState args)
{
if (args.Current is not EyeComponentState state)
while (query.MoveNext(out var uid, out var eyeComponent))
{
return;
}
if (eyeComponent._eye == null)
continue;
component.DrawFov = state.DrawFov;
// TODO: Should be a way for content to override lerping and lerp the zoom
component.Zoom = state.Zoom;
component.Offset = state.Offset;
component.VisibilityMask = state.VisibilityMask;
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
{
xform = Transform(uid);
eyeComponent.Target = null;
}
eyeComponent._eye.Position = xform.MapPosition;
}
}
}
/// <summary>
/// Raised on an entity when it is attached to one with an <see cref="EyeComponent"/>
/// </summary>
[ByRefEvent]
public readonly record struct EyeAttachedEvent(EntityUid Entity, EyeComponent Component);

View File

@@ -1,43 +0,0 @@
using System;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
#nullable enable
namespace Robust.Client.GameObjects
{
/// <summary>
/// Updates the position of every Eye every frame, so that the camera follows the player around.
/// </summary>
[UsedImplicitly]
public sealed class EyeUpdateSystem : EntitySystem
{
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
// Make sure this runs *after* entities have been moved by interpolation and movement.
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
}
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
foreach (var eyeComponent in EntityManager.EntityQuery<EyeComponent>(true))
{
eyeComponent.UpdateEyePosition();
}
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -58,6 +59,8 @@ namespace Robust.Client.GameObjects
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private List<Entity<MapGridComponent>> _grids = new();
public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager)
{
_entityManager = entManager;
@@ -71,13 +74,15 @@ namespace Robust.Client.GameObjects
var viewport = args.WorldBounds;
var worldHandle = args.WorldHandle;
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
_grids.Clear();
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
foreach (var grid in _grids)
{
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid.Owner).WorldMatrix;
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid).WorldMatrix;
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
var chunkEnumerator = grid.GetMapChunks(viewport);
var chunkEnumerator = grid.Comp.GetMapChunks(viewport);
while (chunkEnumerator.MoveNext(out var chunk))
{

View File

@@ -3,16 +3,13 @@ using System.Numerics;
using Robust.Client.GameStates;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -52,7 +49,7 @@ namespace Robust.Client.GameObjects
/// <param name="message">Arguments for this event.</param>
/// <param name="replay">if true, current cmd state will not be checked or updated - use this for "replaying" an
/// old input that was saved or buffered until further processing could be done</param>
public bool HandleInputCommand(ICommonSession? session, BoundKeyFunction function, FullInputCmdMessage message, bool replay = false)
public bool HandleInputCommand(ICommonSession? session, BoundKeyFunction function, IFullInputCmdMessage message, bool replay = false)
{
#if DEBUG
@@ -78,14 +75,27 @@ namespace Robust.Client.GameObjects
continue;
// local handlers can block sending over the network.
if (handler.HandleCmdMessage(session, message))
if (handler.HandleCmdMessage(EntityManager, session, message))
{
return true;
}
}
// send it off to the server
DispatchInputCommand(message);
var clientMsg = (ClientFullInputCmdMessage)message;
var fullMsg = new FullInputCmdMessage(
clientMsg.Tick,
clientMsg.SubTick,
(int)clientMsg.InputSequence,
clientMsg.InputFunctionId,
clientMsg.State,
GetNetCoordinates(clientMsg.Coordinates),
clientMsg.ScreenCoordinates)
{
Uid = GetNetEntity(clientMsg.Uid)
};
DispatchInputCommand(clientMsg, fullMsg);
return false;
}
@@ -93,7 +103,7 @@ namespace Robust.Client.GameObjects
/// Handle a predicted input command.
/// </summary>
/// <param name="inputCmd">Input command to handle as predicted.</param>
public void PredictInputCommand(FullInputCmdMessage inputCmd)
public void PredictInputCommand(IFullInputCmdMessage inputCmd)
{
DebugTools.AssertNotNull(_playerManager.LocalPlayer);
@@ -103,21 +113,22 @@ namespace Robust.Client.GameObjects
var session = _playerManager.LocalPlayer!.Session;
foreach (var handler in BindRegistry.GetHandlers(keyFunc))
{
if (handler.HandleCmdMessage(session, inputCmd)) break;
if (handler.HandleCmdMessage(EntityManager, session, inputCmd))
break;
}
Predicted = false;
}
private void DispatchInputCommand(FullInputCmdMessage message)
private void DispatchInputCommand(ClientFullInputCmdMessage clientMsg, FullInputCmdMessage message)
{
_stateManager.InputCommandDispatched(message);
_stateManager.InputCommandDispatched(clientMsg, message);
EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, message.InputSequence);
}
public override void Initialize()
{
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
@@ -152,16 +163,16 @@ namespace Robust.Client.GameObjects
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
coords, new ScreenCoordinates(0, 0, default), EntityUid.Invalid);
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
{
if (message.AttachedEntity != default) // attach
if (message.Entity != default) // attach
{
SetEntityContextActive(_inputManager, message.AttachedEntity);
SetEntityContextActive(_inputManager, message.Entity);
}
else // detach
{
@@ -213,44 +224,4 @@ namespace Robust.Client.GameObjects
_sawmillInputContext = _logManager.GetSawmill("input.context");
}
}
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public sealed class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.
/// </summary>
public EntityUid AttachedEntity { get; }
/// <summary>
/// Creates a new instance of <see cref="PlayerAttachSysMessage"/>.
/// </summary>
/// <param name="attachedEntity">New entity the player is attached to.</param>
public PlayerAttachSysMessage(EntityUid attachedEntity)
{
AttachedEntity = attachedEntity;
}
}
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public PlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public PlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
}

View File

@@ -1,6 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Client.ComponentTrees;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -15,59 +17,108 @@ namespace Robust.Client.GameObjects
{
base.Initialize();
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<PointLightComponent, ComponentHandleState>(OnLightHandleState);
}
private void OnLightHandleState(EntityUid uid, PointLightComponent component, ref ComponentHandleState args)
{
if (args.Current is not PointLightComponentState state)
return;
component.Enabled = state.Enabled;
component.Offset = state.Offset;
component.Softness = state.Softness;
component.CastShadows = state.CastShadows;
component.Energy = state.Energy;
component.Radius = state.Radius;
component.Color = state.Color;
_lightTree.QueueTreeUpdate(uid, component);
}
public override SharedPointLightComponent EnsureLight(EntityUid uid)
{
return EnsureComp<PointLightComponent>(uid);
}
public override bool ResolveLight(EntityUid uid, [NotNullWhen(true)] ref SharedPointLightComponent? component)
{
if (component is not null)
return true;
TryComp<PointLightComponent>(uid, out var comp);
component = comp;
return component != null;
}
public override bool TryGetLight(EntityUid uid, [NotNullWhen(true)] out SharedPointLightComponent? component)
{
if (TryComp<PointLightComponent>(uid, out var comp))
{
component = comp;
return true;
}
component = null;
return false;
}
public override bool RemoveLightDeferred(EntityUid uid)
{
return RemCompDeferred<PointLightComponent>(uid);
}
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
{
UpdateMask(component);
SetMask(component.MaskPath, component);
}
internal void UpdateMask(PointLightComponent component)
public void SetMask(string? maskPath, PointLightComponent component)
{
if (component._maskPath is not null)
component.Mask = _resourceCache.GetResource<TextureResource>(component._maskPath);
if (maskPath is not null)
component.Mask = _resourceCache.GetResource<TextureResource>(maskPath);
else
component.Mask = null;
}
#region Setters
public void SetContainerOccluded(EntityUid uid, bool occluded, PointLightComponent? comp = null)
public void SetContainerOccluded(EntityUid uid, bool occluded, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || occluded == comp.ContainerOccluded)
if (!ResolveLight(uid, ref comp) || occluded == comp.ContainerOccluded || comp is not PointLightComponent clientComp)
return;
comp.ContainerOccluded = occluded;
Dirty(uid, comp);
if (comp.Enabled)
_lightTree.QueueTreeUpdate(uid, comp);
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || enabled == comp.Enabled)
if (!ResolveLight(uid, ref comp) || enabled == comp.Enabled || comp is not PointLightComponent clientComp)
return;
comp._enabled = enabled;
comp.Enabled = enabled;
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
Dirty(uid, comp);
var cast = (PointLightComponent)comp;
if (!cast.ContainerOccluded)
_lightTree.QueueTreeUpdate(uid, cast);
if (!comp.ContainerOccluded)
_lightTree.QueueTreeUpdate(uid, clientComp);
}
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
{
if (!Resolve(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius))
if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius) ||
comp is not PointLightComponent clientComp)
return;
comp._radius = radius;
comp.Radius = radius;
Dirty(uid, comp);
var cast = (PointLightComponent)comp;
if (cast.TreeUid != null)
_lightTree.QueueTreeUpdate(uid, cast);
if (clientComp.TreeUid != null)
_lightTree.QueueTreeUpdate(uid, clientComp);
}
#endregion
}

View File

@@ -4,6 +4,7 @@ using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;

View File

@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -15,6 +18,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects
@@ -183,6 +187,48 @@ namespace Robust.Client.GameObjects
{
_queuedFrameUpdate.Add(uid);
}
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
{
Texture? sprite = null;
switch (spriteSpec)
{
case SpriteSpecifier.Rsi rsi:
var rsiActual = _resourceCache.GetResource<RSIResource>(rsi.RsiPath).RSI;
rsiActual.TryGetState(rsi.RsiState, out var state);
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
sprite ??= Frame0(spriteSpec);
break;
case SpriteSpecifier.Texture texture:
sprite = texture.GetTexture(_resourceCache);
break;
default:
throw new NotImplementedException();
}
return sprite;
}
}
/// <summary>

View File

@@ -1,51 +1,41 @@
using System.Numerics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
public sealed partial class TransformSystem
{
public override void SetLocalPosition(TransformComponent xform, Vector2 value)
public override void SetLocalPosition(EntityUid uid, Vector2 value, TransformComponent? xform = null)
{
xform.PrevPosition = xform._localPosition;
if (!XformQuery.Resolve(uid, ref xform))
return;
xform.NextPosition = value;
xform.LerpParent = xform.ParentUid;
base.SetLocalPosition(xform, value);
ActivateLerp(xform);
ActivateLerp(uid, xform);
base.SetLocalPosition(uid, value, xform);
}
public override void SetLocalPositionNoLerp(TransformComponent xform, Vector2 value)
public override void SetLocalRotation(EntityUid uid, Angle value, TransformComponent? xform = null)
{
xform.NextPosition = null;
xform.LerpParent = EntityUid.Invalid;
base.SetLocalPositionNoLerp(xform, value);
if (!XformQuery.Resolve(uid, ref xform))
return;
xform.NextRotation = value;
ActivateLerp(uid, xform);
base.SetLocalRotation(uid, value, xform);
}
public override void SetLocalRotationNoLerp(TransformComponent xform, Angle angle)
public override void SetLocalPositionRotation(EntityUid uid, Vector2 pos, Angle rot, TransformComponent? xform = null)
{
xform.NextRotation = null;
xform.LerpParent = EntityUid.Invalid;
base.SetLocalRotationNoLerp(xform, angle);
}
if (!XformQuery.Resolve(uid, ref xform))
return;
public override void SetLocalRotation(TransformComponent xform, Angle angle)
{
xform.PrevRotation = xform._localRotation;
xform.NextRotation = angle;
xform.LerpParent = xform.ParentUid;
base.SetLocalRotation(xform, angle);
ActivateLerp(xform);
}
public override void SetLocalPositionRotation(TransformComponent xform, Vector2 pos, Angle rot)
{
xform.PrevPosition = xform._localPosition;
xform.NextPosition = pos;
xform.PrevRotation = xform._localRotation;
xform.NextRotation = rot;
xform.LerpParent = xform.ParentUid;
base.SetLocalPositionRotation(xform, pos, rot);
ActivateLerp(xform);
ActivateLerp(uid, xform);
base.SetLocalPositionRotation(uid, pos, rot, xform);
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
@@ -25,20 +24,15 @@ namespace Robust.Client.GameObjects
private const float MinInterpolationDistance = 0.001f;
private const float MinInterpolationDistanceSquared = MinInterpolationDistance * MinInterpolationDistance;
private const double MinInterpolationAngle = Math.PI / 720;
// 45 degrees.
private const double MaxInterpolationAngle = Math.PI / 4;
[Dependency] private readonly IGameTiming _gameTiming = default!;
// Only keep track of transforms actively lerping.
// Much faster than iterating 3000+ transforms every frame.
[ViewVariables] private readonly List<TransformComponent> _lerpingTransforms = new();
[ViewVariables] private readonly List<Entity<TransformComponent>> _lerpingTransforms = new();
public void Reset()
{
foreach (var xform in _lerpingTransforms)
foreach (var (_, xform) in _lerpingTransforms)
{
xform.ActivelyLerping = false;
xform.NextPosition = null;
@@ -48,21 +42,77 @@ namespace Robust.Client.GameObjects
_lerpingTransforms.Clear();
}
public override void ActivateLerp(TransformComponent xform)
public override void ActivateLerp(EntityUid uid, TransformComponent xform)
{
if (xform.ActivelyLerping)
// This lerping logic is pretty convoluted and generally assumes that the client does not mispredict.
// A more foolproof solution would be to just cache the coordinates at which any given entity was most
// recently rendered and using that as the lerp origin. However that'd require enumerating over all entities
// every tick which is pretty icky.
// The general considerations are:
// - If the client receives a server state for an entity moving from a->b and predicts nothing else, then it
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.
// If the client predicts an entity moves while already lerping, it should clear the
// predict a->b, lerp a->b
// predicted a->b, then predict b->c. Lerp b->c
// predicted a->b, then predict b->c. Lerp b->c
// predicted a->b, predicted b->c, then predict c->d. Lerp c->d
// server state a->b, then predicted b->c, lerp b->c
// server state a->b, then predicted b->c, then predict d, lerp b->c
if (_gameTiming.ApplyingState)
{
if (xform.ActivelyLerping)
{
// This should not happen, but can happen if some bad component state application code modifies an entity's coordinates.
Log.Error($"Entity {(ToPrettyString(uid))} tried to lerp twice while applying component states.");
return;
}
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = false;
xform.LerpParent = xform.ParentUid;
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LastLerp = _gameTiming.CurTick;
return;
}
xform.ActivelyLerping = true;
_lerpingTransforms.Add(xform);
}
xform.LastLerp = _gameTiming.CurTick;
if (!_gameTiming.IsFirstTimePredicted)
{
xform.ActivelyLerping = false;
return;
}
public override void DeactivateLerp(TransformComponent component)
{
// this should cause the lerp to do nothing
component.NextPosition = null;
component.NextRotation = null;
component.LerpParent = EntityUid.Invalid;
if (!xform.ActivelyLerping)
{
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = true;
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LerpParent = xform.ParentUid;
return;
}
if (!xform.PredictedLerp || xform.LerpParent != xform.ParentUid)
{
// Existing lerp was not due to prediction, but due to state application. That lerp should already
// have been rendered, so we will start a new lerp from the current position.
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LerpParent = xform.ParentUid;
}
}
public override void FrameUpdate(float frameTime)
@@ -73,12 +123,13 @@ namespace Robust.Client.GameObjects
for (var i = 0; i < _lerpingTransforms.Count; i++)
{
var transform = _lerpingTransforms[i];
var (uid, transform) = _lerpingTransforms[i];
var found = false;
// Only lerp if parent didn't change.
// E.g. entering lockers would do it.
if (transform.LerpParent == transform.ParentUid
if (transform.ActivelyLerping
&& transform.LerpParent == transform.ParentUid
&& transform.ParentUid.IsValid()
&& !transform.Deleted)
{
@@ -90,8 +141,7 @@ namespace Robust.Client.GameObjects
if (distance is > MinInterpolationDistanceSquared and < MaxInterpolationDistanceSquared)
{
transform.LocalPosition = Vector2.Lerp(lerpSource, lerpDest, step);
// Setting LocalPosition clears LerpPosition so fix that.
SetLocalPositionNoLerp(uid, Vector2.Lerp(lerpSource, lerpDest, step), transform);
transform.NextPosition = lerpDest;
found = true;
}
@@ -101,15 +151,9 @@ namespace Robust.Client.GameObjects
{
var lerpDest = transform.NextRotation.Value;
var lerpSource = transform.PrevRotation;
var distance = Math.Abs(Angle.ShortestDistance(lerpDest, lerpSource));
if (distance is > MinInterpolationAngle and < MaxInterpolationAngle)
{
transform.LocalRotation = Angle.Lerp(lerpSource, lerpDest, step);
// Setting LocalRotation clears LerpAngle so fix that.
transform.NextRotation = lerpDest;
found = true;
}
SetLocalRotationNoLerp(uid, Angle.Lerp(lerpSource, lerpDest, step), transform);
transform.NextRotation = lerpDest;
found = true;
}
}

View File

@@ -1,13 +1,12 @@
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using System;
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
@@ -19,41 +18,22 @@ namespace Robust.Client.GameObjects
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
SubscribeLocalEvent<ClientUserInterfaceComponent, ComponentInit>(OnUserInterfaceInit);
SubscribeLocalEvent<ClientUserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
}
private void OnUserInterfaceInit(EntityUid uid, ClientUserInterfaceComponent component, ComponentInit args)
{
component._interfaces.Clear();
foreach (var data in component._interfaceData)
{
component._interfaces[data.UiKey] = data;
}
}
private void OnUserInterfaceShutdown(EntityUid uid, ClientUserInterfaceComponent component, ComponentShutdown args)
{
foreach (var bui in component.OpenInterfaces.Values)
{
bui.Dispose();
}
}
private void MessageReceived(BoundUIWrapMessage ev)
{
var uid = ev.Entity;
if (!TryComp<ClientUserInterfaceComponent>(uid, out var cmp))
var uid = GetEntity(ev.Entity);
if (!TryComp<UserInterfaceComponent>(uid, out var cmp))
return;
var uiKey = ev.UiKey;
var message = ev.Message;
// This should probably not happen at this point, but better make extra sure!
if(_playerManager.LocalPlayer != null)
if (_playerManager.LocalPlayer != null)
message.Session = _playerManager.LocalPlayer.Session;
message.Entity = uid;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
// Raise as object so the correct type is used.
@@ -66,7 +46,7 @@ namespace Robust.Client.GameObjects
break;
case CloseBoundInterfaceMessage _:
TryCloseUi(uid, uiKey, remoteCall: true, uiComp: cmp);
TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
break;
default:
@@ -77,7 +57,7 @@ namespace Robust.Client.GameObjects
}
}
private bool TryOpenUi(EntityUid uid, Enum uiKey, ClientUserInterfaceComponent? uiComp = null)
private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
{
if (!Resolve(uid, ref uiComp))
return false;
@@ -85,7 +65,7 @@ namespace Robust.Client.GameObjects
if (uiComp.OpenInterfaces.ContainsKey(uiKey))
return false;
var data = uiComp._interfaces[uiKey];
var data = uiComp.MappedInterfaceData[uiKey];
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
@@ -96,36 +76,13 @@ namespace Robust.Client.GameObjects
uiComp.OpenInterfaces[uiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if(playerSession != null)
if (playerSession != null)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
}
return true;
}
internal bool TryCloseUi(EntityUid uid, Enum uiKey, bool remoteCall = false, ClientUserInterfaceComponent? uiComp = null)
{
if (!Resolve(uid, ref uiComp))
return false;
if (!uiComp.OpenInterfaces.TryGetValue(uiKey, out var boundUserInterface))
return false;
if (!remoteCall)
SendUiMessage(boundUserInterface, new CloseBoundInterfaceMessage());
uiComp.OpenInterfaces.Remove(uiKey);
boundUserInterface.Dispose();
var playerSession = _playerManager.LocalPlayer?.Session;
if(playerSession != null)
RaiseLocalEvent(uid, new BoundUIClosedEvent(uiKey, uid, playerSession), true);
return true;
}
internal void SendUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage msg)
{
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Owner, msg, bui.UiKey));
}
}
}

View File

@@ -6,7 +6,7 @@ namespace Robust.Client.GameObjects
{
// These methods are used by the Game State Manager.
EntityUid CreateEntity(string? prototypeName, EntityUid uid = default);
EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata);
void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null);

View File

@@ -1,7 +1,6 @@
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
@@ -15,7 +14,7 @@ public sealed class ClientDirtySystem : EntitySystem
{
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
// Entities that have removed networked components
// could pool the ushort sets, but predicted component changes are rare... soo...
internal readonly Dictionary<EntityUid, HashSet<ushort>> RemovedComponents = new();
@@ -40,11 +39,11 @@ public sealed class ClientDirtySystem : EntitySystem
private void OnTerminate(ref EntityTerminatingEvent ev)
{
if (!_timing.InPrediction || ev.Entity.IsClientSide())
if (!_timing.InPrediction || IsClientSide(ev.Entity))
return;
// Client-side entity deletion is not supported and will cause errors.
Logger.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity)}. Trace: {Environment.StackTrace}");
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity)}. Trace: {Environment.StackTrace}");
}
private void OnCompRemoved(RemovedComponentEventArgs args)
@@ -52,8 +51,9 @@ public sealed class ClientDirtySystem : EntitySystem
if (args.Terminating)
return;
var uid = args.BaseArgs.Owner;
var comp = args.BaseArgs.Component;
if (!_timing.InPrediction || comp.Owner.IsClientSide() || !comp.NetSyncEnabled)
if (!_timing.InPrediction || !comp.NetSyncEnabled || IsClientSide(uid, args.Meta))
return;
// Was this component added during prediction? If yes, then there is no need to re-add it when resetting.
@@ -62,7 +62,7 @@ public sealed class ClientDirtySystem : EntitySystem
var netId = _compFact.GetRegistration(comp).NetID;
if (netId != null)
RemovedComponents.GetOrNew(comp.Owner).Add(netId.Value);
RemovedComponents.GetOrNew(uid).Add(netId.Value);
}
public void Reset()
@@ -73,7 +73,7 @@ public sealed class ClientDirtySystem : EntitySystem
private void OnEntityDirty(EntityUid e)
{
if (_timing.InPrediction && !e.IsClientSide())
if (_timing.InPrediction && !IsClientSide(e))
DirtyEntities.Add(e);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,38 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor, IPostInjectInit
internal sealed class GameStateProcessor : IGameStateProcessor
{
[Dependency] private ILogManager _logMan = default!;
public const int MaxBufferSize = 512;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
private readonly List<GameState> _stateBuffer = new();
private readonly Dictionary<GameTick, List<EntityUid>> _pvsDetachMessages = new();
private ISawmill _logger = default!;
private ISawmill _stateLogger = default!;
private readonly Dictionary<GameTick, List<NetEntity>> _pvsDetachMessages = new();
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
/// </summary>
internal readonly Dictionary<EntityUid, Dictionary<ushort, ComponentState>> _lastStateFullRep
internal readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _lastStateFullRep
= new();
/// <inheritdoc />
@@ -70,9 +58,12 @@ namespace Robust.Client.GameStates
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IClientGameTiming timing)
/// <param name="clientGameStateManager"></param>
public GameStateProcessor(IClientGameStateManager state, IClientGameTiming timing, ISawmill logger)
{
_timing = timing;
_state = state;
_logger = logger;
}
/// <inheritdoc />
@@ -82,7 +73,7 @@ namespace Robust.Client.GameStates
if (state.ToSequence <= _timing.LastRealTick)
{
if (Logging)
_stateLogger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -94,7 +85,7 @@ namespace Robust.Client.GameStates
continue;
if (Logging)
_stateLogger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -103,13 +94,13 @@ namespace Robust.Client.GameStates
if (!WaitingForFull)
{
// This is a good state that we will be using.
_stateBuffer.Add(state);
TryAdd(state);
if (Logging)
_stateLogger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
@@ -127,10 +118,44 @@ namespace Robust.Client.GameStates
return false;
}
_stateBuffer.Add(state);
TryAdd(state);
return true;
}
public void TryAdd(GameState state)
{
if (_stateBuffer.Count <= MaxBufferSize)
{
_stateBuffer.Add(state);
return;
}
// This can happen if a required state gets dropped somehow and the client keeps receiving future
// game states that they can't apply. I.e., GetApplicableStateCount() is zero, even though there are many
// states in the list.
//
// This can seemingly happen when the server sends ""reliable"" game states while the client is paused?
// For example, when debugging the client, while the server is running:
// - The client stops sending acks for states that the server sends out.
// - Thus the client will exceed the net.force_ack_threshold cvar
// - The server starts sending some packets ""reliably"" and just force updates the clients last ack.
//
// What should happen is that when the client resumes, it receives the reliably sent states and can just
// resume. However, even though the packets are sent ""reliably"", they just seem to get dropped.
// I don't quite understand how/why yet, but this ensures the client doesn't get stuck.
#if FULL_RELEASE
_logger.Warning(@$"Exceeded maximum state buffer size!
Tick: {_timing.CurTick}/{_timing.LastProcessedTick}/{_timing.LastRealTick}
Size: {_stateBuffer.Count}
Applicable states: {GetApplicableStateCount()}
Was waiting for full: {WaitingForFull} {LastFullStateRequested}
Had full state: {LastFullState != null}"
);
#endif
_state.RequestFullState();
}
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
@@ -151,7 +176,7 @@ namespace Robust.Client.GameStates
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
if (Logging)
_stateLogger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
_logger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
return applyNextState;
@@ -178,10 +203,10 @@ namespace Robust.Client.GameStates
foreach (var entityState in state.EntityStates.Span)
{
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
if (!_lastStateFullRep.TryGetValue(entityState.NetEntity, out var compData))
{
compData = new Dictionary<ushort, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
_lastStateFullRep.Add(entityState.NetEntity, compData);
}
foreach (var change in entityState.ComponentChanges.Span)
@@ -263,7 +288,7 @@ namespace Robust.Client.GameStates
return false;
}
internal void AddLeavePvsMessage(List<EntityUid> entities, GameTick tick)
internal void AddLeavePvsMessage(List<NetEntity> entities, GameTick tick)
{
// Late message may still need to be processed,
DebugTools.Assert(entities.Count > 0);
@@ -272,9 +297,9 @@ namespace Robust.Client.GameStates
public void ClearDetachQueue() => _pvsDetachMessages.Clear();
public List<(GameTick Tick, List<EntityUid> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
public List<(GameTick Tick, List<NetEntity> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
{
var result = new List<(GameTick Tick, List<EntityUid> Entities)>();
var result = new List<(GameTick Tick, List<NetEntity> Entities)>();
foreach (var (tick, entities) in _pvsDetachMessages)
{
if (tick > toTick)
@@ -343,27 +368,35 @@ namespace Robust.Client.GameStates
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = GameTick.Zero;
LastFullStateRequested = (GameTick.Zero, DateTime.MaxValue);
}
public void RequestFullState()
public void OnFullStateRequested(GameTick tick)
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = _timing.LastRealTick;
LastFullStateRequested = (tick, DateTime.UtcNow);
}
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> implicitData)
public void OnFullStateReceived()
{
foreach (var (uid, implicitEntState) in implicitData)
LastFullState = null;
LastFullStateRequested = null;
}
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
{
foreach (var (netEntity, implicitEntState) in implicitData)
{
var fullRep = _lastStateFullRep[uid];
var fullRep = _lastStateFullRep[netEntity];
foreach (var (netId, implicitCompState) in implicitEntState)
{
if (!fullRep.TryGetValue(netId, out var serverState))
ref var serverState = ref CollectionsMarshal.GetValueRefOrAddDefault(fullRep, netId, out var exists);
if (!exists)
{
fullRep.Add(netId, implicitCompState);
serverState = implicitCompState;
continue;
}
@@ -374,37 +407,50 @@ namespace Robust.Client.GameStates
// state from the entity prototype.
if (implicitCompState is not IComponentDeltaState implicitDelta || !implicitDelta.FullState)
{
_logger.Error($"Server sent delta state and client failed to construct an implicit full state for entity {uid}");
_logger.Error($"Server sent delta state and client failed to construct an implicit full state for entity {netEntity}");
continue;
}
serverDelta.ApplyToFullState(implicitCompState);
fullRep[netId] = implicitCompState;
serverState = implicitCompState;
DebugTools.Assert(implicitCompState is IComponentDeltaState d && d.FullState);
}
}
}
public Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity)
public Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity netEntity)
{
return _lastStateFullRep[entity];
return _lastStateFullRep[netEntity];
}
public Dictionary<EntityUid, Dictionary<ushort, ComponentState>> GetFullRep()
public Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep()
{
return _lastStateFullRep;
}
public bool TryGetLastServerStates(EntityUid entity,
public bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
{
return _lastStateFullRep.TryGetValue(entity, out dictionary);
}
public int CalculateBufferSize(GameTick fromTick)
public bool IsQueuedForDetach(NetEntity entity)
{
// This isn't fast, but its just meant for use in tests & debug asserts.
foreach (var msg in _pvsDetachMessages.Values)
{
if (msg.Contains(entity))
return true;
}
return false;
}
public int GetApplicableStateCount(GameTick? fromTick = null)
{
fromTick ??= _timing.LastRealTick;
bool foundState;
var nextTick = fromTick;
var nextTick = fromTick.Value;
do
{
@@ -422,13 +468,9 @@ namespace Robust.Client.GameStates
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value);
return (int) (nextTick.Value - fromTick.Value.Value);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("net");
_stateLogger = _logMan.GetSawmill("net.state");
}
public int StateCount => _stateBuffer.Count;
}
}

View File

@@ -32,7 +32,15 @@ namespace Robust.Client.GameStates
/// <summary>
/// Number of applicable game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
int GetApplicableStateCount();
[Obsolete("use GetApplicableStateCount()")]
int CurrentBufferSize => GetApplicableStateCount();
/// <summary>
/// Total number of game states currently in the state buffer.
/// </summary>
int StateCount { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
@@ -75,7 +83,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Applies a given set of game states.
/// </summary>
IEnumerable<EntityUid> ApplyGameState(GameState curState, GameState? nextState);
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
/// <summary>
/// Resets any entities that have changed while predicting future ticks.
@@ -86,12 +94,12 @@ namespace Robust.Client.GameStates
/// An input command has been dispatched.
/// </summary>
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
void InputCommandDispatched(ClientFullInputCmdMessage clientMsg, FullInputCmdMessage message);
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
void RequestFullState(EntityUid? missingEntity = null);
void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null);
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
@@ -105,7 +113,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Returns the full collection of cached game states that are used to reset predicted entities.
/// </summary>
Dictionary<EntityUid, Dictionary<ushort, ComponentState>> GetFullRep();
Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep();
/// <summary>
/// This will perform some setup in order to reset the game to an earlier state. To fully reset the state
@@ -144,12 +152,12 @@ namespace Robust.Client.GameStates
/// Queue a collection of entities that are to be detached to null-space & marked as PVS-detached.
/// This store and modify the list given to it.
/// </summary>
void QueuePvsDetach(List<EntityUid> entities, GameTick tick);
void QueuePvsDetach(List<NetEntity> entities, GameTick tick);
/// <summary>
/// Immediately detach several entities.
/// </summary>
void DetachImmediate(List<EntityUid> entities);
void DetachImmediate(List<NetEntity> entities);
/// <summary>
/// Clears the PVS detach queue.

View File

@@ -83,22 +83,22 @@ namespace Robust.Client.GameStates
/// The data to merge.
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
/// </param>
void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data);
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> data);
/// <summary>
/// Get the last state data from the server for an entity.
/// </summary>
/// <returns>Dictionary (net ID -> ComponentState)</returns>
Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity);
Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity entity);
/// <summary>
/// Calculate the number of applicable states in the game state buffer from a given tick.
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
int GetApplicableStateCount(GameTick? fromTick);
bool TryGetLastServerStates(EntityUid entity,
bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
}
}

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameStates
private readonly Font _font;
private readonly int _lineHeight;
private readonly Dictionary<EntityUid, NetEntData> _netEnts = new();
private readonly Dictionary<NetEntity, NetEntData> _netEnts = new();
public NetEntityOverlay()
{
@@ -77,12 +77,12 @@ namespace Robust.Client.GameStates
foreach (var entityState in gameState.EntityStates.Span)
{
if (!_netEnts.TryGetValue(entityState.Uid, out var netEnt))
if (!_netEnts.TryGetValue(entityState.NetEntity, out var netEnt))
{
if (_netEnts.Count >= _maxEnts)
continue;
_netEnts[entityState.Uid] = netEnt = new();
_netEnts[entityState.NetEntity] = netEnt = new();
}
if (!netEnt.InPVS && netEnt.LastUpdate < gameState.ToSequence)
@@ -119,11 +119,13 @@ namespace Robust.Client.GameStates
var screenHandle = args.ScreenHandle;
int i = 0;
foreach (var (uid, netEnt) in _netEnts)
foreach (var (nent, netEnt) in _netEnts)
{
var uid = _entityManager.GetEntity(nent);
if (!_entityManager.EntityExists(uid))
{
_netEnts.Remove(uid);
_netEnts.Remove(nent);
continue;
}

View File

@@ -26,6 +26,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
private const int TargetPayloadBps = 56000 / 8; // Target Payload size in Bytes per second. A mind-numbing fifty-six thousand bits per second, who would ever need more?
@@ -67,13 +68,13 @@ namespace Robust.Client.GameStates
var lag = _netManager.ServerChannel!.Ping;
// calc interp info
var buffer = _gameStateManager.CurrentBufferSize;
var buffer = _gameStateManager.GetApplicableStateCount();
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, buffer));
// not watching an ent
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
if(!WatchEntId.IsValid() || _entManager.IsClientSide(WatchEntId))
return;
string? entStateString = null;
@@ -86,7 +87,9 @@ namespace Robust.Client.GameStates
var sb = new StringBuilder();
foreach (var entState in entStates.Span)
{
if (entState.Uid != WatchEntId)
var uid = _entManager.GetEntity(entState.NetEntity);
if (uid != WatchEntId)
continue;
if (!entState.ComponentChanges.HasContents)
@@ -115,7 +118,9 @@ namespace Robust.Client.GameStates
foreach (var ent in args.Detached)
{
if (ent != WatchEntId)
var uid = _entManager.GetEntity(ent);
if (uid != WatchEntId)
continue;
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
@@ -126,7 +131,9 @@ namespace Robust.Client.GameStates
{
foreach (var entDelete in entDeletes.Span)
{
if (entDelete == WatchEntId)
var uid = _entManager.GetEntity(entDelete);
if (uid == WatchEntId)
entDelString = "\n Deleted";
}
}
@@ -261,7 +268,7 @@ namespace Robust.Client.GameStates
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
// buffer text
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
}
protected override void DisposeBehavior()
@@ -294,30 +301,33 @@ namespace Robust.Client.GameStates
private sealed class NetWatchEntCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override string Command => "net_watchent";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntityUid eValue;
EntityUid? entity;
if (args.Length == 0)
{
eValue = IoCManager.Resolve<IPlayerManager>().LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
}
else if (!EntityUid.TryParse(args[0], out eValue))
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (!overlayMan.TryGetOverlay(out NetGraphOverlay? overlay))
if (!_overlayManager.TryGetOverlay(out NetGraphOverlay? overlay))
{
overlay = new();
overlayMan.AddOverlay(overlay);
overlay = new NetGraphOverlay();
_overlayManager.AddOverlay(overlay);
}
overlay.WatchEntId = eValue;
overlay.WatchEntId = entity.Value;
}
}
}

View File

@@ -1,19 +1,19 @@
using System;
using Robust.Shared.Enums;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
{
internal sealed class NetInterpOverlay : Overlay
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -24,6 +24,11 @@ namespace Robust.Client.GameStates
private readonly SharedContainerSystem _container;
private readonly SharedTransformSystem _xform;
/// <summary>
/// When an entity stops lerping the overlay will continue to draw a box around the entity for this amount of time.
/// </summary>
public static readonly TimeSpan Delay = TimeSpan.FromSeconds(2f);
public NetInterpOverlay(EntityLookupSystem lookup)
{
IoCManager.InjectDependencies(this);
@@ -40,8 +45,8 @@ namespace Robust.Client.GameStates
var worldHandle = (DrawingHandleWorld) handle;
var viewport = args.WorldAABB;
var query = _entityManager.AllEntityQueryEnumerator<PhysicsComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var physics, out var transform))
var query = _entityManager.AllEntityQueryEnumerator<TransformComponent>();
while (query.MoveNext(out var uid, out var transform))
{
// if not on the same map, continue
if (transform.MapID != _eyeManager.CurrentMap || _container.IsEntityInContainer(uid))
@@ -50,8 +55,8 @@ namespace Robust.Client.GameStates
if (transform.GridUid == uid)
continue;
// This entity isn't lerping, no need to draw debug info for it
if(transform.NextPosition == null)
var delta = (_timing.CurTick.Value - transform.LastLerp.Value) * _timing.TickPeriod;
if(!transform.ActivelyLerping && delta > Delay)
continue;
var aabb = _lookup.GetWorldAABB(uid);
@@ -61,7 +66,9 @@ namespace Robust.Client.GameStates
continue;
var (pos, rot) = _xform.GetWorldPositionRotation(transform, _entityManager.GetEntityQuery<TransformComponent>());
var boxOffset = transform.NextPosition.Value - transform.LocalPosition;
var boxOffset = transform.NextPosition != null
? transform.NextPosition.Value - transform.LocalPosition
: default;
var worldOffset = (rot - transform.LocalRotation).RotateVec(boxOffset);
var nextPos = pos + worldOffset;

View File

@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;

View File

@@ -1,4 +1,6 @@
namespace Robust.Client.Graphics
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
{
/// <summary>
/// A fixed eye is an eye which is fixed to one point, its position.

View File

@@ -1,5 +1,6 @@
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
@@ -40,22 +40,28 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
var grids = new List<Entity<MapGridComponent>>();
_mapManager.FindGridsIntersecting(mapId, worldBounds, ref grids);
foreach (var mapGrid in grids)
{
if (!_mapChunkData.ContainsKey(mapGrid.Owner))
if (!_mapChunkData.ContainsKey(mapGrid))
continue;
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid.Owner);
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var enumerator = mapGrid.GetMapChunks(worldBounds);
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
var data = _mapChunkData[mapGrid];
while (enumerator.MoveNext(out var chunk))
{
if (_isChunkDirty(mapGrid, chunk))
_updateChunkMesh(mapGrid, chunk);
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = _mapChunkData[mapGrid.Owner][chunk.Indices];
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
continue;
@@ -67,22 +73,36 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
}
CullEmptyChunks();
}
private void _updateChunkMesh(MapGridComponent grid, MapChunk chunk)
private void CullEmptyChunks()
{
var data = _mapChunkData[grid.Owner];
if (!data.TryGetValue(chunk.Indices, out var datum))
foreach (var (grid, chunks) in _mapChunkData)
{
datum = _initChunkBuffers(grid, chunk);
}
var gridComp = _mapManager.GetGridComp(grid);
foreach (var (index, chunk) in chunks)
{
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
{
DebugTools.Assert(gridComp.Chunks[index].FilledTiles > 0);
continue;
}
DeleteChunk(chunk);
chunks.Remove(index);
}
}
}
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var i = 0;
var cSz = grid.ChunkSize;
var cSz = grid.Comp.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
{
@@ -129,7 +149,7 @@ namespace Robust.Client.Graphics.Clyde
datum.TileCount = i;
}
private unsafe MapChunkData _initChunkBuffers(MapGridComponent grid, MapChunk chunk)
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
{
var vao = GenVertexArray();
BindVertexArray(vao);
@@ -157,41 +177,22 @@ namespace Robust.Client.Graphics.Clyde
Dirty = true
};
_mapChunkData[grid.Owner].Add(chunk.Indices, datum);
return datum;
}
private bool _isChunkDirty(MapGridComponent grid, MapChunk chunk)
private void DeleteChunk(MapChunkData data)
{
var data = _mapChunkData[grid.Owner];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;
}
public void _setChunkDirty(MapGridComponent grid, Vector2i chunk)
{
var data = _mapChunkData.GetOrNew(grid.Owner);
if (data.TryGetValue(chunk, out var datum))
{
datum.Dirty = true;
}
// Don't need to set it if we don't have an entry since lack of an entry is treated as dirty.
}
private void _updateOnGridModified(GridModifiedEvent args)
{
foreach (var (pos, _) in args.Modified)
{
var grid = args.Grid;
var chunk = grid.GridTileToChunkIndices(pos);
_setChunkDirty(grid, chunk);
}
DeleteVertexArray(data.VAO);
CheckGlError();
data.VBO.Delete();
data.EBO.Delete();
}
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var grid = _mapManager.GetGrid(args.NewTile.GridUid);
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
_setChunkDirty(grid, chunk);
var gridData = _mapChunkData.GetOrNew(args.Entity);
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -207,10 +208,7 @@ namespace Robust.Client.Graphics.Clyde
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)
{
DeleteVertexArray(chunkDatum.VAO);
CheckGlError();
chunkDatum.VBO.Delete();
chunkDatum.EBO.Delete();
DeleteChunk(chunkDatum);
}
_mapChunkData.Remove(gridId);

View File

@@ -9,6 +9,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
@@ -349,7 +350,7 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
if (entry.Sprite.RaiseShaderEvent)
_entityManager.EventBus.RaiseLocalEvent(entry.Sprite.Owner,
_entityManager.EventBus.RaiseLocalEvent(entry.Uid,
new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
}
}

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using ES20 = OpenToolkit.Graphics.ES20;

View File

@@ -16,8 +16,10 @@ using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Robust.Shared.Physics;
using Robust.Client.ComponentTrees;
using Robust.Shared.Graphics;
using static Robust.Shared.GameObjects.OccluderComponent;
using Robust.Shared.Utility;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics.Clyde
@@ -336,10 +338,12 @@ namespace Robust.Client.Graphics.Clyde
}
var mapId = eye.Position.MapId;
if (mapId == MapId.Nullspace)
return;
// If this map has lighting disabled, return
var mapUid = _mapManager.GetMapEntityId(mapId);
if (!_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
if (!_entityManager.TryGetComponent<MapComponent>(mapUid, out var map) || !map.LightingEnabled)
{
return;
}

View File

@@ -5,6 +5,7 @@ using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics.Clyde
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;

View File

@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;

View File

@@ -6,6 +6,7 @@ using System.Numerics;
using System.Text;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;

View File

@@ -1,11 +1,3 @@
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using System;
using System.Buffers;
using System.Collections.Generic;
@@ -14,6 +6,15 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Threading.Tasks;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde;
@@ -259,7 +260,7 @@ internal partial class Clyde
if (cmp != 0)
return cmp;
return a.Sprite.Owner.CompareTo(b.Sprite.Owner);
return a.Uid.CompareTo(b.Uid);
}
}
}

View File

@@ -8,6 +8,8 @@ using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
@@ -17,6 +19,7 @@ using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using PIF = OpenToolkit.Graphics.OpenGL4.PixelInternalFormat;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -12,15 +12,15 @@ using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using Color = Robust.Shared.Maths.Color;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
{
@@ -173,7 +173,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.SubscribeEvent<TileChangedEvent>(EventSource.Local, this, _updateTileMapOnUpdate);
_entityManager.EventBus.SubscribeEvent<GridStartupEvent>(EventSource.Local, this, _updateOnGridCreated);
_entityManager.EventBus.SubscribeEvent<GridRemovalEvent>(EventSource.Local, this, _updateOnGridRemoved);
_entityManager.EventBus.SubscribeEvent<GridModifiedEvent>(EventSource.Local, this, _updateOnGridModified);
}
public void ShutdownGridEcsEvents()
@@ -181,7 +180,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.UnsubscribeEvent<TileChangedEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridStartupEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridRemovalEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridModifiedEvent>(EventSource.Local, this);
}
private void GLInitBindings(bool gles)

View File

@@ -8,6 +8,7 @@ using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;

View File

@@ -1,9 +0,0 @@
namespace Robust.Client.Graphics
{
internal enum ClydeStockTexture : byte
{
White,
Black,
Transparent
}
}

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -154,7 +155,7 @@ namespace Robust.Client.Graphics
Vector2 scale,
Angle? worldRot,
Angle eyeRotation = default,
Direction? overrideDirection = null,
Shared.Maths.Direction? overrideDirection = null,
SpriteComponent? sprite = null,
TransformComponent? xform = null,
SharedTransformSystem? xformSystem = null);

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics

View File

@@ -1,5 +1,6 @@
using System;
using System.Numerics;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;

View File

@@ -1,6 +1,7 @@
using System;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -38,8 +38,7 @@ namespace Robust.Client.Graphics
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).
/// Raised when the window has been resized.
/// </summary>
event Action<WindowResizedEventArgs> Resized;
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Maths;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{

View File

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

View File

@@ -37,7 +37,7 @@ namespace Robust.Client.Graphics
Vector2 scale,
Angle? worldRot,
Angle eyeRotation = default,
Direction? overrideDirection = null,
Shared.Maths.Direction? overrideDirection = null,
SpriteComponent? sprite = null,
TransformComponent? xform = null,
SharedTransformSystem? xformSystem = null);

View File

@@ -1,3 +1,5 @@
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
{
/// <summary>

View File

@@ -1,12 +1,14 @@
namespace Robust.Client.Graphics
using Robust.Shared.Graphics.RSI;
namespace Robust.Client.Graphics
{
public interface IRsiStateLike : IDirectionalTextureProvider
{
RSI.State.DirectionType Directions { get; }
RsiDirectionType RsiDirections { get; }
bool IsAnimated { get; }
int AnimationFrameCount { get; }
float GetDelay(int frame);
Texture GetFrame(RSI.State.Direction dir, int frame);
Texture GetFrame(RsiDirection dir, int frame);
}
}

View File

@@ -3,6 +3,7 @@ using JetBrains.Annotations;
using Robust.Shared.Timing;
using Robust.Shared.Enums;
using System;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
{

View File

@@ -1,6 +1,8 @@
using System;
using System.Linq;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -25,7 +27,7 @@ namespace Robust.Client.Graphics
// 2D array for the texture to use for each animation frame at each direction.
public readonly Texture[][] Icons;
internal State(Vector2i size, RSI rsi, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
internal State(Vector2i size, RSI rsi, StateId stateId, RsiDirectionType rsiDirection, float[] delays, Texture[][] icons)
{
DebugTools.Assert(size.X > 0);
DebugTools.Assert(size.Y > 0);
@@ -34,7 +36,7 @@ namespace Robust.Client.Graphics
Size = size;
RSI = rsi;
StateId = stateId;
Directions = direction;
RsiDirections = rsiDirection;
Delays = delays;
TotalDelay = delays.Sum();
Icons = icons;
@@ -63,7 +65,7 @@ namespace Robust.Client.Graphics
/// <summary>
/// How many directions this state has.
/// </summary>
public DirectionType Directions { get; }
public RsiDirectionType RsiDirections { get; }
/// <summary>
/// The first frame of the "south" direction.
@@ -90,14 +92,14 @@ namespace Robust.Client.Graphics
int IRsiStateLike.AnimationFrameCount => DelayCount;
public Texture GetFrame(Direction direction, int frame)
public Texture GetFrame(RsiDirection rsiDirection, int frame)
{
return Icons[(int) direction][frame];
return Icons[(int) rsiDirection][frame];
}
public Texture[] GetFrames(Direction direction)
public Texture[] GetFrames(RsiDirection rsiDirection)
{
return Icons[(int) direction];
return Icons[(int) rsiDirection];
}
/// <summary>
@@ -124,52 +126,12 @@ namespace Robust.Client.Graphics
Texture IDirectionalTextureProvider.TextureFor(Shared.Maths.Direction dir)
{
if (Directions == DirectionType.Dir1)
if (RsiDirections == RsiDirectionType.Dir1)
{
return Frame0;
}
return GetFrame(dir.Convert(Directions), 0);
}
/// <summary>
/// Specifies which types of directions an RSI state has.
/// </summary>
public enum DirectionType : byte
{
/// <summary>
/// A single direction, namely South.
/// </summary>
Dir1,
/// <summary>
/// 4 cardinal directions.
/// </summary>
Dir4,
/// <summary>
/// 4 cardinal + 4 diagonal directions.
/// </summary>
Dir8,
}
/// <summary>
/// Specifies a direction in an RSI state.
/// </summary>
/// <remarks>
/// Value of the enum here matches the index used to store it in the icons array. If this ever changes, then
/// <see cref="GameObjects.SpriteComponent.Layer._rsiDirectionMatrices"/> also needs to be updated.
/// </remarks>
public enum Direction : byte
{
South = 0,
North = 1,
East = 2,
West = 3,
SouthEast = 4,
SouthWest = 5,
NorthEast = 6,
NorthWest = 7,
return GetFrame(dir.Convert(RsiDirections), 0);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Numerics;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;

View File

@@ -1,233 +1,112 @@
using System;
using System.IO;
using JetBrains.Annotations;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.RepresentationModel;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
/// <summary>
/// Contains a texture used for drawing things.
/// </summary>
[PublicAPI]
public abstract class Texture : IRsiStateLike
{
/// <summary>
/// Contains a texture used for drawing things.
/// The width of the texture, in pixels.
/// </summary>
[PublicAPI]
public abstract class Texture : IRsiStateLike
public int Width => Size.X;
/// <summary>
/// The height of the texture, in pixels.
/// </summary>
public int Height => Size.Y;
/// <summary>
/// The size of the texture, in pixels.
/// </summary>
public Vector2i Size { get; /*protected set;*/ }
public Color this[int x, int y] => this.GetPixel(x, y);
protected Texture(Vector2i size)
{
/// <summary>
/// The width of the texture, in pixels.
/// </summary>
public int Width => Size.X;
Size = size;
}
/// <summary>
/// The height of the texture, in pixels.
/// </summary>
public int Height => Size.Y;
Texture IDirectionalTextureProvider.Default => this;
/// <summary>
/// The size of the texture, in pixels.
/// </summary>
public Vector2i Size { get; /*protected set;*/ }
Texture IDirectionalTextureProvider.TextureFor(Direction dir)
{
return this;
}
public Color this[int x, int y] => this.GetPixel(x, y);
RsiDirectionType IRsiStateLike.RsiDirections => RsiDirectionType.Dir1;
bool IRsiStateLike.IsAnimated => false;
int IRsiStateLike.AnimationFrameCount => 0;
protected Texture(Vector2i size)
{
Size = size;
}
float IRsiStateLike.GetDelay(int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
public static Texture Transparent =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.Transparent);
return 0;
}
public static Texture White =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.White);
Texture IRsiStateLike.GetFrame(RsiDirection dir, int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
public static Texture Black =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.Black);
return this;
}
/// <summary>
/// Loads a new texture an existing image.
/// </summary>
/// <param name="image">The image to load.</param>
/// <param name="name">The "name" of this texture. This can be referred to later to aid debugging.</param>
/// <param name="loadParameters">
/// Parameters that influence the loading of textures.
/// Defaults to <see cref="TextureLoadParameters.Default"/> if <c>null</c>.
/// </param>
/// <typeparam name="T">The type of pixels of the image. At the moment, images must be <see cref="Rgba32"/>.</typeparam>
public static Texture LoadFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParameters = null) where T : unmanaged, IPixel<T>
{
var manager = IoCManager.Resolve<IClyde>();
return manager.LoadTextureFromImage(image, name, loadParameters);
}
public abstract Color GetPixel(int x, int y);
/// <summary>
/// Loads an image from a stream containing PNG data.
/// </summary>
/// <param name="stream">The stream to load the image from.</param>
/// <param name="name">The "name" of this texture. This can be referred to later to aid debugging.</param>
/// <param name="loadParameters">
/// Parameters that influence the loading of textures.
/// Defaults to <see cref="TextureLoadParameters.Default"/> if <c>null</c>.
/// </param>
public static Texture LoadFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParameters = null)
{
var manager = IoCManager.Resolve<IClyde>();
return manager.LoadTextureFromPNGStream(stream, name, loadParameters);
}
public static Texture Transparent =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.Transparent);
Texture IDirectionalTextureProvider.Default => this;
public static Texture White =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.White);
Texture IDirectionalTextureProvider.TextureFor(Direction dir)
{
return this;
}
public static Texture Black =>
IoCManager.Resolve<IClydeInternal>().GetStockTexture(ClydeStockTexture.Black);
RSI.State.DirectionType IRsiStateLike.Directions => RSI.State.DirectionType.Dir1;
bool IRsiStateLike.IsAnimated => false;
int IRsiStateLike.AnimationFrameCount => 0;
float IRsiStateLike.GetDelay(int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
return 0;
}
Texture IRsiStateLike.GetFrame(RSI.State.Direction dir, int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
return this;
}
public abstract Color GetPixel(int x, int y);
/// <summary>
/// Loads a new texture an existing image.
/// </summary>
/// <param name="image">The image to load.</param>
/// <param name="name">The "name" of this texture. This can be referred to later to aid debugging.</param>
/// <param name="loadParameters">
/// Parameters that influence the loading of textures.
/// Defaults to <see cref="Robust.Client.Graphics.TextureLoadParameters.Default"/> if <c>null</c>.
/// </param>
/// <typeparam name="T">The type of pixels of the image. At the moment, images must be <see cref="Rgba32"/>.</typeparam>
public static Texture LoadFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParameters = null) where T : unmanaged, IPixel<T>
{
var manager = IoCManager.Resolve<IClyde>();
return manager.LoadTextureFromImage(image, name, loadParameters);
}
/// <summary>
/// Flags for loading of textures.
/// Loads an image from a stream containing PNG data.
/// </summary>
[PublicAPI]
public struct TextureLoadParameters
/// <param name="stream">The stream to load the image from.</param>
/// <param name="name">The "name" of this texture. This can be referred to later to aid debugging.</param>
/// <param name="loadParameters">
/// Parameters that influence the loading of textures.
/// Defaults to <see cref="Robust.Client.Graphics.TextureLoadParameters.Default"/> if <c>null</c>.
/// </param>
public static Texture LoadFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParameters = null)
{
/// <summary>
/// The default sampling parameters for the texture.
/// </summary>
public TextureSampleParameters SampleParameters { get; set; }
/// <summary>
/// If true, the image data will be treated as sRGB.
/// </summary>
public bool Srgb { get; set; }
public static TextureLoadParameters FromYaml(YamlMappingNode yaml)
{
var loadParams = Default;
if (yaml.TryGetNode("sample", out YamlMappingNode? sampleNode))
{
loadParams.SampleParameters = TextureSampleParameters.FromYaml(sampleNode);
}
if (yaml.TryGetNode("srgb", out var srgb))
{
loadParams.Srgb = srgb.AsBool();
}
return loadParams;
}
public static readonly TextureLoadParameters Default = new()
{
SampleParameters = TextureSampleParameters.Default,
Srgb = true
};
}
/// <summary>
/// Sample flags for textures.
/// These are separate from <see cref="TextureLoadParameters"/>,
/// because it is possible to create "proxies" to existing textures
/// with different sampling parameters than the base texture.
/// </summary>
[PublicAPI]
public struct TextureSampleParameters
{
// NOTE: If somebody is gonna add support for 3D/1D textures, change this doc comment.
// See the note on this page for why: https://www.khronos.org/opengl/wiki/Sampler_Object#Filtering
/// <summary>
/// If true, use bi-linear texture filtering if the texture cannot be rendered 1:1
/// </summary>
public bool Filter { get; set; }
/// <summary>
/// Controls how to wrap the texture if texture coordinates outside 0-1 are accessed.
/// </summary>
public TextureWrapMode WrapMode { get; set; }
public static TextureSampleParameters FromYaml(YamlMappingNode node)
{
var wrap = TextureWrapMode.None;
var filter = false;
if (node.TryGetNode("filter", out var filterNode))
{
filter = filterNode.AsBool();
}
if (node.TryGetNode("wrap", out var wrapNode))
{
switch (wrapNode.AsString())
{
case "none":
wrap = TextureWrapMode.None;
break;
case "repeat":
wrap = TextureWrapMode.Repeat;
break;
case "mirrored_repeat":
wrap = TextureWrapMode.MirroredRepeat;
break;
default:
throw new ArgumentException("Not a valid wrap mode.");
}
}
return new TextureSampleParameters {Filter = filter, WrapMode = wrap};
}
public static readonly TextureSampleParameters Default = new()
{
Filter = false,
WrapMode = TextureWrapMode.None
};
}
/// <summary>
/// Controls behavior when reading texture coordinates outside 0-1, which usually wraps the texture somehow.
/// </summary>
[PublicAPI]
public enum TextureWrapMode : byte
{
/// <summary>
/// Do not wrap, instead clamp to edge.
/// </summary>
None = 0,
/// <summary>
/// Repeat the texture.
/// </summary>
Repeat,
/// <summary>
/// Repeat the texture mirrored.
/// </summary>
MirroredRepeat,
var manager = IoCManager.Resolve<IClyde>();
return manager.LoadTextureFromPNGStream(stream, name, loadParameters);
}
}

View File

@@ -8,6 +8,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Map;
@@ -21,6 +23,8 @@ public sealed class TileEdgeOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private List<Entity<MapGridComponent>> _grids = new();
public TileEdgeOverlay(IEntityManager entManager, IMapManager mapManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
@@ -35,16 +39,18 @@ public sealed class TileEdgeOverlay : Overlay
if (args.MapId == MapId.Nullspace)
return;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var grid in _grids)
{
var tileSize = grid.TileSize;
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var xform = xformQuery.GetComponent(grid.Owner);
var xform = xformQuery.GetComponent(grid);
args.WorldHandle.SetTransform(xform.WorldMatrix);
foreach (var tileRef in grid.GetTilesIntersecting(args.WorldBounds, false))
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -60,12 +66,17 @@ public sealed class TileEdgeOverlay : Overlay
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = grid.GetTileRef(neighborIndices);
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile

View File

@@ -62,8 +62,9 @@ namespace Robust.Client.Physics
Log.Info($"Received grid fixture debug data");
if (!_enableDebug) return;
_nodes[ev.Grid] = ev.Nodes;
_connections[ev.Grid] = ev.Connections;
var grid = GetEntity(ev.Grid);
_nodes[grid] = ev.Nodes;
_connections[grid] = ev.Connections;
}
private sealed class GridSplitNodeOverlay : Overlay

View File

@@ -20,7 +20,7 @@ namespace Robust.Client.Physics
{
if (args.Current is not JointComponentState jointState) return;
component.Relay = jointState.Relay;
component.Relay = EnsureEntity<JointComponent>(jointState.Relay, uid);
// Initial state gets applied before the entity (& entity's transform) have been initialized.
// So just let joint init code handle that.
@@ -29,7 +29,7 @@ namespace Robust.Client.Physics
component.Joints.Clear();
foreach (var (id, state) in jointState.Joints)
{
component.Joints[id] = state.GetJoint();
component.Joints[id] = state.GetJoint(EntityManager, uid);
}
return;
}
@@ -62,8 +62,8 @@ namespace Robust.Client.Physics
continue;
}
var other = state.UidA == uid ? state.UidB : state.UidA;
var uidA = GetEntity(state.UidA);
var other = uidA == uid ? GetEntity(state.UidB) : uidA;
// Add new joint (if possible).
// Need to wait for BOTH joint components to come in first before we can add it. Yay dependencies!
@@ -82,11 +82,11 @@ namespace Robust.Client.Physics
// TODO: component state handling ordering.
if (Transform(uid).MapID == MapId.Nullspace)
{
AddedJoints.Add(state.GetJoint());
AddedJoints.Add(state.GetJoint(EntityManager, uid));
continue;
}
AddJoint(state.GetJoint());
AddJoint(state.GetJoint(EntityManager, uid));
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Buffers;
using System.Collections.Generic;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -20,18 +20,17 @@ public sealed partial class PhysicsSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<PlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<PhysicsComponent, JointAddedEvent>(OnJointAdded);
SubscribeLocalEvent<PhysicsComponent, JointRemovedEvent>(OnJointRemoved);
}
private void UpdateIsPredicted()
{
var query = GetEntityQuery<PhysicsComponent>();
foreach (var uid in _toUpdate)
{
if (!query.TryGetComponent(uid, out var physics))
if (!PhysicsQuery.TryGetComponent(uid, out var physics))
continue;
var ev = new UpdateIsPredictedEvent(uid);
@@ -64,12 +63,12 @@ public sealed partial class PhysicsSystem
UpdateIsPredicted(args.Joint.BodyBUid);
}
private void OnAttach(PlayerAttachedEvent ev)
private void OnAttach(LocalPlayerAttachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}
private void OnDetach(PlayerDetachedEvent ev)
private void OnDetach(LocalPlayerDetachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -16,7 +15,6 @@ namespace Robust.Client.Physics
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
public override void Update(float frameTime)
@@ -27,7 +25,7 @@ namespace Robust.Client.Physics
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
{
var toRemove = new List<PhysicsComponent>();
var toRemove = new List<Entity<PhysicsComponent>>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
@@ -39,13 +37,13 @@ namespace Robust.Client.Physics
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(body);
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
}
}
foreach (var body in toRemove)
{
SetAwake(body.Owner, body, false);
SetAwake(body, false);
}
base.Cleanup(component, frameTime);
@@ -83,11 +81,8 @@ namespace Robust.Client.Physics
continue;
}
xform.PrevPosition = position;
xform.PrevRotation = rotation;
xform.LerpParent = parentUid;
xform.NextPosition = xform.LocalPosition;
xform.NextRotation = xform.LocalRotation;
// Transform system will handle lerping.
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
}
component.LerpData.Clear();

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Log;
using Direction = Robust.Shared.Maths.Direction;
namespace Robust.Client.Placement
{
@@ -225,7 +226,7 @@ namespace Robust.Client.Placement
}
}))
.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler(
(session, coords, uid) =>
(session, netCoords, nent) =>
{
if (!IsActive)
return false;
@@ -239,15 +240,15 @@ namespace Robust.Client.Placement
if (Eraser)
{
if (HandleDeletion(coords))
if (HandleDeletion(netCoords))
return true;
if (uid == EntityUid.Invalid)
if (nent == EntityUid.Invalid)
{
return false;
}
HandleDeletion(uid);
HandleDeletion(nent);
}
else
{
@@ -285,23 +286,17 @@ namespace Robust.Client.Placement
}, outsidePrediction: true))
.Register<PlacementManager>();
var localPlayer = PlayerManager.LocalPlayer;
localPlayer!.EntityAttached += OnEntityAttached;
PlayerManager.LocalPlayerDetached += OnDetached;
}
private void TearDownInput()
{
CommandBinds.Unregister<PlacementManager>();
if (PlayerManager.LocalPlayer != null)
{
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
}
PlayerManager.LocalPlayerDetached -= OnDetached;
}
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
private void OnDetached(EntityUid obj)
{
// player attached to a new entity, basically disable the editor
Clear();
}
@@ -428,7 +423,7 @@ namespace Robust.Client.Placement
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestEntRemove;
msg.EntityUid = entity;
msg.EntityUid = EntityManager.GetNetEntity(entity);
_networkManager.ClientSendMessage(msg);
}
@@ -436,7 +431,7 @@ namespace Robust.Client.Placement
{
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestRectRemove;
msg.EntityCoordinates = new EntityCoordinates(StartPoint.EntityId, rect.BottomLeft);
msg.NetCoordinates = new NetCoordinates(EntityManager.GetNetEntity(StartPoint.EntityId), rect.BottomLeft);
msg.RectSize = rect.Size;
_networkManager.ClientSendMessage(msg);
}
@@ -790,7 +785,7 @@ namespace Robust.Client.Placement
message.EntityTemplateName = CurrentPermission.EntityType;
// world x and y
message.EntityCoordinates = coordinates;
message.NetCoordinates = EntityManager.GetNetCoordinates(coordinates);
message.DirRcv = Direction;

View File

@@ -7,6 +7,7 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;

View File

@@ -1,44 +1,71 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
using Robust.Shared.Player;
namespace Robust.Client.Player
namespace Robust.Client.Player;
public interface IPlayerManager : ISharedPlayerManager
{
public interface IPlayerManager : ISharedPlayerManager
{
new IEnumerable<ICommonSession> Sessions { get; }
/// <summary>
/// Invoked when the list of sessions/players gets updated.
/// </summary>
event Action? PlayerListUpdated;
[ViewVariables]
IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity. See also <see cref="LocalPlayerAttachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerAttached;
[ViewVariables]
LocalPlayer? LocalPlayer { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from new entity. See also <see cref="LocalPlayerDetachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerDetached;
/// <summary>
/// Invoked after LocalPlayer is changed
/// </summary>
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
event EventHandler PlayerListUpdated;
/// <summary>
/// Sets up a single player game. This creates a dummy <see cref="ISharedPlayerManager.LocalSession"/> without an
/// <see cref="INetChannel"/>.
/// </summary>
void SetupSinglePlayer(string name);
void Initialize();
void Startup();
void Shutdown();
/// <summary>
/// Sets up the manager for a multiplayer game. This creates a <see cref="ISharedPlayerManager.LocalSession"/>
/// using the given <see cref="INetChannel"/>.
/// </summary>
void SetupMultiplayer(INetChannel channel);
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
}
public sealed class LocalPlayerChangedEventArgs : EventArgs
{
public readonly LocalPlayer? OldPlayer;
public readonly LocalPlayer? NewPlayer;
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
{
OldPlayer = oldPlayer;
NewPlayer = newPlayer;
}
}
[Obsolete("Use LocalSession instead")]
LocalPlayer? LocalPlayer { get;}
}
/// <summary>
/// ECS event that gets raised when the local player gets attached to a new entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerAttachedEvent : EntityEventArgs
{
public LocalPlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
/// <summary>
/// ECS event that gets raised when the local player gets detached from an entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerDetachedEvent : EntityEventArgs
{
public LocalPlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}

View File

@@ -1,11 +1,6 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
@@ -15,171 +10,30 @@ namespace Robust.Client.Player
/// </summary>
public sealed class LocalPlayer
{
/// <summary>
/// An entity has been attached to the local player.
/// </summary>
public event Action<EntityAttachedEventArgs>? EntityAttached;
/// <summary>
/// An entity has been detached from the local player.
/// </summary>
public event Action<EntityDetachedEventArgs>? EntityDetached;
public LocalPlayer(ICommonSession session)
{
Session = session;
}
/// <summary>
/// Game entity that the local player is controlling. If this is default, the player is not attached to any
/// entity at all.
/// </summary>
[ViewVariables] public EntityUid? ControlledEntity { get; private set; }
[ViewVariables] public NetUserId UserId { get; set; }
/// <summary>
/// Session of the local client.
/// </summary>
[ViewVariables]
public ICommonSession Session => InternalSession;
public EntityUid? ControlledEntity => Session.AttachedEntity;
internal PlayerSession InternalSession { get; set; } = default!;
[ViewVariables]
public NetUserId UserId => Session.UserId;
/// <summary>
/// OOC name of the local player.
/// </summary>
[ViewVariables]
public string Name { get; set; } = default!;
public string Name => Session.Name;
/// <summary>
/// The status of the client's session has changed.
/// Session of the local client.
/// </summary>
public event EventHandler<StatusEventArgs>? StatusChanged;
/// <summary>
/// Attaches a client to an entity.
/// </summary>
/// <param name="entity">Entity to attach the client to.</param>
public void AttachEntity(EntityUid entity, IEntityManager entMan, IBaseClient client)
{
if (ControlledEntity == entity)
return;
// Detach and cleanup first
DetachEntity();
if (!entMan.EntityExists(entity))
{
Logger.Error($"Attempting to attach player to non-existent entity {entity}!");
return;
}
ControlledEntity = entity;
InternalSession.AttachedEntity = entity;
if (!entMan.TryGetComponent<EyeComponent?>(entity, out var eye))
{
eye = entMan.AddComponent<EyeComponent>(entity);
if (client.RunLevel != ClientRunLevel.SinglePlayerGame)
{
Logger.Warning($"Attaching local player to an entity {entMan.ToPrettyString(entity)} without an eye. This eye will not be netsynced and may cause issues.");
}
eye.NetSyncEnabled = false;
}
eye.Current = true;
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
// notify ECS Systems
var eventBus = entMan.EventBus;
eventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(entity));
eventBus.RaiseLocalEvent(entity, new PlayerAttachedEvent(entity), true);
}
/// <summary>
/// Detaches the client from an entity.
/// </summary>
public void DetachEntity()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var previous = ControlledEntity;
if (entMan.TryGetComponent(previous, out MetaDataComponent? metaData) &&
metaData.EntityInitialized &&
!metaData.EntityDeleted)
{
entMan.GetComponent<EyeComponent>(previous.Value).Current = false;
}
ControlledEntity = null;
InternalSession.AttachedEntity = null;
if (previous != null)
{
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value));
}
}
/// <summary>
/// Changes the state of the session.
/// </summary>
public void SwitchState(SessionStatus newStatus)
{
SwitchState(Session.Status, newStatus);
}
/// <summary>
/// Changes the state of the session. This overload allows you to spoof the oldStatus, use with caution.
/// </summary>
public void SwitchState(SessionStatus oldStatus, SessionStatus newStatus)
{
var args = new StatusEventArgs(oldStatus, newStatus);
Session.Status = newStatus;
StatusChanged?.Invoke(this, args);
}
}
/// <summary>
/// Event arguments for when the status of a session changes.
/// </summary>
public sealed class StatusEventArgs : EventArgs
{
/// <summary>
/// Status that the session switched from.
/// </summary>
public SessionStatus OldStatus { get; }
/// <summary>
/// Status that the session switched to.
/// </summary>
public SessionStatus NewStatus { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public StatusEventArgs(SessionStatus oldStatus, SessionStatus newStatus)
{
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
public sealed class EntityDetachedEventArgs : EventArgs
{
public EntityDetachedEventArgs(EntityUid oldEntity)
{
OldEntity = oldEntity;
}
public EntityUid OldEntity { get; }
}
public sealed class EntityAttachedEventArgs : EventArgs
{
public EntityAttachedEventArgs(EntityUid newEntity)
{
NewEntity = newEntity;
}
public EntityUid NewEntity { get; }
public ICommonSession Session;
}
}

View File

@@ -2,15 +2,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
@@ -19,142 +17,187 @@ namespace Robust.Client.Player
/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated.
/// This class also communicates with the server to let the server control what entity it is attached to.
/// </summary>
public sealed class PlayerManager : IPlayerManager
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
{
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
/// <summary>
/// Active sessions of connected clients to the server.
/// Received player states that had an unknown <see cref="NetEntity"/>.
/// </summary>
private readonly Dictionary<NetUserId, ICommonSession> _sessions = new();
private Dictionary<NetUserId, SessionState> _pendingStates = new ();
private List<SessionState> _pending = new();
/// <inheritdoc />
public IEnumerable<ICommonSession> NetworkedSessions
public override ICommonSession[] NetworkedSessions
{
get
{
if (LocalPlayer is not null)
return new[] {LocalPlayer.Session};
return Enumerable.Empty<ICommonSession>();
return LocalSession != null
? new [] { LocalSession }
: Array.Empty<ICommonSession>();
}
}
/// <inheritdoc />
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
public LocalPlayer? LocalPlayer { get; set; }
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action<EntityUid>? LocalPlayerDetached;
public event Action<EntityUid>? LocalPlayerAttached;
/// <inheritdoc />
public int PlayerCount => _sessions.Values.Count;
/// <inheritdoc />
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
/// <inheritdoc />
[ViewVariables]
public LocalPlayer? LocalPlayer
public override void Initialize(int maxPlayers)
{
get => _localPlayer;
private set
{
if (_localPlayer == value) return;
var oldValue = _localPlayer;
_localPlayer = value;
LocalPlayerChanged?.Invoke(new LocalPlayerChangedEventArgs(oldValue, _localPlayer));
}
}
private LocalPlayer? _localPlayer;
public event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
/// <inheritdoc />
[ViewVariables]
IEnumerable<ICommonSession> IPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict => _sessions;
/// <inheritdoc />
public event EventHandler? PlayerListUpdated;
/// <inheritdoc />
public void Initialize()
{
_client.RunLevelChanged += OnRunLevelChanged;
base.Initialize(maxPlayers);
_network.RegisterNetMessage<MsgPlayerListReq>();
_network.RegisterNetMessage<MsgPlayerList>(HandlePlayerList);
PlayerStatusChanged += StatusChanged;
}
private void StatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session == LocalPlayer?.Session)
LocalStatusChanged?.Invoke(e);
}
/// <inheritdoc />
public void Startup()
public override void Startup()
{
DebugTools.Assert(LocalPlayer == null);
LocalPlayer = new LocalPlayer();
if (LocalSession == null)
throw new InvalidOperationException("LocalSession cannot be null");
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
LocalPlayer = new LocalPlayer(LocalSession);
base.Startup();
}
public void SetupSinglePlayer(string name)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
LocalSession = CreateAndAddSession(default, name);
Startup();
PlayerListUpdated?.Invoke();
}
public void SetupMultiplayer(INetChannel channel)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
var session = CreateAndAddSession(channel.UserId, channel.UserName);
session.Channel = channel;
LocalSession = session;
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
}
/// <inheritdoc />
public void Shutdown()
public override void Shutdown()
{
LocalPlayer?.DetachEntity();
if (LocalSession != null)
SetAttachedEntity(LocalSession, null);
LocalPlayer = null;
_sessions.Clear();
LocalSession = null;
_pendingStates.Clear();
base.Shutdown();
PlayerListUpdated?.Invoke();
}
/// <inheritdoc />
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
{
if (session.AttachedEntity == uid)
return;
var old = session.AttachedEntity;
base.SetAttachedEntity(session, uid);
if (session != LocalSession)
return;
if (old.HasValue)
{
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}.");
EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true);
LocalPlayerDetached?.Invoke(old.Value);
}
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
return;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
return;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
{
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
eye.NetSyncEnabled = false;
}
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
}
public void ApplyPlayerStates(IReadOnlyCollection<SessionState> list)
{
var dirty = ApplyStates(list, true);
if (_pendingStates.Count == 0)
{
// This is somewhat inefficient as it might try to re-apply states that failed just a moment ago.
_pending.Clear();
_pending.AddRange(_pendingStates.Values);
_pendingStates.Clear();
dirty |= ApplyStates(_pending, false);
}
if (dirty)
PlayerListUpdated?.Invoke();
}
private bool ApplyStates(IReadOnlyCollection<SessionState> list, bool fullList)
{
if (list.Count == 0)
{
// This happens when the server says "nothing changed!"
return;
}
return false;
DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application.
, "Received player state without being connected?");
DebugTools.Assert(LocalPlayer != null, "Call Startup()");
DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup.");
DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup.");
var myState = list.FirstOrDefault(s => s.UserId == LocalPlayer.UserId);
var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId);
if (myState != null)
bool dirty = false;
if (state != null)
{
UpdateAttachedEntity(myState.ControlledEntity);
UpdateSessionStatus(myState.Status);
dirty = true;
if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid)
&& state.ControlledEntity is { Valid:true } )
{
Sawmill.Error($"Received player state for local player with an unknown net entity!");
_pendingStates[state.UserId] = state;
}
else
{
_pendingStates.Remove(state.UserId);
}
SetAttachedEntity(LocalSession, uid);
SetStatus(LocalSession, state.Status);
}
UpdatePlayerList(list);
}
/// <summary>
/// Compares the server sessionStatus to the client one, and updates if needed.
/// </summary>
private void UpdateSessionStatus(SessionStatus myStateStatus)
{
if (LocalPlayer!.Session.Status != myStateStatus)
LocalPlayer.SwitchState(myStateStatus);
}
/// <summary>
/// Compares the server attachedEntity to the client one, and updates if needed.
/// </summary>
/// <param name="entity">AttachedEntity in the server session.</param>
private void UpdateAttachedEntity(EntityUid? entity)
{
if (LocalPlayer!.ControlledEntity == entity)
{
return;
}
if (entity == null)
{
LocalPlayer.DetachEntity();
return;
}
LocalPlayer.AttachEntity(entity.Value, _entManager, _client);
return UpdatePlayerList(list, fullList) || dirty;
}
/// <summary>
@@ -162,115 +205,87 @@ namespace Robust.Client.Player
/// </summary>
private void HandlePlayerList(MsgPlayerList msg)
{
UpdatePlayerList(msg.Plyrs);
ApplyPlayerStates(msg.Plyrs);
}
/// <summary>
/// Compares the server player list to the client one, and updates if needed.
/// </summary>
private void UpdatePlayerList(IEnumerable<PlayerState> remotePlayers)
private bool UpdatePlayerList(IEnumerable<SessionState> remotePlayers, bool fullList)
{
var dirty = false;
var hitSet = new List<NetUserId>();
var users = new List<NetUserId>();
foreach (var state in remotePlayers)
{
hitSet.Add(state.UserId);
users.Add(state.UserId);
if (_sessions.TryGetValue(state.UserId, out var session))
if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled)
&& state.ControlledEntity is {Valid: true})
{
var local = (PlayerSession) session;
// Exists, update data.
if (local.Name == state.Name
&& local.Status == state.Status
&& local.Ping == state.Ping
&& local.AttachedEntity == state.ControlledEntity)
{
continue;
}
dirty = true;
local.Name = state.Name;
local.Status = state.Status;
local.Ping = state.Ping;
local.AttachedEntity = state.ControlledEntity;
_pendingStates[state.UserId] = state;
}
else
{
// New, give him a slot.
dirty = true;
var newSession = new PlayerSession(state.UserId)
{
Name = state.Name,
Status = state.Status,
Ping = state.Ping,
AttachedEntity = state.ControlledEntity,
};
_sessions.Add(state.UserId, newSession);
if (state.UserId == LocalPlayer!.UserId)
{
LocalPlayer.InternalSession = newSession;
newSession.ConnectedClient = _network.ServerChannel!;
// We just connected to the server, hurray!
LocalPlayer.SwitchState(SessionStatus.Connecting, newSession.Status);
}
_pendingStates.Remove(state.UserId);
}
}
foreach (var existing in _sessions.Keys.ToArray())
{
// clear slot, player left
if (!hitSet.Contains(existing))
if (!InternalSessions.TryGetValue(state.UserId, out var session))
{
DebugTools.Assert(LocalPlayer!.UserId != existing || _client.RunLevel == ClientRunLevel.SinglePlayerGame, // replays apply player states.
"I'm still connected to the server, but i left?");
_sessions.Remove(existing);
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
newSession.Name = state.Name;
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled);
dirty = true;
continue;
}
// Check if the data is actually different
if (session.Name == state.Name
&& session.Status == state.Status
&& session.Ping == state.Ping
&& session.AttachedEntity == controlled)
{
continue;
}
dirty = true;
var local = (CommonSession) session;
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled);
}
// Remove old users. This only works if the provided state is a list of all players
if (fullList)
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
{
// clear slot, player left
if (users.Contains(oldUser))
continue;
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),
"Client is still connected to the server but not in the list of players?");
RemoveSession(oldUser);
_pendingStates.Remove(oldUser);
dirty = true;
}
}
if (dirty)
{
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
return dirty;
}
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (e.NewLevel != ClientRunLevel.SinglePlayerGame)
return;
DebugTools.AssertNotNull(LocalPlayer);
// We do some further setup steps for singleplayer here...
// The local player's GUID in singleplayer will always be the default.
var guid = default(NetUserId);
var session = new PlayerSession(guid)
if (LocalEntity == uid)
{
Name = LocalPlayer!.Name,
Ping = 0,
};
LocalPlayer.UserId = guid;
LocalPlayer.InternalSession = session;
// Add the local session to the list.
_sessions.Add(guid, session);
LocalPlayer.SwitchState(SessionStatus.InGame);
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (LocalPlayer?.ControlledEntity == uid)
{
session = LocalPlayer.Session;
session = LocalSession!;
return true;
}

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