Compare commits

...

95 Commits

Author SHA1 Message Date
Paul
0b749ff8bb makes prototypeinheritance opt in 2021-03-05 11:13:00 +01:00
Paul
069fa89fcb adds Try variants to FirstOrNull & FirstOrDefault
fixes ientity serialization when loading the map
2021-03-05 10:07:18 +01:00
Paul Ritter
80f9f24243 Serialization v3 aka constant suffering (#1606)
* oops

* fixes serialization il

* copytest

* typo & misc fixes

* 139 moment

* boxing

* mesa dum

* stuff

* goodbye bad friend

* last commit before the big (4) rewrite

* adds datanodes

* kills yamlobjserializer in favor of the new system

* adds more serializers, actually implements them & removes most of the last of the old system

* changed yamlfieldattribute namespace

* adds back iselfserialize

* refactors consts&flags

* renames everything to data(field/definition)

* adds afterserialization

* help

* dataclassgen

* fuggen help me mannen

* Fix most errors on content

* Fix engine errors except map loader

* maploader & misc fix

* misc fixes

* thing

* help

* refactors datanodes

* help me mannen

* Separate ITypeSerializer into reader and writer

* Convert all type serializers

* priority

* adds alot

* il fixes

* adds robustgen

* argh

* adds array & enum serialization

* fixes dataclasses

* adds vec2i / misc fixes

* fixes inheritance

* a very notcursed todo

* fixes some custom dataclasses

* push dis

* Remove data classes

* boutta box

* yes

* Add angle and regex serializer tests

* Make TypeSerializerTest abstract

* sets up ioc etc

* remove pushinheritance

* fixes

* Merge fixes, fix yaml hot reloading

* General fixes2

* Make enum serialization ignore case

* Fix the tag not being copied in data nodes

* Fix not properly serializing flag enums

* Fix component serialization on startup

* Implement ValueDataNode ToString

* Serialization IL fixes, fix return and string equality

* Remove async from prototype manager

* Make serializing unsupported node as enum exception more descriptive

* Fix serv3 tryread casting to serializer instead of reader

* Add constructor for invalid node type exception

* Temporary fix for SERV3: Turn populate delegate into regular code

* Fix not copying the data of non primitive types

* Fix not using the data definition found in copying

* Make ISerializationHooks require explicit implementations

* Add test for serialization inheritance

* Improve IsOverridenIn method

* Fix error message when a data definition is null

* Add method to cast a read value in Serv3Manager

* Rename IServ3Manager to ISerializationManager

* Rename usages of serv3manager, add generic copy method

* Fix IL copy method lookup

* Rename old usages of serv3manager

* Add ITypeCopier

* resistance is futile

* we will conquer this codebase

* Add copy method to all serializers

* Make primitive mismatch error message more descriptive

* bing bong im going to freacking heck

* oopsie moment

* hello are you interested in my wares

* does generic serializers under new architecture

* Convert every non generic serializer to the new format, general fixes

* Update usgaes of generic serializers, cleanup

* does some pushinheritance logic

* finishes pushinheritance FRAMEWORK

* shed

* Add box2, color and component registry serializer tests

* Create more deserialized types and store prototypes with their deserialized results

* Fixes and serializer updates

* Add serialization manager extensions

* adds pushinheritance

* Update all prototypes to have a parent and have consistent id/parent properties

* Fix grammar component serialization

* Add generic serializer tests

* thonk

* Add array serializer test

* Replace logger warning calls with exceptions

* fixes

* Move redundant methods to serialization manager extensions, cleanup

* Add array serialization

* fixes context

* more fixes

* argh

* inheritance

* this should do it

* fixes

* adds copiers & fixes some stuff

* copiers use context v1

* finishing copy context

* more context fixes

* Test fixes

* funky maps

* Fix server user interface component serialization

* Fix value tuple serialization

* Add copying for value types and arrays. Fix copy internal for primitives, enums and strings

* fixes

* fixes more stuff

* yes

* Make abstract/interface skips debugs instead of warnings

* Fix typo

* Make some dictionaries readonly

* Add checks for the serialization manager initializing and already being initialized

* Add base type required and usage for MeansDataDefinition and ImplicitDataDefinitionForInheritorsAttribute

* copy by ref

* Fix exception wording

* Update data field required summary with the new forbidden docs

* Use extension in map loader

* wanna erp

* Change serializing to not use il temporarily

* Make writing work with nullable types

* pushing

* check

* cuddling slaps HARD

* Add serialization priority test

* important fix

* a serialization thing

* serializer moment

* Add validation for some type serializers

* adds context

* moar context

* fixes

* Do the thing for appearance

* yoo lmao

* push haha pp

* Temporarily make copy delegate regular c# code

* Create deserialized component registry to handle not inheriting conflicting references

* YAML LINTER BABY

* ayes

* Fix sprite component norot not being default true like in latest master

* Remove redundant todos

* Add summary doc to every ISerializationManager method

* icon fixes

* Add skip hook argument to readers and copiers

* Merge fixes

* Fix ordering of arguments in read and copy reflection call

* Fix user interface components deserialization

* pew pew

* i am going to HECK

* Add MustUseReturnValue to copy-over methods

* Make serialization log calls use the same sawmill

* gamin

* Fix doc errors in ISerializationManager.cs

* goodbye brave soldier

* fixes

* WIP merge fixes and entity serialization

* aaaaaaaaaaaaaaa

* aaaaaaaaaaaaaaa

* adds inheritancebehaviour

* test/datafield fixes

* forgot that one

* adds more verbose validation

* This fixes the YAML hot reloading

* Replace yield break with Enumerable.Empty

* adds copiers

* aaaaaaaaaaaaa

* array fix
priority fix
misc fixes

* fix(?)

* fix.

* funny map serialization (wip)

* funny map serialization (wip)

* Add TODO

* adds proper info the validation

* Make yaml linter 5 times faster (~80% less execution time)

* Improves the error message for missing fields in the linter

* Include component name in unknown component type error node

* adds alwaysrelevant usa

* fixes mapsaving

* moved surpressor to analyzers proj

* warning cleanup & moves surpressor

* removes old msbuild targets

* Revert "Make yaml linter 5 times faster (~80% less execution time)"

This reverts commit 2ee4cc2c26.

* Add serialization to RobustServerSimulation and mock reflection methods
Fixes container tests

* Fix nullability warnings

* Improve yaml linter message feedback

* oops moment

* Add IEquatable, IComparable, ToString and operators to DataPosition
Rename it to NodeMark
Make it a readonly struct

* Remove try catch from enum parsing

* Make dependency management in serialization less bad

* Make dependencies an argument instead of a property on the serialization manager

* Clean up type serializers

* Improve validation messages and resourc epath checking

* Fix sprite error message

* reached perfection

Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Vera Aguilera Puerto <zddm@outlook.es>
2021-03-04 15:59:14 -08:00
Pieter-Jan Briers
93018c9843 Silence localization warnings on client again. 2021-03-03 16:02:30 +01:00
Pieter-Jan Briers
e2675271d0 Parallelize assembly sandbox checking harder. 2021-03-03 16:02:12 +01:00
Pieter-Jan Briers
d1f7edecef Use Directory.EnumerateFiles in PathHelpers.GetFiles.
Significant improvement in startup time.
2021-03-03 10:52:05 +01:00
Pieter-Jan Briers
b5a3c0b988 Do not load files under Locale/ not ending with .ftl.
Will ignore stuff like .DS_Store/.directory/thumbs.db
2021-03-02 21:22:44 +01:00
Acruid
06e62b031a SoundSystem (#1604)
* Adds the SoundSystem static proxy class for the AudioSystem.
Added a shared IAudioSystem interface for the future.

* Moved ConnectedClient property from IPlayerSession down to ICommonSession.

* Connected up the SoundSystem to the client/server AudioSystems.

* Converted client calls over to the new system.

* Marked the old serverside functions to play sound obsolete, use the new ones from the IAudioSystem.

* Added ISharedPlayerManager to the IoC registration.
2021-03-01 20:22:28 -08:00
Acruid
24707b7385 Shared Containers (#1579)
* Added a basic server simulation framework for help with tests.

* Moved as much as possible to Robust.Shared/Containers.
Moved ContainerSlot from content to engine.

* Moved ClientContainer to shared.

* Merged client/server ContainerManagerComponents into a single shared version.

* ContainerManagerComponent is now implicitly registered with the attributes.

* Migrated to 2021 serialization technology.

* Existing Unit Tests work.

* More tests coverage.
Fixed bug with transferring items between containers.

* Container Type info is now sent over the network.

* Merge client/server container systems.

* Code cleanup.

* Attempted to fix dictionary serialization.
Logs warning when trying to check if an unknown GridId is paused.

* Remove OldCode.
2021-03-01 15:19:59 -08:00
Pieter-Jan Briers
ab95f39f9f Localize SS14Window 2021-03-01 00:45:36 +01:00
Pieter-Jan Briers
cdd38abab5 Fix two shutdown crashes by removing IDisposable managers. 2021-02-28 23:10:03 +01:00
Pieter-Jan Briers
d751c0b3ab Revert "Physics (#1602)"
This reverts commit fefcc7cba3.
2021-02-28 18:45:18 +01:00
Pieter-Jan Briers
2ace0e9e5a Expose Patreon tier info from auth server. 2021-02-28 18:45:01 +01:00
Pieter-Jan Briers
31716f5104 Work around Roslyn scripting bug with ref structs. 2021-02-28 18:45:01 +01:00
metalgearsloth
fefcc7cba3 Physics (#1602)
* Physics worlds

* Paul's a good boy

* Build working

* Ingame and not lagging to hell

* Why didn't you commit ahhhhh

* Hard collisions working

* Solver parity

* Decent broadphase work done

* BroadPhase outline done

* BroadPhase working

* waiting for pvs

* Fix static PVS AABB

* Stop static bodies from awakening

* Optimise a bunch of stuff

* Even more broadphase stuff

* I'm fucking stupid

* Optimise fixture updates

* Collision solver start

* Building

* A is for Argumentative

* Fix contact caching island flags

* Circle shapes actually workeded

* Damping

* DS2 consumables only

* Slightly more stable

* Even slightlier more stablier

* VV your heart out

* Initial joint support

* 90% of joints I just wanted to push as I'd scream if I lost progress

* JOINT PURGATORY

* Joints barely functional lmao

* Okay these joints slightly more functional

* Remove station FrictionJoint

* Also that

* Some Box2D ports

* Cleanup mass

* Edge shape

* Active contacts

* Fix active contacts

* Optimise active contacts even more

* Boxes be stacking

* I would die for smug oh my fucking god

* In which everything is fixed

* Distance joints working LETS GO

* Remove frequency on distancejoint

* Fix some stuff and break joints

* Crashing fixed mehbeh

* ICollideSpecial and more resilience

* auto-clear

* showbb vera

* Slap that TODO in there

* Fix restartround crash

* Random fixes

* Fix fixture networking

* Add intersection method for broadphase

* Fix contacts

* Licenses done

* Optimisations

* Fix wall clips

* Config caching for island

* allocations optimisations

* Optimise casts

* Optimise events queue for physics

* Contact manager optimisations

* Optimise controllers

* Sloth joint or something idk

* Controller graph

* Remove content cvar

* Random cleanup

* Finally remove VirtualController

* Manifold structs again

* Optimise this absolute retardation

* Optimise

* fix license

* Cleanup physics interface

* AHHHHHHHHHHHHH

* Fix collisions again

* snivybus

* Fix potential nasty manifold bug

* Tests go snivy

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-01 03:09:36 +11:00
Paul
30df989e8d Merge branch 'serialization_v3_nodataclasses' of https://github.com/PaulRitter/RobustToolbox into master 2021-02-28 15:55:26 +01:00
Acruid
86bfea6bd4 ICommonSession Improvements + Player Filter (#1600)
* Removed IBaseSession, pushed all members down to ICommonSession.

* Pulled all members of client IPlayerSession into ICommonSession.
Marked client IPlayerSession as obsolete, use the base ICommonSession.

* Restricted setter access for properties in ICommonSession, only engine should be setting them.

* Fixed ping implementation on server.

* Moved AttachedEntityUid to ICommonSession.

* Added a shared IPlayerManager and pulled some common properties down to it.

* Added a shared player Filter class that holds a set of recipients in a networked call. Very useful for selecting recipients in a shared context.
2021-02-27 20:42:54 -08:00
Paul
d890f168c2 Spawner windows remember positions - engine commit 2021-02-27 12:27:46 +01:00
Paul
f888a810bf fixes that pesky warning 2021-02-27 11:58:51 +01:00
tmtmtl30
16249a4dde doubles default gain value (#1593) 2021-02-26 20:54:43 -08:00
Manel Navola
e33488ba55 Implemented erasing rectangular areas (#1419)
* Added support for erasing rectangular areas

* Apply suggestions from code review

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Switched sending start coordinate + end coordinate to sending start coordinate + rect selection size for preventing different parented positions, general code improvements

* Rewritten certain code part so the checks pass

* Added unshaded shader to rect erasing

* Tweaked alpha of erasing rectangle for better visualizing

Co-authored-by: Manel Navola <ManelNavola@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2021-02-26 20:52:12 -08:00
RemieRichards
bfe6eeddb1 Localization Fixes. Stop double-localizing localizationID-sourced names, PROPER returning "True" or "False" instead of "true" or "false" 2021-02-27 01:04:04 +00:00
RemieRichards
7f540e741c Add myself to CODEOWNERS for fluent translations. 2021-02-25 20:30:57 +00:00
Pieter-Jan Briers
b7855f5af5 Fix reloading localizations. 2021-02-25 20:47:17 +01:00
Pieter-Jan Briers
91391e1205 Update NetSerializer submodule 2021-02-25 12:06:28 +01:00
Pieter-Jan Briers
d5199ec459 Update NuGet packages. 2021-02-25 12:06:05 +01:00
Vera Aguilera Puerto
e1e6f4fd10 ContainerHelpers EmptyContainer now has an argument to attach removed entities to grid or map 2021-02-25 11:43:09 +01:00
Leo
e5b6fccf67 Add a scroll speed property to ScrollContainer (#1590)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-02-25 10:39:53 +00:00
RemberBL
95a912c329 Adds args.Handle(); into UI code for scrolling (#1595) 2021-02-25 11:08:19 +01:00
Pieter-Jan Briers
2b4833fc4e Allow content to read assembly versions in sandbox. 2021-02-24 12:18:44 +01:00
Pieter-Jan Briers
b814fc851a Fix more scrollbar DPI scaling bugs. 2021-02-24 12:18:29 +01:00
Pieter-Jan Briers
e87863203b Use DataFieldCached for AppearanceComponent.
What could go wrong?
2021-02-23 23:56:41 +01:00
Pieter-Jan Briers
33b66d9e18 Fix OpenCentered and OpenToLeft window methods. 2021-02-23 23:24:58 +01:00
Pieter-Jan Briers
fd406f7897 Selector-based VV windows have correct size.
Fixes #1594.
2021-02-23 23:10:58 +01:00
Pieter-Jan Briers
7a836d1018 Work around broken nullability.
Revert "Fix nullability errors"

This reverts commit a7f31f9ebf.

Revert "NotNullWhen()"

This reverts commit b332644d48.

Work around broken nullability.
2021-02-23 23:07:19 +01:00
Alex Evgrashin
393c15c44a Post shader will use real sprite bounding box (#1536)
Co-authored-by: Alex Evgrashin <evgrashin.adl@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-02-23 22:54:48 +01:00
Pieter-Jan Briers
a7f31f9ebf Fix nullability errors 2021-02-23 22:53:38 +01:00
RemieRichards
b332644d48 NotNullWhen() 2021-02-23 21:40:33 +00:00
RemieRichards
510f7c0e7c Merge branch 'master' of https://github.com/space-wizards/RobustToolbox into localization_grammar 2021-02-23 21:33:06 +00:00
RemieRichards
fdd05e3d3a Fix GrammarComponent gender parsing, Add tests for GENDER() function (which covers custom types (Enum) and custom functions (GENDER)) 2021-02-23 21:31:13 +00:00
Pieter-Jan Briers
6d41958853 Fix nullability of TryIndex<T>. 2021-02-23 22:25:48 +01:00
Pieter-Jan Briers
cecc4dfcf2 Improve SharedTransformSystem:
Do not fire events for deleted entities.
Optimize to remove allocations & LINQ.
2021-02-23 22:05:49 +01:00
Pieter-Jan Briers
4ac40f2e90 Make norot on sprites default for the time being.
To band aid the pulling issues.
2021-02-23 21:32:00 +01:00
Pieter-Jan Briers
3e12d44173 Bool/enum/entity handling for localization parameters. 2021-02-23 20:59:21 +01:00
RemieRichards
a42b39bd84 Adds GENDER(), PROPER() and ATTRIB() localization functions, GrammarComponent. 2021-02-23 19:53:56 +00:00
DrSmugleaf
22affccf24 Add individual layer offset (#1583)
* Add individual layer offset

* Fix error message

* Bring back layer offsetting
2021-02-23 12:55:45 +01:00
Pieter-Jan Briers
028724c47b Localization improvements:
*Allow content to define localization functions.
* Add rldloc command to reload localizations.
* Doc comments
* Error handling
* Parallelize loading of localization files, since I can only assume we'll have a lot eventually.
* Type system stuff to allow content to pass custom data types into fluent.
* Code cleanup.
2021-02-23 11:35:54 +01:00
Pieter-Jan Briers
0114bff2fc Add IFormattable to sandbox whitelist. 2021-02-23 11:27:51 +01:00
Pieter-Jan Briers
4ddbd644eb Add helper method to set up logging inside unit tests. 2021-02-23 11:27:42 +01:00
Pieter-Jan Briers
f0366531ef Inject the csi directly into my master. 2021-02-23 01:39:33 +01:00
Pieter-Jan Briers
6bd5814f4a Automatically fetch names and descs from loc. 2021-02-22 11:11:11 +01:00
Pieter-Jan Briers
78c132fdab Mute loc warnings for the time being. 2021-02-22 08:57:30 +01:00
Remie Richards
460cf57d7c Fluent Localization (#1584) 2021-02-22 00:36:02 +01:00
Pieter-Jan Briers
a3190a8aca Improvements to SpriteComponent for new angle changes: (#1589)
1. Fixes CopyFrom with new rotation parameters.
2. Adds a couple APIs for ClickableComponent content side.
2021-02-22 00:30:16 +01:00
Pieter-Jan Briers
6921fb2fbf Make UserInterfaceManager not dispose root control on game shutdown.
No real reason to do this and it only risks breaking something.
2021-02-21 23:52:09 +01:00
Fortune117
9954d571de Fix for Crash Caused by the Entity Spawn Menu (#1588) 2021-02-21 20:16:01 +01:00
Pieter-Jan Briers
17fe000a1e Fix math tests due to angle/direction changes. 2021-02-21 20:06:39 +01:00
Pieter-Jan Briers
fba415e765 Bit of work to make Direction.South == Angle.Zero.
This makes Direction no longer follow a cartesian trig circle but oh well. Also helper methods to work with this.

Math tests currently fail, pushing this so Acruid can see about fixing SpriteComponent.
2021-02-21 19:47:42 +01:00
Pieter-Jan Briers
583b7ebf38 WPF layout (#1581) 2021-02-21 12:28:13 +01:00
Acruid
771a256925 Fixes bug with an exception being throw when trying to overwrite a deleted Component. (#1587)
Entity now uses constructor injection instead of property injection.
2021-02-20 23:30:09 -08:00
Acruid
ae79e89347 Added a shared PointLightComponent interface. (#1585)
* Added a shared PointLightComponent interface.

* Fix unit tests.
2021-02-20 16:06:34 -08:00
Acruid
6c7eeb95eb Marks Register and RegisterReference obsolete in IComponentFactory. (#1582) 2021-02-20 12:18:37 -08:00
Acruid
de0bd1887f Sprite Rendering Bugfixes (#1551)
* Added documentation to Clyde on the sprite rendering calls.

* Added a rotation debug entity.

* Non-directional RSIs and raw textures are now rotated properly.

* Directional RSIs and Sprite Smoothing work.

* Remove the Directional flag usages.

* Supports layers with different numbers of directions.

* Fixes window rendering.
2021-02-20 11:06:08 -08:00
metalgearsloth
eb3a815d48 Remove AiLogicProcessor (#1568)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-02-19 21:52:32 -08:00
DrSmugleaf
e2a4dcdff1 Fix comparing by name and not ID for entity prototype updates (#1578) 2021-02-20 02:41:51 +01:00
DrSmugleaf
68b0d7bf2e Fix not clearing the queue after hot reload (#1576) 2021-02-20 01:43:56 +01:00
DrSmugleaf
a9b163992b Fix and add test for PrototypeManager LoadString (#1574) 2021-02-20 01:43:43 +01:00
DrSmugleaf
2409965cf8 Fix build (#1575) 2021-02-20 00:15:43 +01:00
DrSmugleaf
eada37378a Add YAML hot reloading (#1571)
* Implement hot reloading for entity prototypes

* Implement automatic prototype hot-reloading

* Merge fixes

* Add yaml hot reloading and a message to notify the client

* Add reloading only changed files, remove cooldown, add retries and remove IPrototype

* Remove reload command

* Make the client listen for reloads instead and only when focused

* Fix errors

* Only queue a reload when the queue has items in it

* Make fails after 10 retries log instead of throw if reloading

Co-authored-by: Jackson Lewis <inquisitivepenguin@protonmail.com>
2021-02-20 00:02:04 +01:00
DrSmugleaf
0f1da1ba2a Add window focused callback to Clyde (#1573) 2021-02-19 22:10:03 +01:00
Acruid
e0cdcd228e Fixed Timer Namespace in unit tests. 2021-02-18 20:35:34 -08:00
Acruid
fdb5e014b5 PauseManager moved to Shared (#1553)
* Moved IPauseManager from server to shared.

* Moved ITimerManager from Timers to Timing.

* Added missing IConsoleHost to server/client RegisterIoC. Tests work again.
2021-02-18 20:12:26 -08:00
DrSmugleaf
cefcad775b Make addcomp and rmcomp give better feedback and case insensitive (#1570)
* Make addcomp and rmcomp case insensitive

* Fix up names

* Make addcomp and rmcomp give better feedback

* Make addcomp and rmcomp less fail happy
2021-02-18 20:01:14 -08:00
Vera Aguilera Puerto
e40feac1f1 Adds VV autorefresh when right-clicking the refresh button. (#1558)
* Adds VV autorefresh when right-clicking the refresh button.

* cancel token on close

* button tooltip
2021-02-18 00:14:11 -08:00
DrSmugleaf
3ef4ac7452 Make component states dependant on the player getting them (#1569) 2021-02-17 23:48:17 -08:00
Pieter-Jan Briers
93bf1b09e7 Fix disconnecting while connecting causes you to be locked out of the server. 2021-02-17 23:22:11 +01:00
DrSmugleaf
a1e557e870 Add IPrototypeManager method to load a string (#1567) 2021-02-17 13:20:39 -08:00
Pieter-Jan Briers
864adb7445 Add DateTimeStyles to sandbox. 2021-02-17 11:52:36 +01:00
mirrorcult
9e3f3f0c1c vec2i serializer (#1563)
Co-authored-by: cyclowns <cyclowns@protonmail.ch>
2021-02-16 12:19:45 -08:00
DrSmugleaf
a40c4a435c Fix file not found exceptions when starting up the game with a debugger (#1562)
* Fix exceptions when starting up the game

* Remove try catches
2021-02-16 20:05:22 +01:00
DrSmugleaf
17182dd0e8 Engine PR for enabling nullability in Content.Client (#1565) 2021-02-16 20:05:06 +01:00
DrSmugleaf
d8b50044a2 Add (de)serialization for immutable lists (#1549) 2021-02-16 20:04:28 +01:00
Pieter-Jan Briers
4dc396e73d Fixes warning in TypePropertySerialization_Test.cs 2021-02-16 09:20:06 +01:00
Pieter-Jan Briers
6ae0b0e892 Fix [GenerateTypedNameReferences] with sealed types.
Fixes #1546
2021-02-16 09:19:57 +01:00
Pieter-Jan Briers
7162ca3456 Probably fix the bug where people get locked out of the server due to duplicate connction attempts. 2021-02-16 09:02:14 +01:00
Pieter-Jan Briers
1b44c1a1b8 Allow NotImplementedException in sandbox. 2021-02-15 17:57:38 +01:00
Clyybber
5b80b33e00 Change GetFileSystemInfos to EnumerateFileSystemInfos for iteration (#1561) 2021-02-15 16:26:16 +01:00
DrSmugleaf
f05c1b2395 Add Attribute generic constraint to IReflectionManager.FindTypesWithAttribute (#1548) 2021-02-14 02:14:00 +01:00
Pieter-Jan Briers
d9b2c73440 XamlUI improvements.
1. Add XAML namespaces https://spacestation14.io with Avalonia hacks.
2. Make markup extensions work with Avalonia hacks.
3. Add LocExtension for localized strings.
4. Add Vector2 parsing type converter to XAML compilation.
5. Make SS14Window better thanks to these improvements.
2021-02-14 02:09:37 +01:00
Pieter-Jan Briers
29a39f8e0a Adds LocExtension markup extension for XAML.
Does Loc.GetString().
2021-02-13 15:13:05 +01:00
Pieter-Jan Briers
2d72a2bdb5 Server timing improvements.
Add helpers.
Fix desynchronized NetTime.
2021-02-13 11:42:12 +01:00
Pieter-Jan Briers
91da635631 Fix Robust.Shared.Interfaces namespace in ScriptInstanceShared 2021-02-13 11:42:12 +01:00
chairbender
68ab3d504a Various fixes to support new chatbox (#1547)
* #272 no arrow, actually change id on channel changer

* #272 ability to apply style class to child

* #272 try methods for selecting items in OptionButton.cs

* #272 allow escaping right angle bracket in formatted message

* #272 ability to detect when local player is set / unset

* #272 make RemoveModal public since PushModal is as well, so modals can be removed on-demand if needed rather than relying on a click elsewhere

* #272 revert
2021-02-12 18:20:29 -08:00
Pieter-Jan Briers
5187040a64 Remove unsafe code from GrowableStack 2021-02-13 00:47:05 +01:00
DrSmugleaf
e0c63e7ce6 Add SerializedType attribute to specify the id used in !type tags (#1545)
* Add SerializedType attribute to serialize types without the !type tag

* Fix nulls in tests

* Fix null in tests maybe

* Return to type tags

* Fix imports
2021-02-11 13:50:55 -08:00
470 changed files with 16430 additions and 9302 deletions

17
.github/CODEOWNERS vendored
View File

@@ -1,14 +1,7 @@
# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# Last match in file takes precedence.
# These owners will be the default owners for everything in the repo.
# * @defunkt
* @Acruid @PJB3005 @Silvertorch5
# Be they Fluent translations or Freemarker templates, I know them both!
*.ftl @RemieRichards
# Order is important. The last matching pattern has the most precedence.
# So if a pull request only touches javascript files, only these owners
# will be requested to review.
# *.js @octocat @github/js
# You can also use email addresses if you prefer.
# docs/* docs@example.com
# Ping for all PRs
* @Acruid @PJB3005 @Silvertorch5

View File

@@ -574,29 +574,22 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- name: NGetText
- name: Fluent.Net
license: |
The MIT License (MIT)
blushingpenguin and Contributors
Copyright (c) 2012 Vitaly Zilnik
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
http://www.apache.org/licenses/LICENSE-2.0
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
- name: NetSerializer
license: |

View File

@@ -0,0 +1 @@
ss14window-placeholder-title = Exemplary Window Title Here

View File

@@ -0,0 +1,40 @@
- type: entity
id: debugRotation1
name: dbg_rotation1
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
sprite: debugRotation.rsi
state: direction1
placement:
mode: AlignTileAny
- type: entity
id: debugRotation4
name: dbg_rotation4
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
sprite: debugRotation.rsi
state: direction4
placement:
mode: AlignTileAny
- type: entity
id: debugRotationTex
name: dbg_rotationTex
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
texture: debugRotation.rsi/direction1.png
placement:
mode: AlignTileAny

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"states": [{
"name": "direction1",
"directions": 1,
"delays": [[1.0]]
}, {
"name": "direction4",
"directions": 4,
"delays": [[1.0], [1.0], [1.0], [1.0]]
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,10 @@
using Microsoft.CodeAnalysis;
namespace Robust.Generators
{
public static class Diagnostics
{
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Generators;
namespace Robust.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MeansImplicitAssigmentSuppressor : DiagnosticSuppressor
{
const string MeansImplicitAssignmentAttribute = "Robust.Shared.MeansImplicitAssignmentAttribute";
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
var implAttr = context.Compilation.GetTypeByMetadataName(MeansImplicitAssignmentAttribute);
foreach (var reportedDiagnostic in context.ReportedDiagnostics)
{
if(reportedDiagnostic.Id != Diagnostics.MeansImplicitAssignment.SuppressedDiagnosticId) continue;
var node = reportedDiagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(reportedDiagnostic.Location.SourceSpan);
if (node == null) continue;
var symbol = context.GetSemanticModel(reportedDiagnostic.Location.SourceTree).GetDeclaredSymbol(node);
if (symbol == null || !symbol.GetAttributes().Any(a =>
a.AttributeClass?.GetAttributes().Any(attr =>
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, implAttr)) == true))
{
continue;
}
context.ReportSuppression(Suppression.Create(
Diagnostics.MeansImplicitAssignment,
reportedDiagnostic));
}
}
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(Diagnostics.MeansImplicitAssignment);
}
}

View File

@@ -0,0 +1,37 @@
using System.Linq;
using Pidgin;
using static Pidgin.Parser;
namespace Robust.Build.Tasks
{
public static class MathParsing
{
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
public static Parser<char, float> Single1 { get; }
= Single.Between(SkipWhitespaces);
public static Parser<char, (float, float)> Single2 { get; }
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1]);
});
public static Parser<char, (float, float, float, float)> Single4 { get; }
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1], arr[2], arr[3]);
});
public static Parser<char, float[]> Thickness { get; }
= SkipWhitespaces.Then(
OneOf(
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4})),
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
Try(Single1.Select(c => new[] {c}))
));
}
}

View File

@@ -0,0 +1,32 @@
using System.Reflection.Emit;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
internal class RXamlColorAstNode
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
{
private readonly IXamlMethod _method;
private readonly string _color;
public RXamlColorAstNode(IXamlLineInfo lineInfo, RXamlWellKnownTypes types, string color) : base(lineInfo)
{
_color = color;
Type = new XamlAstClrTypeReference(lineInfo, types.Color, false);
_method = types.ColorFromXaml;
}
public IXamlAstTypeReference Type { get; }
public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldstr(_color);
codeGen.EmitCall(_method);
return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Linq;
using System.Reflection.Emit;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
public abstract class RXamlVecLikeConstAstNode<T>
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
where T : unmanaged
{
private readonly IXamlConstructor _constructor;
protected readonly T[] Values;
public RXamlVecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, T[] values)
: base(lineInfo)
{
_constructor = constructor;
Values = values;
var @params = constructor.Parameters;
if (@params.Count != values.Length)
throw new ArgumentException("Invalid amount of parameters");
if (@params.Any(c => c != componentType))
throw new ArgumentException("Invalid constructor: not all parameters match component type");
Type = new XamlAstClrTypeReference(lineInfo, type, false);
}
public IXamlAstTypeReference Type { get; }
public virtual XamlILNodeEmitResult Emit(
XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
codeGen.Newobj(_constructor);
return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
}
public sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
{
public RXamlSingleVecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, float[] values)
: base(lineInfo, type, constructor, componentType, values)
{
}
public override XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
foreach (var value in Values)
{
codeGen.Emit(OpCodes.Ldc_R4, value);
}
return base.Emit(context, codeGen);
}
}
public sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
{
public RXamlInt32VecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, int[] values)
: base(lineInfo, type, constructor, componentType, values)
{
}
public override XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
foreach (var value in Values)
{
codeGen.Emit(OpCodes.Ldc_I4, value);
}
return base.Emit(context, codeGen);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Linq;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
class RXamlWellKnownTypes
{
public XamlTypeWellKnownTypes XamlIlTypes { get; }
public IXamlType Single { get; }
public IXamlType Int32 { get; }
public IXamlType Vector2 { get; }
public IXamlConstructor Vector2ConstructorFull { get; }
public IXamlType Vector2i { get; }
public IXamlConstructor Vector2iConstructorFull { get; }
public IXamlType Thickness { get; }
public IXamlConstructor ThicknessConstructorFull { get; }
public IXamlType Color { get; }
public IXamlMethod ColorFromXaml { get; }
public RXamlWellKnownTypes(TransformerConfiguration cfg)
{
var ts = cfg.TypeSystem;
XamlIlTypes = cfg.WellKnownTypes;
Single = ts.GetType("System.Single");
Int32 = ts.GetType("System.Int32");
(Vector2, Vector2ConstructorFull) = GetNumericTypeInfo("Robust.Shared.Maths.Vector2", Single, 2);
(Vector2i, Vector2iConstructorFull) = GetNumericTypeInfo("Robust.Shared.Maths.Vector2i", Int32, 2);
(Thickness, ThicknessConstructorFull) = GetNumericTypeInfo("Robust.Shared.Maths.Thickness", Single, 4);
(IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount)
{
var type = cfg.TypeSystem.GetType(name);
var ctor = type.GetConstructor(Enumerable.Repeat(componentType, componentCount).ToList());
return (type, ctor);
}
Color = cfg.TypeSystem.GetType("Robust.Shared.Maths.Color");
ColorFromXaml = Color.GetMethod(new FindMethodMethodSignature("FromXaml", Color, XamlIlTypes.String)
{
IsStatic = true
});
}
}
static class RXamlWellKnownTypesExtensions
{
public static RXamlWellKnownTypes GetRobustTypes(this AstTransformationContext ctx)
{
if (ctx.TryGetItem<RXamlWellKnownTypes>(out var rv))
return rv;
ctx.SetItem(rv = new RXamlWellKnownTypes(ctx.Configuration));
return rv;
}
}
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="16.8.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,6 +6,7 @@ using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Pidgin;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
@@ -70,7 +71,7 @@ namespace Robust.Build.Tasks
{
XmlnsAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.XmlnsDefinitionAttribute"),
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
},
ContentAttributes =
@@ -98,7 +99,7 @@ namespace Robust.Build.Tasks
typeSystem,
typeSystem.TargetAssembly,
xamlLanguage,
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage));
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
@@ -297,6 +298,112 @@ namespace Robust.Build.Tasks
return true;
}
private static bool CustomValueConverter(
AstTransformationContext context,
IXamlAstValueNode node,
IXamlType type,
out IXamlAstValueNode result)
{
if (!(node is XamlAstTextNode textNode))
{
result = null;
return false;
}
var text = textNode.Text;
var types = context.GetRobustTypes();
if (type.Equals(types.Vector2))
{
var foo = MathParsing.Single2.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
var (x, y) = foo.Value;
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Vector2, types.Vector2ConstructorFull,
types.Single, new[] {x, y});
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Color))
{
// TODO: Interpret these colors at XAML compile time instead of at runtime.
result = new RXamlColorAstNode(node, types, text);
return true;
}
result = null;
return false;
}
public const string ContextNameScopeFieldName = "RobustNameScope";
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)

View File

@@ -24,6 +24,7 @@ namespace Robust.Client.NameGenerator
{
private const string AttributeName = "Robust.Client.AutoGenerated.GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute";
private const string AttributeCode = @"// <auto-generated />
using System;
namespace Robust.Client.AutoGenerated
@@ -54,8 +55,8 @@ namespace Robust.Client.AutoGenerated
{
var clrtype = objectNode.Type.GetClrType();
var isControl = IsControl(clrtype);
//clrtype.Interfaces.Any(i =>
//i.IsInterface && i.FullName == "Robust.Client.UserInterface.IControl");
//clrtype.Interfaces.Any(i =>
//i.IsInterface && i.FullName == "Robust.Client.UserInterface.IControl");
if (!isControl)
return node;
@@ -80,9 +81,13 @@ namespace Robust.Client.AutoGenerated
return node;
}
public void Push(IXamlAstNode node) { }
public void Push(IXamlAstNode node)
{
}
public void Pop() { }
public void Pop()
{
}
}
private static string GenerateSourceCode(
@@ -97,16 +102,20 @@ namespace Robust.Client.AutoGenerated
var compiler =
new XamlILCompiler(
new TransformerConfiguration(typeSystem, typeSystem.Assemblies[0],
new XamlLanguageTypeMappings(typeSystem)),
new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes = {typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute")}
}),
new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>(), false);
compiler.Transformers.Add(new TypeReferenceResolver());
compiler.Transform(parsed);
var initialRoot = (XamlAstObjectNode) parsed.Root;
var names = NameVisitor.GetNames(initialRoot);
var fieldAccess = classSymbol.IsSealed ? "private" : "protected";
//var names = NameVisitor.GetNames((XamlAstObjectNode)XDocumentXamlParser.Parse(xamlFile).Root);
var namedControls = names.Select(info => " " +
$"protected global::{info.type} {info.name} => " +
$"this.FindControl<global::{info.type}>(\"{info.name}\");");
$"{fieldAccess} global::{info.type} {info.name} => " +
$"this.FindControl<global::{info.type}>(\"{info.name}\");");
return $@"// <auto-generated />
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -124,7 +133,7 @@ namespace {nameSpace}
public void Execute(GeneratorExecutionContext context)
{
var comp = (CSharpCompilation) context.Compilation;
if(comp.GetTypeByMetadataName(AttributeName) == null)
if (comp.GetTypeByMetadataName(AttributeName) == null)
context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8));
if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver))
{
@@ -132,7 +141,7 @@ namespace {nameSpace}
}
var symbols = UnpackAnnotatedTypes(context, comp, receiver);
if(symbols == null)
if (symbols == null)
return;
foreach (var typeSymbol in symbols)
@@ -168,7 +177,8 @@ namespace {nameSpace}
"Usage",
DiagnosticSeverity.Error,
true),
Location.Create(xamlFileName, new TextSpan(0,0), new LinePositionSpan(new LinePosition(0,0),new LinePosition(0,0)))));
Location.Create(xamlFileName, new TextSpan(0, 0),
new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 0)))));
continue;
}
@@ -194,7 +204,8 @@ namespace {nameSpace}
}
}
private IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(in GeneratorExecutionContext context, CSharpCompilation comp, NameReferenceSyntaxReceiver receiver)
private IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(in GeneratorExecutionContext context,
CSharpCompilation comp, NameReferenceSyntaxReceiver receiver)
{
var options = (CSharpParseOptions) comp.SyntaxTrees[0].Options;
var compilation =

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
/// <seealso cref="AnimationPlayerComponent"/>
public sealed class Animation
{
public readonly List<AnimationTrack> AnimationTracks = new();
public List<AnimationTrack> AnimationTracks { get; private set; } = new();
public TimeSpan Length { get; set; }
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Robust.Client.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Client.Animations
{
@@ -14,7 +15,7 @@ namespace Robust.Client.Animations
/// <summary>
/// A list of key frames for when to fire flicks.
/// </summary>
public readonly List<KeyFrame> KeyFrames = new();
public List<KeyFrame> KeyFrames { get; private set; } = new();
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
{
@@ -36,8 +37,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
EntitySystem.Get<AudioSystem>()
.Play(keyFrame.Resource, entity, keyFrame.AudioParamsFunc.Invoke());
SoundSystem.Play(Filter.Local(), keyFrame.Resource, entity, keyFrame.AudioParamsFunc.Invoke());
}
return (keyFrameIndex, playingTime);

View File

@@ -10,7 +10,7 @@ namespace Robust.Client.Animations
/// </summary>
public abstract class AnimationTrackProperty : AnimationTrack
{
public readonly List<KeyFrame> KeyFrames = new();
public List<KeyFrame> KeyFrames { get; protected set; } = new();
/// <summary>
/// How to interpolate values when between two keyframes.

View File

@@ -15,7 +15,7 @@ namespace Robust.Client.Animations
/// <summary>
/// A list of key frames for when to fire flicks.
/// </summary>
public readonly List<KeyFrame> KeyFrames = new();
public List<KeyFrame> KeyFrames { get; private set; } = new();
// TODO: Should this layer key be per keyframe maybe?
/// <summary>

View File

@@ -63,9 +63,11 @@ namespace Robust.Client.Audio.Midi
bool IsAvailable { get; }
public int OcclusionCollisionMask { get; set; }
void Shutdown();
}
internal class MidiManager : IDisposable, IMidiManager
internal class MidiManager : IMidiManager
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
@@ -352,7 +354,7 @@ namespace Robust.Client.Audio.Midi
}
}
public void Dispose()
public void Shutdown()
{
_alive = false;
_midiThread?.Join();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Client.Audio.Midi;
using Robust.Client.Console;
using Robust.Client.Debugging;
@@ -10,6 +10,7 @@ using Robust.Client.Input;
using Robust.Client.Map;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.Prototypes;
using Robust.Client.Reflection;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
@@ -17,13 +18,17 @@ using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
namespace Robust.Client
{
@@ -33,7 +38,7 @@ namespace Robust.Client
{
SharedIoC.RegisterIoC();
IoCManager.Register<IPrototypeManager, PrototypeManager>();
IoCManager.Register<IPrototypeManager, ClientPrototypeManager>();
IoCManager.Register<IEntityManager, ClientEntityManager>();
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
@@ -51,6 +56,7 @@ namespace Robust.Client
IoCManager.Register<IClientGameStateManager, ClientGameStateManager>();
IoCManager.Register<IBaseClient, BaseClient>();
IoCManager.Register<IPlayerManager, PlayerManager>();
IoCManager.Register<ISharedPlayerManager, PlayerManager>();
IoCManager.Register<IStateManager, StateManager>();
IoCManager.Register<IUserInterfaceManager, UserInterfaceManager>();
IoCManager.Register<IUserInterfaceManagerInternal, UserInterfaceManager>();
@@ -59,6 +65,7 @@ namespace Robust.Client
IoCManager.Register<ILightManager, LightManager>();
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IMidiManager, MidiManager>();
@@ -96,7 +103,6 @@ namespace Robust.Client
IoCManager.Register<IViewVariablesManagerInternal, ViewVariablesManager>();
IoCManager.Register<IClientConGroupController, ClientConGroupController>();
IoCManager.Register<IScriptClient, ScriptClient>();
//IoCManager.Register<IXamlCompiler, XamlCompiler>();
}
}
}

View File

@@ -12,7 +12,6 @@ using Robust.Client.Input;
using Robust.Client.Debugging;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.ResourceManagement.ResourceTypes;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -481,6 +480,8 @@ namespace Robust.Client.Console.Commands
{
_writeNode(root, 0, writer);
}
shell.WriteLine("Saved guidump");
}
private static void _writeNode(Control control, int indents, TextWriter writer)
@@ -542,7 +543,7 @@ namespace Robust.Client.Console.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var window = new SS14Window { CustomMinimumSize = (500, 400)};
var window = new SS14Window { MinSize = (500, 400)};
var tabContainer = new TabContainer();
window.Contents.AddChild(tabContainer);
var scroll = new ScrollContainer();
@@ -562,7 +563,7 @@ namespace Robust.Client.Console.Commands
optionButton.OnItemSelected += eventArgs => optionButton.SelectId(eventArgs.Id);
vBox.AddChild(optionButton);
var tree = new Tree { SizeFlagsVertical = Control.SizeFlags.FillExpand };
var tree = new Tree { VerticalExpand = true };
var root = tree.CreateItem();
root.Text = "Honk!";
var child = tree.CreateItem();
@@ -599,7 +600,7 @@ namespace Robust.Client.Console.Commands
{
grid.AddChild(new Button
{
CustomMinimumSize = (50, 50),
MinSize = (50, 50),
Text = $"{x}, {y}"
});
}
@@ -631,6 +632,29 @@ namespace Robust.Client.Console.Commands
}
});
tabContainer.AddChild(new HSplitContainer
{
Children =
{
new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.Red},
Children =
{
new Label{ Text = "FOOBARBAZ"},
}
},
new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.Blue},
Children =
{
new Label{ Text = "FOOBARBAZ"},
}
},
}
});
window.OpenCentered();
}
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands
{
internal sealed class ReloadLocalizationsCommand : IConsoleCommand
{
public string Command => "rldloc";
public string Description => "Reloads localization (client & server)";
public string Help => "Usage: rldloc";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<ILocalizationManager>().ReloadLocalizations();
shell.RemoteExecuteCommand("sudo rldloc");
}
}
}

View File

@@ -1,5 +1,8 @@
#if CLIENT_SCRIPTING
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
@@ -36,6 +39,8 @@ namespace Robust.Client.Console
private readonly ScriptGlobals _globals;
private ScriptState? _state;
private (string[] imports, string code)? _autoImportRepeatBuffer;
public ScriptConsoleClient()
{
Title = Loc.GetString("Robust C# Interactive (CLIENT)");
@@ -54,38 +59,56 @@ namespace Robust.Client.Console
var code = InputBar.Text;
InputBar.Clear();
// Remove > or . at the end of the output panel.
OutputPanel.RemoveEntry(^1);
_inputBuffer.AppendLine(code);
_linesEntered += 1;
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()), ScriptInstanceShared.ParseOptions);
if (!SyntaxFactory.IsCompleteSubmission(tree))
if (_autoImportRepeatBuffer.HasValue && code == "y")
{
if (_linesEntered == 1)
var (imports, repeatCode) = _autoImportRepeatBuffer.Value;
var sb = new StringBuilder();
foreach (var import in imports)
{
OutputPanel.AddText($"> {code}");
sb.AppendFormat("using {0};\n", import);
}
else
{
OutputPanel.AddText($". {code}");
}
OutputPanel.AddText(".");
return;
sb.Append(repeatCode);
code = sb.ToString();
}
code = _inputBuffer.ToString().Trim();
// Remove echo of partial submission from the output panel.
for (var i = 1; i < _linesEntered; i++)
else
{
// Remove > or . at the end of the output panel.
OutputPanel.RemoveEntry(^1);
}
_inputBuffer.Clear();
_linesEntered = 0;
_inputBuffer.AppendLine(code);
_linesEntered += 1;
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()),
ScriptInstanceShared.ParseOptions);
if (!SyntaxFactory.IsCompleteSubmission(tree))
{
if (_linesEntered == 1)
{
OutputPanel.AddText($"> {code}");
}
else
{
OutputPanel.AddText($". {code}");
}
OutputPanel.AddText(".");
return;
}
code = _inputBuffer.ToString().Trim();
// Remove echo of partial submission from the output panel.
for (var i = 1; i < _linesEntered; i++)
{
OutputPanel.RemoveEntry(^1);
}
_inputBuffer.Clear();
_linesEntered = 0;
}
Script newScript;
@@ -135,6 +158,8 @@ namespace Robust.Client.Console
OutputPanel.AddMessage(msg);
OutputPanel.AddText(">");
PromptAutoImports(e.Diagnostics, code);
return;
}
@@ -148,13 +173,23 @@ namespace Robust.Client.Console
else if (ScriptInstanceShared.HasReturnValue(newScript))
{
var msg = new FormattedMessage();
msg.AddText(CSharpObjectFormatter.Instance.FormatObject(_state.ReturnValue));
msg.AddText(ScriptInstanceShared.SafeFormat(_state.ReturnValue));
OutputPanel.AddMessage(msg);
}
OutputPanel.AddText(">");
}
private void PromptAutoImports(IEnumerable<Diagnostic> diags, string code)
{
if (!ScriptInstanceShared.CalcAutoImports(_reflectionManager, diags, out var found))
return;
OutputPanel.AddText($"Auto-import {string.Join(", ", found)} (enter 'y')?");
_autoImportRepeatBuffer = (found.ToArray(), code);
}
private sealed class ScriptGlobalsImpl : ScriptGlobals
{
private readonly ScriptConsoleClient _owner;
@@ -180,7 +215,7 @@ namespace Robust.Client.Console
public override void show(object obj)
{
write(CSharpObjectFormatter.Instance.FormatObject(obj));
write(ScriptInstanceShared.SafeFormat(obj));
}
}
}

View File

@@ -20,8 +20,6 @@ namespace Robust.Client.Console
{
private readonly IReflectionManager _reflectionManager;
protected override Vector2? CustomSize => (300, 300);
private readonly VBoxContainer _watchesVBox;
private readonly LineEdit _addWatchEdit;
private readonly Button _addWatchButton;
@@ -37,12 +35,12 @@ namespace Robust.Client.Console
var mainVBox = new VBoxContainer
{
CustomMinimumSize = (500, 300),
MinSize = (500, 300),
Children =
{
(_watchesVBox = new VBoxContainer
{
SizeFlagsVertical = SizeFlags.FillExpand
VerticalExpand = true
}),
new HBoxContainer
{
@@ -50,7 +48,7 @@ namespace Robust.Client.Console
{
(_addWatchEdit = new HistoryLineEdit
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
HorizontalExpand = true,
PlaceHolder = Loc.GetString("Add watch (C# interactive)")
}),
(_addWatchButton = new Button
@@ -66,6 +64,8 @@ namespace Robust.Client.Console
_addWatchEdit.OnTextEntered += _ => AddWatch();
Contents.AddChild(mainVBox);
SetSize = (300, 300);
}
private void AddWatch()
@@ -113,7 +113,7 @@ namespace Robust.Client.Console
{
(_outputLabel = new Label
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
HorizontalExpand = true,
ClipText = true
}),
(delButton = new Button
@@ -176,7 +176,7 @@ namespace Robust.Client.Console
{
Text = message,
ClipText = true,
SizeFlagsHorizontal = SizeFlags.FillExpand
HorizontalExpand = true
},
(delButton = new Button {Text = Loc.GetString("Remove")})
}

View File

@@ -1,7 +1,8 @@
using System;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Robust.Client.Audio.Midi;
using Robust.Client.Console;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
@@ -26,7 +27,7 @@ using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timers;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -44,7 +45,7 @@ namespace Robust.Client
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IClientConsoleHost _console = default!;
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
@@ -62,6 +63,7 @@ namespace Robust.Client
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
[Dependency] private readonly IAuthManager _authManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
private CommandLineArgs? _commandLineArgs;
private bool _disableAssemblyLoadContext;
@@ -153,6 +155,8 @@ namespace Robust.Client
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
IoCManager.Resolve<ISerializationManager>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
@@ -162,7 +166,8 @@ namespace Robust.Client
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_consoleHost.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
_prototypeManager.Resync();
_mapManager.Initialize();
@@ -318,6 +323,7 @@ namespace Robust.Client
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Error;
#if DEBUG_ONLY_FCE_INFO
#if DEBUG_ONLY_FCE_LOG
@@ -376,6 +382,8 @@ namespace Robust.Client
private void Cleanup()
{
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
}

View File

@@ -17,7 +17,6 @@ namespace Robust.Client
RegisterReflection();
}
internal static void RegisterReflection()
{
// Gets a handle to the shared and the current (client) dll.

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Robust.Client.GameObjects
@@ -25,7 +26,6 @@ namespace Robust.Client.GameObjects
RegisterReference<PhysicsComponent, IPhysBody>();
RegisterReference<PhysicsComponent, IPhysicsComponent>();
RegisterIgnore("KeyBindingInput");
Register<PointLightComponent>();
Register<InputComponent>();
@@ -50,14 +50,10 @@ namespace Robust.Client.GameObjects
Register<AnimationPlayerComponent>();
Register<ContainerManagerComponent>();
RegisterReference<ContainerManagerComponent, IContainerManager>();
Register<TimerComponent>();
#if DEBUG
Register<DebugExceptionOnAddComponent>();
Register<DebugExceptionExposeDataComponent>();
Register<DebugExceptionInitializeComponent>();
Register<DebugExceptionStartupComponent>();
#endif

View File

@@ -4,8 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
@@ -15,13 +14,11 @@ namespace Robust.Client.GameObjects
{
[ViewVariables]
private Dictionary<object, object> data = new();
[ViewVariables]
[DataField("visuals")]
internal List<AppearanceVisualizer> Visualizers = new();
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
private static bool _didRegisterSerializer;
[ViewVariables]
private bool _appearanceDirty;
@@ -107,18 +104,6 @@ namespace Robust.Client.GameObjects
_appearanceDirty = false;
}
public override void ExposeData(ObjectSerializer serializer)
{
if (!_didRegisterSerializer)
{
YamlObjectSerializer.RegisterTypeSerializer(typeof(AppearanceVisualizer),
new VisualizerTypeSerializer(_reflectionManager));
_didRegisterSerializer = true;
}
serializer.DataField(ref Visualizers, "visuals", new List<AppearanceVisualizer>());
}
public override void Initialize()
{
base.Initialize();
@@ -131,78 +116,6 @@ namespace Robust.Client.GameObjects
MarkDirty();
}
class VisualizerTypeSerializer : YamlObjectSerializer.TypeSerializer
{
private readonly IReflectionManager _reflectionManager;
public VisualizerTypeSerializer(IReflectionManager reflectionManager)
{
_reflectionManager = reflectionManager;
}
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
{
var mapping = (YamlMappingNode) node;
var nodeType = mapping.GetNode("type");
switch (nodeType.AsString())
{
case SpriteLayerToggle.NAME:
var keyString = mapping.GetNode("key").AsString();
object key;
if (_reflectionManager.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
var layer = mapping.GetNode("layer").AsInt();
return new SpriteLayerToggle(key, layer);
default:
var visType = _reflectionManager.LooseGetType(nodeType.AsString());
if (!typeof(AppearanceVisualizer).IsAssignableFrom(visType))
{
throw new InvalidOperationException();
}
var vis = (AppearanceVisualizer) Activator.CreateInstance(visType)!;
vis.LoadData(mapping);
return vis;
}
}
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
{
switch (obj)
{
case SpriteLayerToggle spriteLayerToggle:
YamlScalarNode key;
if (spriteLayerToggle.Key is Enum)
{
var name = spriteLayerToggle.Key.GetType().FullName;
key = new YamlScalarNode($"{name}.{spriteLayerToggle.Key}");
}
else
{
key = new YamlScalarNode(spriteLayerToggle.Key.ToString());
}
return new YamlMappingNode
{
{new YamlScalarNode("type"), new YamlScalarNode(SpriteLayerToggle.NAME)},
{new YamlScalarNode("key"), key},
{new YamlScalarNode("layer"), new YamlScalarNode(spriteLayerToggle.SpriteLayer.ToString())},
};
default:
// TODO: A proper way to do serialization here.
// I can't use the ExposeData system here since that's specific to entity serializers.
return new YamlMappingNode();
}
}
}
internal class SpriteLayerToggle : AppearanceVisualizer
{
@@ -223,15 +136,9 @@ namespace Robust.Client.GameObjects
/// Handles the visualization of data inside of an appearance component.
/// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract class AppearanceVisualizer
{
/// <summary>
/// Load data from the prototype declaring this visualizer, to configure settings and such.
/// </summary>
public virtual void LoadData(YamlMappingNode node)
{
}
/// <summary>
/// Initializes an entity to be managed by this appearance controller.
/// DO NOT assume this is your only entity. Visualizers are shared.

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
public sealed partial class ContainerManagerComponent
{
[DebuggerDisplay("ClientContainer {Owner.Uid}/{ID}")]
private sealed class ClientContainer : IContainer
{
public List<IEntity> Entities { get; } = new List<IEntity>();
public ClientContainer(string id, ContainerManagerComponent manager)
{
ID = id;
Manager = manager;
}
[ViewVariables] public IContainerManager Manager { get; }
[ViewVariables] public string ID { get; }
[ViewVariables] public IEntity Owner => Manager.Owner;
[ViewVariables] public bool Deleted { get; private set; }
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => Entities;
[ViewVariables]
public bool ShowContents { get; set; }
[ViewVariables]
public bool OccludesLight { get; set; }
public bool CanInsert(IEntity toinsert)
{
return false;
}
public bool Insert(IEntity toinsert)
{
return false;
}
public bool CanRemove(IEntity toremove)
{
return false;
}
public bool Remove(IEntity toremove)
{
return false;
}
public void ForceRemove(IEntity toRemove)
{
throw new NotSupportedException("Cannot directly modify containers on the client");
}
public bool Contains(IEntity contained)
{
return Entities.Contains(contained);
}
public void DoInsert(IEntity entity)
{
Entities.Add(entity);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new UpdateContainerOcclusionMessage(entity));
}
public void DoRemove(IEntity entity)
{
Entities.Remove(entity);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new UpdateContainerOcclusionMessage(entity));
}
public void Shutdown()
{
Deleted = true;
}
}
public override void InternalContainerShutdown(IContainer container)
{
}
}
}

View File

@@ -1,168 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
public sealed partial class ContainerManagerComponent : SharedContainerManagerComponent
{
[ViewVariables]
private readonly Dictionary<string, ClientContainer> _containers = new();
public override T MakeContainer<T>(string id)
{
throw new NotSupportedException("Cannot modify containers on the client.");
}
public override bool Remove(IEntity entity)
{
// TODO: This will probably need relaxing if we want to predict things like inventories.
throw new NotSupportedException("Cannot modify containers on the client.");
}
protected override IEnumerable<IContainer> GetAllContainersImpl()
{
return _containers.Values.Where(c => !c.Deleted);
}
public override IContainer GetContainer(string id)
{
return _containers[id];
}
public override bool HasContainer(string id)
{
return _containers.ContainsKey(id);
}
public override bool TryGetContainer(string id, [NotNullWhen(true)] out IContainer? container)
{
var ret = _containers.TryGetValue(id, out var cont);
container = cont!;
return ret;
}
/// <inheritdoc />
public override bool TryGetContainer(IEntity entity, [NotNullWhen(true)] out IContainer? container)
{
foreach (var contain in _containers.Values)
{
if (!contain.Deleted && contain.Contains(entity))
{
container = contain;
return true;
}
}
container = default;
return false;
}
public override bool ContainsEntity(IEntity entity)
{
foreach (var container in _containers.Values)
{
if (!container.Deleted && container.Contains(entity))
{
return true;
}
}
return false;
}
public override void ForceRemove(IEntity entity)
{
throw new NotSupportedException("Cannot modify containers on the client.");
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if(!(curState is ContainerManagerComponentState cast))
return;
// Delete now-gone containers.
List<string>? toDelete = null;
foreach (var (id, container) in _containers)
{
if (!cast.Containers.ContainsKey(id))
{
container.Shutdown();
toDelete ??= new List<string>();
toDelete.Add(id);
}
}
if (toDelete != null)
{
foreach (var dead in toDelete)
{
_containers.Remove(dead);
}
}
// Add new containers and update existing contents.
foreach (var (id, data) in cast.Containers)
{
if (!_containers.TryGetValue(id, out var container))
{
container = new ClientContainer(id, this);
_containers.Add(id, container);
}
// sync show flag
container.ShowContents = data.ShowContents;
container.OccludesLight = data.OccludesLight;
// Remove gone entities.
List<IEntity>? toRemove = null;
foreach (var entity in container.Entities)
{
if (!data.ContainedEntities.Contains(entity.Uid))
{
toRemove ??= new List<IEntity>();
toRemove.Add(entity);
}
}
if (toRemove != null)
{
foreach (var goner in toRemove)
{
container.DoRemove(goner);
}
}
// Add new entities.
foreach (var uid in data.ContainedEntities)
{
var entity = Owner.EntityManager.GetEntity(uid);
if (!container.Entities.Contains(entity))
{
container.DoInsert(entity);
}
}
}
}
protected override void Shutdown()
{
base.Shutdown();
// On shutdown we won't get to process remove events in the containers so this has to be manually done.
foreach (var container in _containers.Values)
{
foreach (var containerEntity in container.Entities)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
new UpdateContainerOcclusionMessage(containerEntity));
}
}
}
}
}

View File

@@ -3,7 +3,9 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
@@ -20,7 +22,9 @@ namespace Robust.Client.GameObjects
// Horrible hack to get around ordering issues.
private bool _setCurrentOnInitialize;
private bool _setDrawFovOnInitialize;
[DataField("drawFov")]
private bool _setDrawFovOnInitialize = true;
[DataField("zoom")]
private Vector2 _setZoomOnInitialize = Vector2.One/2f;
private Vector2 _offset = Vector2.Zero;
@@ -157,15 +161,6 @@ namespace Robust.Client.GameObjects
Current = false;
}
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataFieldCached(ref _setZoomOnInitialize, "zoom", Vector2.One/2f);
serializer.DataFieldCached(ref _setDrawFovOnInitialize, "drawFov", true);
}
/// <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.

View File

@@ -1,105 +1,51 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
[RegisterComponent]
public class IconComponent : Component
public class IconComponent : Component, ISerializationHooks
{
public override string Name => "Icon";
public IDirectionalTextureProvider? Icon { get; private set; }
[Dependency] private readonly IResourceCache _resourceCache = default!;
[DataField("sprite")]
private ResourcePath? rsi;
[DataField("state")]
private string? stateID;
void ISerializationHooks.AfterDeserialization()
{
if (rsi != null && stateID != null)
{
Icon = new SpriteSpecifier.Rsi(rsi, stateID).Frame0();
}
}
public const string LogCategory = "go.comp.icon";
const string SerializationCache = "icon";
public override void ExposeData(ObjectSerializer serializer)
private static IRsiStateLike TextureForConfig(IconComponent compData, IResourceCache resourceCache)
{
base.ExposeData(serializer);
// TODO: Does this need writing?
if (serializer.Reading)
{
Icon = TextureForConfig(serializer, _resourceCache);
}
}
private static IRsiStateLike TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache)
{
DebugTools.Assert(serializer.Reading);
if (serializer.TryGetCacheData<IRsiStateLike>(SerializationCache, out var dirTex))
{
return dirTex;
}
var tex = serializer.ReadDataField<string?>("texture", null);
if (!string.IsNullOrWhiteSpace(tex))
{
dirTex = resourceCache.GetResource<TextureResource>(SpriteComponent.TextureRoot / tex).Texture;
serializer.SetCacheData(SerializationCache, dirTex);
return dirTex;
}
RSI rsi;
var rsiPath = serializer.ReadDataField<string?>("sprite", null);
if (string.IsNullOrWhiteSpace(rsiPath))
{
dirTex = resourceCache.GetFallback<TextureResource>().Texture;
serializer.SetCacheData(SerializationCache, dirTex);
return dirTex;
}
var path = SpriteComponent.TextureRoot / rsiPath;
try
{
rsi = resourceCache.GetResource<RSIResource>(path).RSI;
}
catch
{
dirTex = resourceCache.GetFallback<TextureResource>().Texture;
serializer.SetCacheData(SerializationCache, dirTex);
return dirTex;
}
var stateId = serializer.ReadDataField<string?>("state", null);
if (string.IsNullOrWhiteSpace(stateId))
{
Logger.ErrorS(LogCategory, "No state specified.");
dirTex = resourceCache.GetFallback<TextureResource>().Texture;
serializer.SetCacheData(SerializationCache, dirTex);
return dirTex;
}
if (rsi.TryGetState(stateId, out var state))
{
serializer.SetCacheData(SerializationCache, state);
return state;
}
else
{
Logger.ErrorS(LogCategory, "State '{0}' does not exist on RSI.", stateId);
return resourceCache.GetFallback<TextureResource>().Texture;
}
return compData.Icon?.Default ?? resourceCache.GetFallback<TextureResource>().Texture;
}
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
if (!prototype.Components.TryGetValue("Icon", out var mapping))
if (!prototype.Components.TryGetValue("Icon", out var compData))
{
return null;
}
return TextureForConfig(YamlObjectSerializer.NewReader(mapping), resourceCache);
return TextureForConfig((IconComponent)compData, resourceCache);
}
}
}

View File

@@ -1,11 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
[RegisterComponent]
[ComponentReference(typeof(SharedIgnorePauseComponent))]
public sealed class IgnorePauseComponent : SharedIgnorePauseComponent
{
}
}

View File

@@ -1,6 +1,8 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
@@ -17,14 +19,7 @@ namespace Robust.Client.GameObjects
/// The context that will be made active for a client that attaches to this entity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public string ContextName { get; set; } = default!;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction("context", InputContextContainer.DefaultContextName, value => ContextName = value, () => ContextName);
}
[DataField("context")]
public string ContextName { get; set; } = InputContextContainer.DefaultContextName;
}
}

View File

@@ -7,12 +7,17 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
public class PointLightComponent : Component
[RegisterComponent]
[ComponentReference(typeof(IPointLightComponent))]
public class PointLightComponent : Component, IPointLightComponent, ISerializationHooks
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
public override string Name => "PointLight";
public override uint? NetID => NetIDs.POINT_LIGHT;
@@ -65,6 +70,21 @@ namespace Robust.Client.GameObjects
set => _rotation = value;
}
/// <inheritdoc />
/// <summary>
/// The resource path to the mask texture the light will use.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public string? MaskPath
{
get => _maskPath;
set
{
_maskPath = value;
UpdateMask();
}
}
/// <summary>
/// Set a mask texture that will be applied to the light while rendering.
/// The mask's red channel will be linearly multiplied.p
@@ -115,16 +135,26 @@ namespace Robust.Client.GameObjects
}
}
private float _radius = 5;
[DataField("radius")]
private float _radius = 5f;
[DataField("nestedvisible")]
private bool _visibleNested = true;
private bool _lightOnParent = false;
private bool _lightOnParent;
[DataField("color")]
private Color _color = Color.White;
private Vector2 _offset;
[DataField("offset")]
private Vector2 _offset = Vector2.Zero;
[DataField("enabled")]
private bool _enabled = true;
[DataField("autoRot")]
private bool _maskAutoRotate;
private Angle _rotation;
private float _energy;
private float _softness;
[DataField("energy")]
private float _energy = 1f;
[DataField("softness")]
private float _softness = 1f;
[DataField("mask")]
private string? _maskPath;
/// <summary>
/// Radius, in meters.
@@ -141,12 +171,34 @@ namespace Robust.Client.GameObjects
}
}
private void UpdateMask()
{
if (_maskPath is not null)
Mask = _resourceCache.GetResource<TextureResource>(_maskPath);
else
Mask = null;
}
void ISerializationHooks.AfterDeserialization()
{
if (_maskPath != null)
{
Mask = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(_maskPath);
}
}
public override void Initialize()
{
base.Initialize();
UpdateMask();
}
/// <inheritdoc />
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
if ((message is ParentChangedMessage msg))
if (message is ParentChangedMessage msg)
{
HandleTransformParentChanged(msg);
}
@@ -170,23 +222,6 @@ namespace Robust.Client.GameObjects
}
}
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataFieldCached(ref _offset, "offset", Vector2.Zero);
serializer.DataFieldCached(ref _radius, "radius", 5f);
serializer.DataFieldCached(ref _color, "color", Color.White);
serializer.DataFieldCached(ref _enabled, "enabled", true);
serializer.DataFieldCached(ref _energy, "energy", 1f);
serializer.DataFieldCached(ref _softness, "softness", 1f);
serializer.DataFieldCached(ref _maskAutoRotate, "autoRot", false);
serializer.DataFieldCached(ref _visibleNested, "nestedvisible", true);
if (serializer.Reading && serializer.TryReadDataField<string>("mask", out var value))
{
Mask = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
}
}
public override void OnRemove()
{
base.OnRemove();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Animations;
@@ -49,8 +49,26 @@ namespace Robust.Client.GameObjects
/// Rotation transformations on individual layers still apply.
/// If false, all layers get locked to south and rotation is a transformation.
/// </summary>
[Obsolete("Use NoRotation and/or DirectionOverride")]
bool Directional { get; set; }
/// <summary>
/// All sprite rotation is locked, and will always be drawn upright on
/// the screen, regardless of world or view orientation.
/// </summary>
bool NoRotation {get; set; }
/// <summary>
/// Enables overriding the calculated directional RSI state for this sprite.
/// The state to use is defined in <see cref="DirectionOverride"/>.
/// </summary>
bool EnableDirectionOverride { get; set; }
/// <summary>
/// The directional RSI state that will always be displayed, regardless of orientation.
/// </summary>
Direction DirectionOverride { get; set; }
// NOTE: The below are ALL designed to NOT throw exceptions ever,
// instead making a bunch of noisy error logs.
@@ -64,6 +82,8 @@ namespace Robust.Client.GameObjects
uint RenderOrder { get; set; }
bool IsInert { get; }
Matrix3 GetLocalMatrix();
/// <summary>
/// Sets a layer key to the layer map, creating it if it does not exist.
/// </summary>
@@ -202,5 +222,12 @@ namespace Robust.Client.GameObjects
ISpriteLayer this[object layerKey] { get; }
IEnumerable<ISpriteLayer> AllLayers { get; }
int GetLayerDirectionCount(ISpriteLayer layer);
/// <summary>
/// Calculate sprite bounding box in world-space coordinates.
/// </summary>
Box2 CalculateBoundingBox();
}
}
}

View File

@@ -26,5 +26,17 @@ namespace Robust.Client.GameObjects
RSI.State.Direction EffectiveDirection(Angle worldRotation);
Vector2 LocalToLayer(Vector2 localPos);
/// <summary>
/// Layer size in pixels.
/// Don't account layer scale or sprite world transform.
/// </summary>
Vector2i PixelSize { get; }
/// <summary>
/// Calculate layer bounding box in sprite local-space coordinates.
/// </summary>
/// <returns>Bounding box in sprite local-space coordinates.</returns>
Box2 CalculateBoundingBox();
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -17,6 +17,8 @@ using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -25,8 +27,12 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
namespace Robust.Client.GameObjects
{
public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent,
IComponentDebug
IComponentDebug, ISerializationHooks
{
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[DataField("visible")]
private bool _visible = true;
[ViewVariables(VVAccess.ReadWrite)]
@@ -36,6 +42,7 @@ namespace Robust.Client.GameObjects
set => _visible = value;
}
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
private int drawDepth = DrawDepthTag.Default;
/// <summary>
@@ -48,6 +55,7 @@ namespace Robust.Client.GameObjects
set => drawDepth = value;
}
[DataField("scale")]
private Vector2 scale = Vector2.One;
/// <summary>
@@ -61,7 +69,8 @@ namespace Robust.Client.GameObjects
set => scale = value;
}
private Angle rotation;
[DataField("rotation")]
private Angle rotation = Angle.Zero;
[Animatable]
[ViewVariables(VVAccess.ReadWrite)]
@@ -71,6 +80,7 @@ namespace Robust.Client.GameObjects
set => rotation = value;
}
[DataField("offset")]
private Vector2 offset = Vector2.Zero;
/// <summary>
@@ -84,6 +94,7 @@ namespace Robust.Client.GameObjects
set => offset = value;
}
[DataField("color")]
private Color color = Color.White;
[Animatable]
@@ -101,24 +112,159 @@ namespace Robust.Client.GameObjects
/// If false, all layers get locked to south and rotation is a transformation.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Obsolete("Use NoRotation and/or DirectionOverride")]
public bool Directional
{
get => _directional;
set => _directional = value;
}
[DataField("directional")]
private bool _directional = true;
[DataField("layerDatums")]
private List<PrototypeLayerData> LayerDatums
{
get
{
var layerDatums = new List<PrototypeLayerData>();
foreach (var layer in Layers)
{
layerDatums.Add(layer.ToPrototypeData());
}
return layerDatums;
}
set
{
if(value == null) return;
Layers.Clear();
foreach (var layerDatum in value)
{
var anyTextureAttempted = false;
var layer = new Layer(this);
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
try
{
layer.RSI = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>(path).RSI;
}
catch
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from. Cannot use 'state' property. ({0})",
layerDatum.State);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (IoCManager.Resolve<IPrototypeManager>().TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist.",
layerDatum.Shader);
}
}
layer.Color = layerDatum.Color;
layer.Rotation = layerDatum.Rotation;
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Scale = layerDatum.Scale;
Layers.Add(layer);
if (layerDatum.MapKeys != null)
{
var index = Layers.Count - 1;
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (LayerMap.ContainsKey(key))
{
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
LayerMap.Add(key, index);
}
}
}
_layerMapShared = true;
UpdateIsInert();
}
}
private RSI? _baseRsi;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("rsi", priority: 2)]
public RSI? BaseRSI
{
get => _baseRsi;
set
{
_baseRsi = value;
if (Layers == null || value == null)
if (value == null)
{
return;
}
@@ -146,6 +292,12 @@ namespace Robust.Client.GameObjects
}
}
[DataField("sprite", readOnly: true)] private string? rsi;
[DataField("layers", readOnly: true)] private List<PrototypeLayerData> layerDatums = new ();
[DataField("state", readOnly: true)] private string? state;
[DataField("texture", readOnly: true)] private string? texture;
[ViewVariables(VVAccess.ReadWrite)]
public bool ContainerOccluded { get; set; }
@@ -157,11 +309,7 @@ namespace Robust.Client.GameObjects
[ViewVariables] private Dictionary<object, int> LayerMap = new();
[ViewVariables] private bool _layerMapShared;
[ViewVariables] private List<Layer> Layers = default!;
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[Dependency] private readonly IReflectionManager reflectionManager = default!;
[ViewVariables] private List<Layer> Layers = new();
[ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; }
@@ -169,10 +317,9 @@ namespace Robust.Client.GameObjects
private static ShaderInstance? _defaultShader;
[ViewVariables]
private ShaderInstance? DefaultShader => _defaultShader ??
(_defaultShader = prototypes
.Index<ShaderPrototype>("shaded")
.Instance());
private ShaderInstance? DefaultShader => _defaultShader ??= prototypes
.Index<ShaderPrototype>("shaded")
.Instance();
public const string LogCategory = "go.comp.sprite";
const string LayerSerializationCache = "spritelayer";
@@ -180,6 +327,46 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)] public bool IsInert { get; private set; }
void ISerializationHooks.AfterDeserialization()
{
{
if (!string.IsNullOrWhiteSpace(rsi))
{
var rsiPath = TextureRoot / rsi;
try
{
BaseRSI = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>(rsiPath).RSI;
}
catch (Exception e)
{
Logger.ErrorS(SpriteComponent.LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e);
}
}
}
if (layerDatums.Count == 0)
{
if (state != null || texture != null)
{
layerDatums.Insert(0, new PrototypeLayerData
{
TexturePath = string.IsNullOrWhiteSpace(texture) ? null : texture,
State = string.IsNullOrWhiteSpace(state) ? null : state,
Color = Color.White,
Scale = Vector2.One,
Visible = true,
});
state = null;
texture = null;
}
}
if (layerDatums.Count != 0)
{
LayerDatums = layerDatums;
}
}
/// <summary>
/// Update this sprite component to visibly match the current state of other at the time
/// this is called. Does not keep them perpetually in sync.
@@ -197,6 +384,9 @@ namespace Robust.Client.GameObjects
rotation = other.rotation;
scale = other.scale;
drawDepth = other.drawDepth;
_screenLock = other._screenLock;
_overrideDirection = other._overrideDirection;
_enableOverrideDirection = other._enableOverrideDirection;
Layers = new List<Layer>(other.Layers.Count);
foreach (var otherLayer in other.Layers)
{
@@ -218,6 +408,11 @@ namespace Robust.Client.GameObjects
RenderOrder = other.RenderOrder;
}
public Matrix3 GetLocalMatrix()
{
return Matrix3.CreateTransform(in offset, in rotation, in scale);
}
/// <inheritdoc />
public void LayerMapSet(object key, int layer)
{
@@ -936,6 +1131,31 @@ namespace Robust.Client.GameObjects
LayerSetAutoAnimated(layer, autoAnimated);
}
public void LayerSetOffset(int layer, Vector2 layerOffset)
{
if (Layers.Count <= layer)
{
Logger.ErrorS(LogCategory,
"Layer with index '{0}' does not exist, cannot set offset! Trace:\n{1}",
layer, Environment.StackTrace);
return;
}
Layers[layer].SetOffset(layerOffset);
}
public void LayerSetOffset(object layerKey, Vector2 layerOffset)
{
if (!LayerMapTryGet(layerKey, out var layer))
{
Logger.ErrorS(LogCategory, "Layer with key '{0}' does not exist, cannot set offset! Trace:\n{1}",
layerKey, Environment.StackTrace);
return;
}
LayerSetOffset(layer, layerOffset);
}
/// <inheritdoc />
public RSI.StateId LayerGetState(int layer)
{
@@ -965,40 +1185,65 @@ namespace Robust.Client.GameObjects
public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]];
public IEnumerable<ISpriteLayer> AllLayers => Layers;
internal void Render(DrawingHandleWorld drawingHandle, in Matrix3 worldTransform, Angle worldRotation,
Direction? overrideDirection = null)
// Lobby SpriteView rendering path
internal void Render(DrawingHandleWorld drawingHandle, Angle worldRotation, Direction? overrideDirection = null)
{
var angle = Rotation;
if (Directional)
RenderInternal(drawingHandle, worldRotation, Vector2.Zero, overrideDirection);
}
[DataField("noRot")]
private bool _screenLock = true;
[DataField("overrideDir")]
private Direction _overrideDirection = Direction.East;
[DataField("enableOverrideDir")]
private bool _enableOverrideDirection;
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public bool NoRotation { get => _screenLock; set => _screenLock = value; }
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public Direction DirectionOverride { get => _overrideDirection; set => _overrideDirection = value; }
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public bool EnableDirectionOverride { get => _enableOverrideDirection; set => _enableOverrideDirection = value; }
// Sprite rendering path
internal void Render(DrawingHandleWorld drawingHandle, in Angle worldRotation, in Vector2 worldPosition)
{
Direction? overrideDir = null;
if (_enableOverrideDirection)
{
angle -= worldRotation;
overrideDir = _overrideDirection;
}
RenderInternal(drawingHandle, worldRotation, worldPosition, overrideDir);
}
private void CalcModelMatrix(int numDirs, Angle worldRotation, Vector2 worldPosition, out Matrix3 modelMatrix)
{
Angle angle;
if (_screenLock)
{
angle = Angle.Zero;
}
else
{
angle -= new Angle(MathHelper.PiOver2);
angle = CalcRectWorldAngle(worldRotation, numDirs);
}
var mOffset = Matrix3.CreateTranslation(Offset);
var mRotation = Matrix3.CreateRotation(angle);
Matrix3.Multiply(ref mRotation, ref mOffset, out var transform);
// Only apply scale if needed.
if(!Scale.EqualsApprox(Vector2.One)) transform.Multiply(Matrix3.CreateScale(Scale));
transform.Multiply(worldTransform);
RenderInternal(drawingHandle, worldRotation, overrideDirection, transform);
var sWorldRotation = angle;
modelMatrix = Matrix3.CreateTransform(in worldPosition, in sWorldRotation);
}
internal void Render(DrawingHandleWorld drawingHandle, Angle worldRotation, Direction? overrideDirection = null)
private void RenderInternal(DrawingHandleWorld drawingHandle, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection)
{
RenderInternal(drawingHandle, worldRotation, overrideDirection, Matrix3.Identity);
}
private void RenderInternal(DrawingHandleWorld drawingHandle, Angle worldRotation, Direction? overrideDirection,
in Matrix3 transform)
{
drawingHandle.SetTransform(transform);
var localMatrix = GetLocalMatrix();
foreach (var layer in Layers)
{
@@ -1007,26 +1252,77 @@ namespace Robust.Client.GameObjects
continue;
}
// TODO: Implement layer-specific rotation and scale.
// Oh and when you do update Layer.LocalToLayer so content doesn't break.
var numDirs = GetLayerDirectionCount(layer);
var texture = GetRenderTexture(layer, worldRotation, overrideDirection);
CalcModelMatrix(numDirs, worldRotation, worldPosition, out var modelMatrix);
Matrix3.Multiply(ref localMatrix, ref modelMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
if (layer.Shader != null)
{
drawingHandle.UseShader(layer.Shader);
}
drawingHandle.DrawTexture(texture, -(Vector2) texture.Size / (2f * EyeManager.PixelsPerMeter),
color * layer.Color);
if (layer.Shader != null)
{
drawingHandle.UseShader(null);
}
RenderLayer(drawingHandle, layer, worldRotation, overrideDirection);
}
}
private void RenderLayer(DrawingHandleWorld drawingHandle, Layer layer, Angle worldRotation, Direction? overrideDirection)
{
var texture = GetRenderTexture(layer, worldRotation, overrideDirection);
if (layer.Shader != null)
{
drawingHandle.UseShader(layer.Shader);
}
var layerColor = color * layer.Color;
var position = -(Vector2) texture.Size / (2f * EyeManager.PixelsPerMeter) + layer.Offset;
var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
// TODO: Implement layer-specific rotation and scale.
// Apply these directly to the box.
// Oh and when you do update Layer.LocalToLayer so content doesn't break.
// handle.Modulate changes the color
// drawingHandle.SetTransform() is set above, turning the quad into local space vertices
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
{
drawingHandle.UseShader(null);
}
}
public static Angle CalcRectWorldAngle(Angle worldAngle, int numDirections)
{
var theta = worldAngle.Theta;
var segSize = (MathF.PI*2) / (numDirections * 2);
var segments = (int)(theta / segSize);
var odd = segments % 2;
var result = theta - (segments * segSize) - (odd * segSize);
return result;
}
public int GetLayerDirectionCount(ISpriteLayer layer)
{
if (!layer.RsiState.IsValid)
return 1;
// Pull texture from RSI state instead.
var rsi = layer.Rsi ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
{
state = GetFallbackState(resourceCache);
}
return state.Directions switch
{
RSI.State.DirectionType.Dir1 => 1,
RSI.State.DirectionType.Dir4 => 4,
RSI.State.DirectionType.Dir8 => 8,
_ => throw new ArgumentOutOfRangeException()
};
}
private Texture GetRenderTexture(Layer layer, Angle worldRotation, Direction? overrideDirection)
{
var texture = layer.Texture;
@@ -1048,202 +1344,6 @@ namespace Robust.Client.GameObjects
return texture;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataFieldCached(ref scale, "scale", Vector2.One);
serializer.DataFieldCached(ref rotation, "rotation", Angle.Zero);
serializer.DataFieldCached(ref offset, "offset", Vector2.Zero);
serializer.DataFieldCached(ref drawDepth, "drawdepth", DrawDepthTag.Default,
WithFormat.Constants<DrawDepthTag>());
serializer.DataFieldCached(ref color, "color", Color.White);
serializer.DataFieldCached(ref _directional, "directional", true);
serializer.DataFieldCached(ref _visible, "visible", true);
// TODO: Writing?
if (!serializer.Reading)
{
return;
}
{
var rsi = serializer.ReadDataField<string?>("sprite", null);
if (!string.IsNullOrWhiteSpace(rsi))
{
var rsiPath = TextureRoot / rsi;
try
{
BaseRSI = resourceCache.GetResource<RSIResource>(rsiPath).RSI;
}
catch (Exception e)
{
Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e);
}
}
}
List<Layer> CloneLayers(List<Layer> source)
{
var clone = new List<Layer>(source.Count);
foreach (var layer in source)
{
clone.Add(new Layer(layer, this));
}
return clone;
}
if (serializer.TryGetCacheData<List<Layer>>(LayerSerializationCache, out var layers))
{
LayerMap = serializer.GetCacheData<Dictionary<object, int>>(LayerMapSerializationCache);
_layerMapShared = true;
Layers = CloneLayers(layers);
UpdateIsInert();
return;
}
layers = new List<Layer>();
var layerMap = new Dictionary<object, int>();
var layerData =
serializer.ReadDataField("layers", new List<PrototypeLayerData>());
if(layerData.Count == 0){
var baseState = serializer.ReadDataField<string?>("state", null);
var texturePath = serializer.ReadDataField<string?>("texture", null);
if (baseState != null || texturePath != null)
{
layerData.Insert(0, new PrototypeLayerData
{
TexturePath = string.IsNullOrWhiteSpace(texturePath) ? null : texturePath,
State = string.IsNullOrWhiteSpace(baseState) ? null : baseState,
Color = Color.White,
Scale = Vector2.One,
Visible = true,
});
}
}
foreach (var layerDatum in layerData)
{
var anyTextureAttempted = false;
var layer = new Layer(this);
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
try
{
layer.RSI = resourceCache.GetResource<RSIResource>(path).RSI;
}
catch
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from."
+ "cannot use 'state' property. Prototype: '{0}'", Owner.Prototype?.ID);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
resourceCache.GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist. Prototype: '{1}'",
layerDatum.Shader, Owner.Prototype?.ID);
}
}
layer.Color = layerDatum.Color;
layer.Rotation = layerDatum.Rotation;
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Scale = layerDatum.Scale;
layers.Add(layer);
if (layerDatum.MapKeys != null)
{
var index = layers.Count - 1;
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (reflectionManager.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (layerMap.ContainsKey(key))
{
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
layerMap.Add(key, index);
}
}
}
Layers = layers;
LayerMap = layerMap;
_layerMapShared = true;
serializer.SetCacheData(LayerSerializationCache, CloneLayers(Layers));
serializer.SetCacheData(LayerMapSerializationCache, layerMap);
UpdateIsInert();
}
public override void OnRemove()
{
base.OnRemove();
@@ -1314,7 +1414,6 @@ namespace Robust.Client.GameObjects
Rotation = thestate.Rotation;
Offset = thestate.Offset;
Color = thestate.Color;
Directional = thestate.Directional;
RenderOrder = thestate.RenderOrder;
if (thestate.BaseRsiPath != null && BaseRSI != null)
@@ -1371,16 +1470,28 @@ namespace Robust.Client.GameObjects
}
}
private RSI.State.Direction GetDir(RSI.State.DirectionType type, Angle worldRotation)
private RSI.State.Direction GetDir(RSI.State.DirectionType rsiDirectionType, Angle worldRotation)
{
if (!Directional)
var dir = rsiDirectionType switch
{
return RSI.State.Direction.South;
}
RSI.State.DirectionType.Dir1 => Direction.South,
RSI.State.DirectionType.Dir4 => worldRotation.GetCardinalDir(),
RSI.State.DirectionType.Dir8 => worldRotation.GetDir(),
_ => throw new ArgumentException($"Unknown RSI DirectionType: {rsiDirectionType}.", nameof(rsiDirectionType))
};
var angle = new Angle(worldRotation);
return angle.GetDir().Convert(type);
return dir switch
{
Direction.North => RSI.State.Direction.North,
Direction.South => RSI.State.Direction.South,
Direction.East => RSI.State.Direction.East,
Direction.West => RSI.State.Direction.West,
Direction.SouthEast => RSI.State.Direction.SouthEast,
Direction.SouthWest => RSI.State.Direction.SouthWest,
Direction.NorthEast => RSI.State.Direction.NorthEast,
Direction.NorthWest => RSI.State.Direction.NorthWest,
_ => throw new ArgumentOutOfRangeException(nameof(dir), dir, null)
};
}
private void UpdateIsInert()
@@ -1485,6 +1596,39 @@ namespace Robust.Client.GameObjects
return builder.ToString();
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
{
// fast check for empty sprites
if (Layers.Count == 0)
return new Box2();
// we need to calculate bounding box taking into account all nested layers
// because layers can have offsets, scale or rotation we need to calculate a new BB
// based on lowest bottomLeft and hightest topRight points from all layers
var box = Layers[0].CalculateBoundingBox();
for (int i = 1; i < Layers.Count; i++)
{
var layer = Layers[i];
var layerBB = layer.CalculateBoundingBox();
box = box.Union(layerBB);
}
// apply sprite transformations and calculate sprite bounding box
// we can optimize it a bit, if sprite doesn't have rotation
var spriteBox = box.Scale(Scale);
var spriteHasRotation = !Rotation.EqualsApprox(Angle.Zero);
var spriteBB = spriteHasRotation ?
new Box2Rotated(spriteBox, Rotation).CalcBoundingBox() : spriteBox;
// move it all to world transform system (with sprite offset)
var worldPosition = Owner.Transform.WorldPosition;
var worldBB = spriteBB.Translated(Offset + worldPosition);
return worldBB;
}
/// <summary>
/// Enum to "offset" a cardinal direction.
/// </summary>
@@ -1511,7 +1655,7 @@ namespace Robust.Client.GameObjects
Flip = 3,
}
private class Layer : ISpriteLayer
public class Layer : ISpriteLayer
{
[ViewVariables] private readonly SpriteComponent _parent;
@@ -1526,16 +1670,25 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Scale { get; set; } = Vector2.One;
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible = true;
[ViewVariables(VVAccess.ReadWrite)]
public Color Color { get; set; } = Color.White;
[ViewVariables(VVAccess.ReadWrite)]
public bool AutoAnimated = true;
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Offset { get; set; }
[ViewVariables]
public DirectionOffset DirOffset { get; set; }
[ViewVariables]
public RSI? ActualRsi => RSI ?? _parent.BaseRSI;
@@ -1569,6 +1722,22 @@ namespace Robust.Client.GameObjects
RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); }
Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); }
public PrototypeLayerData ToPrototypeData()
{
return new PrototypeLayerData
{
Color = Color,
Rotation = Rotation,
Scale = Scale,
//todo Shader = Shader,
State = State.Name,
Visible = Visible,
RsiPath = RSI?.Path?.ToString(),
//todo TexturePath = Textur
//todo MapKeys
};
}
bool ISpriteLayer.Visible
{
get => Visible;
@@ -1756,6 +1925,38 @@ namespace Robust.Client.GameObjects
_parent.UpdateIsInert();
}
public void SetOffset(Vector2 offset)
{
Offset = offset;
}
/// <inheritdoc/>
public Vector2i PixelSize
{
get
{
var pixelSize = Vector2i.Zero;
if (Texture != null)
{
pixelSize = Texture.Size;
}
else if (ActualRsi != null)
{
pixelSize = ActualRsi.Size;
}
return pixelSize;
}
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
{
// TODO: scale & rotation for layers is currently unimplemented.
return Box2.CenteredAround(Offset, PixelSize / EyeManager.PixelsPerMeter);
}
}
void IAnimationProperties.SetAnimatableProperty(string name, object value)
@@ -1807,7 +2008,6 @@ namespace Robust.Client.GameObjects
}
return state;
}
}
@@ -1858,7 +2058,6 @@ namespace Robust.Client.GameObjects
if (!anyTexture)
yield return resourceCache.GetFallback<TextureResource>().Texture;
}
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
@@ -1905,6 +2104,7 @@ namespace Robust.Client.GameObjects
public T AddComponent<T>() where T : Component, new()
{
var typeFactory = IoCManager.Resolve<IDynamicTypeFactoryInternal>();
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var comp = (T) typeFactory.CreateInstanceUnchecked(typeof(T));
_components[typeof(T)] = comp;
comp.Owner = this;
@@ -1914,9 +2114,9 @@ namespace Robust.Client.GameObjects
_components[typeof(ISpriteComponent)] = comp;
}
if (Prototype != null && Prototype.Components.TryGetValue(comp.Name, out var node))
if (Prototype != null && Prototype.TryGetComponent<T>(comp.Name, out var node))
{
comp.ExposeData(YamlObjectSerializer.NewReader(node));
comp = serializationManager.Copy(node, comp)!;
}
return comp;

View File

@@ -6,41 +6,31 @@ using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Client.GameObjects
{
public class ClientUserInterfaceComponent : SharedUserInterfaceComponent
public class ClientUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
private readonly Dictionary<object, BoundUserInterface> _openInterfaces =
new();
private Dictionary<object, PrototypeData> _interfaceData = default!;
#pragma warning disable 649
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
#pragma warning restore 649
private readonly Dictionary<object, PrototypeData> _interfaces = new();
public override void ExposeData(ObjectSerializer serializer)
[DataField("interfaces", readOnly: true)]
private List<PrototypeData> _interfaceData = new();
void ISerializationHooks.AfterDeserialization()
{
base.ExposeData(serializer);
_interfaces.Clear();
const string cache = "ui_cache";
if (serializer.TryGetCacheData<Dictionary<object, PrototypeData>>(cache, out var interfaceData))
foreach (var data in _interfaceData)
{
_interfaceData = interfaceData;
return;
_interfaces[data.UiKey] = data;
}
var data = serializer.ReadDataFieldCached("interfaces", new List<PrototypeData>());
interfaceData = new Dictionary<object, PrototypeData>();
foreach (var prototypeData in data)
{
interfaceData[prototypeData.UiKey] = prototypeData;
}
serializer.SetCacheData(cache, interfaceData);
_interfaceData = interfaceData;
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel,
@@ -81,7 +71,7 @@ namespace Robust.Client.GameObjects
private void OpenInterface(BoundInterfaceMessageWrapMessage wrapped)
{
var data = _interfaceData[wrapped.UiKey];
var data = _interfaces[wrapped.UiKey];
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface = (BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[]{this, wrapped.UiKey});

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Audio;
@@ -11,12 +10,13 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
public class AudioSystem : EntitySystem
public class AudioSystem : EntitySystem, IAudioSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -25,9 +25,7 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly List<PlayingStream> _playingClydeStreams = new();
public int OcclusionCollisionMask;
/// <inheritdoc />
public override void Initialize()
{
@@ -35,6 +33,8 @@ namespace Robust.Client.GameObjects
SubscribeNetworkEvent<PlayAudioGlobalMessage>(PlayAudioGlobalHandler);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(PlayAudioPositionalHandler);
SubscribeNetworkEvent<StopAudioMessageClient>(StopAudioMessageHandler);
SubscribeLocalEvent<SoundSystem.QueryAudioSystem>((ev => ev.Audio = this));
}
private void StopAudioMessageHandler(StopAudioMessageClient ev)
@@ -176,7 +176,6 @@ namespace Robust.Client.GameObjects
{
stream.Source.Dispose();
stream.Done = true;
stream.DoPlaybackDone();
}
/// <summary>
@@ -335,92 +334,30 @@ namespace Robust.Client.GameObjects
{
Source.StopPlaying();
}
public event Action? PlaybackDone;
public void DoPlaybackDone()
{
PlaybackDone?.Invoke();
}
}
}
public interface IPlayingAudioStream
{
void Stop();
event Action PlaybackDone;
}
public static class AudioSystemExtensions
{
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
/// <param name="audioSystem">A pre-fetched instance of <see cref="AudioSystem"/> to use, can be null.</param>
public static IPlayingAudioStream? Play(
this IEntity entity,
string filename,
AudioParams? audioParams,
AudioSystem? audioSystem = null)
{
audioSystem ??= EntitySystem.Get<AudioSystem>();
return audioSystem.Play(filename, entity, audioParams);
}
/// <summary>
/// Play an audio stream following an entity.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
/// <param name="audioSystem">A pre-fetched instance of <see cref="AudioSystem"/> to use, can be null.</param>
public static IPlayingAudioStream? Play(
this IEntity entity,
AudioStream stream,
AudioParams? audioParams = null,
AudioSystem? audioSystem = null)
/// <inheritdoc />
public int DefaultSoundRange => 25;
/// <inheritdoc />
public int OcclusionCollisionMask { get; set; }
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, AudioParams? audioParams = null)
{
audioSystem ??= EntitySystem.Get<AudioSystem>();
return audioSystem.Play(stream, entity, audioParams);
return Play(filename, audioParams);
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
/// <param name="audioSystem">A pre-fetched instance of <see cref="AudioSystem"/> to use, can be null.</param>
public static IPlayingAudioStream? Play(
this EntityCoordinates coordinates,
string filename,
AudioParams? audioParams = null,
AudioSystem? audioSystem = null)
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, IEntity entity, AudioParams? audioParams = null)
{
audioSystem ??= EntitySystem.Get<AudioSystem>();
return audioSystem.Play(filename, coordinates, audioParams);
return Play(filename, entity, audioParams);
}
/// <summary>
/// Play an audio stream at a static position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
/// <param name="audioSystem">A pre-fetched instance of <see cref="AudioSystem"/> to use, can be null.</param>
public static IPlayingAudioStream? Play(
this EntityCoordinates coordinates,
AudioStream stream,
AudioParams? audioParams = null,
AudioSystem? audioSystem = null)
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
audioSystem ??= EntitySystem.Get<AudioSystem>();
return audioSystem.Play(stream, coordinates, audioParams);
return Play(filename, coordinates, audioParams);
}
}
}

View File

@@ -4,7 +4,7 @@ using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
public class ContainerSystem : EntitySystem
public class ClientContainerSystem : ContainerSystem
{
private readonly HashSet<IEntity> _updateQueue = new();
@@ -91,14 +91,4 @@ namespace Robust.Client.GameObjects
}
}
}
internal readonly struct UpdateContainerOcclusionMessage
{
public UpdateContainerOcclusionMessage(IEntity entity)
{
Entity = entity;
}
public IEntity Entity { get; }
}
}

View File

@@ -40,7 +40,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, FullInputCmdMessage message, bool replay = false)
{
#if DEBUG

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Shared.GameStates;
@@ -352,7 +353,10 @@ namespace Robust.Client.GameStates
foreach (var component in _componentManager.GetNetComponents(createdEntity))
{
var state = component.GetComponentState();
Debug.Assert(_players.LocalPlayer != null, "_players.LocalPlayer != null");
var player = _players.LocalPlayer.Session;
var state = component.GetComponentState(player);
if (state.GetType() == typeof(ComponentState))
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -36,6 +36,9 @@ namespace Robust.Client.Graphics.Clyde
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
@@ -182,7 +185,7 @@ namespace Robust.Client.Graphics.Clyde
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, newVolume);
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -183,25 +183,61 @@ namespace Robust.Client.Graphics.Clyde
break;
}
RenderTexture? entityPostRenderTarget = null;
Vector2i roundedPos = default;
if (entry.sprite.PostShader != null)
{
_renderHandle.UseRenderTarget(EntityPostRenderTarget);
_renderHandle.Clear(new Color());
// Calculate viewport so that the entity thinks it's drawing to the same position,
// which is necessary for light application,
// but it's ACTUALLY drawing into the center of the render target.
var spritePos = entry.sprite.Owner.Transform.WorldPosition;
var screenPos = _eyeManager.WorldToScreen(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i) screenPos;
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
flippedPos -= EntityPostRenderTarget.Size / 2;
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
// calculate world bounding box
var spriteBB = entry.sprite.CalculateBoundingBox();
var spriteLB = spriteBB.BottomLeft;
var spriteRT = spriteBB.TopRight;
// finally we can calculate screen bounding in pixels
var screenLB = _eyeManager.WorldToScreen(spriteLB);
var screenRT = _eyeManager.WorldToScreen(spriteRT);
// we need to scale RT a for effects like emission or highlight
// scale can be passed with PostShader as variable in future
var postShadeScale = 1.25f;
var screenSpriteSize = (Vector2i)((screenRT - screenLB) * postShadeScale).Rounded();
screenSpriteSize.Y = -screenSpriteSize.Y;
// I'm not 100% sure why it works, but without it post-shader
// can be lower or upper by 1px than original sprite depending on sprite rotation or scale
// probably some rotation rounding error
if (screenSpriteSize.X % 2 != 0)
screenSpriteSize.X++;
if (screenSpriteSize.Y % 2 != 0)
screenSpriteSize.Y++;
// check that sprite size is valid
if (screenSpriteSize.X > 0 && screenSpriteSize.Y > 0)
{
// create new render texture with correct sprite size
entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
name: nameof(entityPostRenderTarget));
_renderHandle.UseRenderTarget(entityPostRenderTarget);
_renderHandle.Clear(new Color());
// Calculate viewport so that the entity thinks it's drawing to the same position,
// which is necessary for light application,
// but it's ACTUALLY drawing into the center of the render target.
var spritePos = spriteBB.Center;
var screenPos = _eyeManager.WorldToScreen(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i)screenPos;
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
flippedPos -= entityPostRenderTarget.Size / 2;
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
}
}
entry.sprite.Render(_renderHandle.DrawingHandleWorld, entry.worldMatrix, entry.worldRotation);
var matrix = entry.worldMatrix;
var worldPosition = new Vector2(matrix.R0C2, matrix.R1C2);
entry.sprite.Render(_renderHandle.DrawingHandleWorld, in entry.worldRotation, in worldPosition);
if (entry.sprite.PostShader != null)
if (entry.sprite.PostShader != null && entityPostRenderTarget != null)
{
var oldProj = _currentMatrixProj;
var oldView = _currentMatrixView;
@@ -214,11 +250,11 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.SetProjView(proj, view);
_renderHandle.SetModelTransform(Matrix3.Identity);
var rounded = roundedPos - EntityPostRenderTarget.Size / 2;
var rounded = roundedPos - entityPostRenderTarget.Size / 2;
var box = Box2i.FromDimensions(rounded, EntityPostRenderTarget.Size);
var box = Box2i.FromDimensions(rounded, entityPostRenderTarget.Size);
_renderHandle.DrawTextureScreen(EntityPostRenderTarget.Texture,
_renderHandle.DrawTextureScreen(entityPostRenderTarget.Texture,
box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight,
Color.White, null);

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Buffers;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement.ResourceTypes;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -775,12 +775,10 @@ namespace Robust.Client.Graphics.Clyde
var worldTransform = transform.WorldMatrix;
var box = occluder.BoundingBox;
// So uh, angle 0 = east... Apparently...
// We account for that here so I don't go insane.
var (tlX, tlY) = worldTransform.Transform(box.BottomLeft);
var (trX, trY) = worldTransform.Transform(box.TopLeft);
var (brX, brY) = worldTransform.Transform(box.TopRight);
var (blX, blY) = worldTransform.Transform(box.BottomRight);
var (tlX, tlY) = worldTransform.Transform(box.TopLeft);
var (trX, trY) = worldTransform.Transform(box.TopRight);
var (brX, brY) = worldTransform.Transform(box.BottomRight);
var (blX, blY) = worldTransform.Transform(box.BottomLeft);
// Faces.
var faceN = new Vector4(tlX, tlY, trX, trY);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.InteropServices;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
@@ -37,28 +37,55 @@ namespace Robust.Client.Graphics.Clyde
_clyde.DrawSetProjViewTransform(proj, view);
}
/// <summary>
/// Draws a sprite to the screen. The coordinate system is left handed.
/// Make sure to set <see cref="DrawSetModelTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="bl">Bottom left vertex of the quad in object space.</param>
/// <param name="br">Bottom right vertex of the quad in object space.</param>
/// <param name="tl">Top left vertex of the quad in object space.</param>
/// <param name="tr">Top right vertex of the quad in object space.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public void DrawTextureScreen(Texture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
in Color modulate, in UIBox2? subRegion)
{
var clydeTexture = ExtractTexture(texture, subRegion, out var csr);
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
var (w, h) = clydeTexture.Size;
var sr = new Box2(csr.Left / w, (h - csr.Top) / h, csr.Right / w, (h - csr.Bottom) / h);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, modulate, sr);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
/// <summary>
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawSetModelTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="bl">Bottom left vertex of the quad in object space.</param>
/// <param name="br">Bottom right vertex of the quad in object space.</param>
/// <param name="tl">Top left vertex of the quad in object space.</param>
/// <param name="tr">Top right vertex of the quad in object space.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
Color modulate, in UIBox2? subRegion)
{
var clydeTexture = ExtractTexture(texture, subRegion, out var csr);
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
var (w, h) = clydeTexture.Size;
var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, modulate, sr);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
/// <summary>
/// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas).
/// </summary>
private static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
{
if (texture is AtlasTexture atlas)
@@ -383,22 +410,40 @@ namespace Robust.Client.Graphics.Clyde
}
}
public override void DrawTextureRectRegion(Texture texture, Box2 rect, UIBox2? subRegion = null,
Color? modulate = null)
/// <summary>
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawSetModelTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public override void DrawTextureRectRegion(Texture texture, Box2 quad,
Color? modulate = null, UIBox2? subRegion = null)
{
var color = (modulate ?? Color.White) * Modulate;
_renderHandle.DrawTextureWorld(texture, rect.BottomLeft, rect.BottomRight,
rect.TopLeft, rect.TopRight, color, subRegion);
_renderHandle.DrawTextureWorld(texture, quad.BottomLeft, quad.BottomRight,
quad.TopLeft, quad.TopRight, color, in subRegion);
}
public override void DrawTextureRectRegion(Texture texture, in Box2Rotated rect,
UIBox2? subRegion = null, Color? modulate = null)
/// <summary>
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawSetModelTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public override void DrawTextureRectRegion(Texture texture, in Box2Rotated quad,
Color? modulate = null, UIBox2? subRegion = null)
{
var color = (modulate ?? Color.White) * Modulate;
_renderHandle.DrawTextureWorld(texture, rect.BottomLeft, rect.BottomRight,
rect.TopLeft, rect.TopRight, color, subRegion);
_renderHandle.DrawTextureWorld(texture, quad.BottomLeft, quad.BottomRight,
quad.TopLeft, quad.TopRight, color, in subRegion);
}
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology,

View File

@@ -477,10 +477,20 @@ namespace Robust.Client.Graphics.Clyde
_currentMatrixView = view;
}
/// <summary>
/// Draws a texture quad to the screen.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="bl">Bottom left vertex of the quad in object space.</param>
/// <param name="br">Bottom right vertex of the quad in object space.</param>
/// <param name="tl">Top left vertex of the quad in object space.</param>
/// <param name="tr">Top right vertex of the quad in object space.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="texCoords">The four corners of the texture coordinates, matching the four vertices.</param>
private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr, in Color modulate,
in Box2 sr)
in Box2 texCoords)
{
EnsureBatchState(texture, modulate, true, GetQuadBatchPrimitiveType(), _queuedShader);
EnsureBatchState(texture, in modulate, true, GetQuadBatchPrimitiveType(), _queuedShader);
bl = _currentMatrixModel.Transform(bl);
br = _currentMatrixModel.Transform(br);
@@ -489,10 +499,10 @@ namespace Robust.Client.Graphics.Clyde
// TODO: split batch if necessary.
var vIdx = BatchVertexIndex;
BatchVertexData[vIdx + 0] = new Vertex2D(bl, sr.BottomLeft);
BatchVertexData[vIdx + 1] = new Vertex2D(br, sr.BottomRight);
BatchVertexData[vIdx + 2] = new Vertex2D(tr, sr.TopRight);
BatchVertexData[vIdx + 3] = new Vertex2D(tl, sr.TopLeft);
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft);
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight);
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight);
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft);
BatchVertexIndex += 4;
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);

View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement.ResourceTypes;
using Robust.Client.ResourceManagement;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using StencilOp = Robust.Client.Graphics.StencilOp;

View File

@@ -55,6 +55,7 @@ namespace Robust.Client.Graphics.Clyde
private GLFWCallbacks.WindowSizeCallback _windowSizeCallback = default!;
private GLFWCallbacks.WindowContentScaleCallback _windowContentScaleCallback = default!;
private GLFWCallbacks.WindowIconifyCallback _windowIconifyCallback = default!;
private GLFWCallbacks.WindowFocusCallback _windowFocusCallback = default!;
private bool _glfwInitialized;
@@ -62,6 +63,7 @@ namespace Robust.Client.Graphics.Clyde
private Window* _glfwWindow;
private Vector2i _framebufferSize;
private bool _isFocused;
private Vector2i _windowSize;
private Vector2i _prevWindowSize;
private Vector2i _prevWindowPos;
@@ -74,6 +76,7 @@ namespace Robust.Client.Graphics.Clyde
// NOTE: in engine we pretend the framebuffer size is the screen size..
// For practical reasons like UI rendering.
public override Vector2i ScreenSize => _framebufferSize;
public override bool IsFocused => _isFocused;
public Vector2 DefaultWindowScale => _windowScale;
public Vector2 MouseScreenPosition => _lastMousePos;
@@ -231,6 +234,7 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetMouseButtonCallback(_glfwWindow, _mouseButtonCallback);
GLFW.SetWindowContentScaleCallback(_glfwWindow, _windowContentScaleCallback);
GLFW.SetWindowIconifyCallback(_glfwWindow, _windowIconifyCallback);
GLFW.SetWindowFocusCallback(_glfwWindow, _windowFocusCallback);
GLFW.MakeContextCurrent(_glfwWindow);
@@ -548,6 +552,19 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void OnGlfwWindowFocus(Window* window, bool focused)
{
try
{
_isFocused = focused;
OnWindowFocused?.Invoke(new WindowFocusedEventArgs(focused));
}
catch (Exception e)
{
CatchCallbackException(e);
}
}
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
@@ -560,6 +577,7 @@ namespace Robust.Client.Graphics.Clyde
_windowSizeCallback = OnGlfwWindowSize;
_windowContentScaleCallback = OnGlfwWindownContentScale;
_windowIconifyCallback = OnGlfwWindowIconify;
_windowFocusCallback = OnGlfwWindowFocus;
}
public override void SetWindowTitle(string title)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -39,8 +39,6 @@ namespace Robust.Client.Graphics.Clyde
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
private RenderTexture EntityPostRenderTarget = default!;
private GLBuffer BatchVBO = default!;
private GLBuffer BatchEBO = default!;
private GLHandle BatchVAO;
@@ -88,7 +86,7 @@ namespace Robust.Client.Graphics.Clyde
public override bool Initialize()
{
base.Initialize();
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
if (!InitWindowing())
@@ -152,6 +150,8 @@ namespace Robust.Client.Graphics.Clyde
public override event Action<WindowResizedEventArgs>? OnWindowResized;
public override event Action<WindowFocusedEventArgs>? OnWindowFocused;
public void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback)
{
_queuedScreenshots.Add((type, callback));
@@ -314,10 +314,6 @@ namespace Robust.Client.Graphics.Clyde
ProjViewUBO = new GLUniformBuffer<ProjViewMatrices>(this, BindingIndexProjView, nameof(ProjViewUBO));
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
EntityPostRenderTarget = CreateRenderTarget(Vector2i.One * 8 * EyeManager.PixelsPerMeter,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
name: nameof(EntityPostRenderTarget));
CreateMainViewport();
}

View File

@@ -21,6 +21,7 @@ namespace Robust.Client.Graphics.Clyde
public IRenderWindow MainWindowRenderTarget { get; }
public override Vector2i ScreenSize { get; } = (1280, 720);
public Vector2 DefaultWindowScale => (1, 1);
public override bool IsFocused => true;
public ShaderInstance InstanceShader(ClydeHandle handle)
{
@@ -79,6 +80,12 @@ namespace Robust.Client.Graphics.Clyde
remove { }
}
public override event Action<WindowFocusedEventArgs> OnWindowFocused
{
add { }
remove { }
}
public void Render()
{
// Nada.

View File

@@ -25,6 +25,8 @@ namespace Robust.Client.Graphics
protected bool VSync { get; private set; } = true;
public abstract Vector2i ScreenSize { get; }
public abstract bool IsFocused { get; }
public abstract void SetWindowTitle(string title);
public virtual bool Initialize()
@@ -45,6 +47,8 @@ namespace Robust.Client.Graphics
public abstract event Action<WindowResizedEventArgs> OnWindowResized;
public abstract event Action<WindowFocusedEventArgs> OnWindowFocused;
protected virtual void ReadConfig()
{
WindowMode = (WindowMode) _configurationManager.GetCVar(CVars.DisplayWindowMode);

View File

@@ -19,17 +19,18 @@ namespace Robust.Client.Graphics
Disposed = true;
}
public void SetTransform(Vector2 position, Angle rotation, Vector2 scale)
public void SetTransform(in Vector2 position, in Angle rotation, in Vector2 scale)
{
CheckDisposed();
var matrix = Matrix3.Identity;
(matrix.R0C0, matrix.R1C1) = scale;
matrix.Rotate(rotation);
matrix.R0C2 += position.X;
matrix.R1C2 += position.Y;
var matrix = Matrix3.CreateTransform(in position, in rotation, in scale);
SetTransform(in matrix);
}
SetTransform(matrix);
public void SetTransform(in Vector2 position, in Angle rotation)
{
var matrix = Matrix3.CreateTransform(in position, in rotation);
SetTransform(in matrix);
}
public abstract void SetTransform(in Matrix3 matrix);

View File

@@ -6,15 +6,63 @@ namespace Robust.Client.Graphics
{
private const int Ppm = EyeManager.PixelsPerMeter;
/// <summary>
/// Draws an untextured colored rectangle to the world.The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="rect">The four vertices of the quad in object space (or world if the transform is identity.).</param>
/// <param name="color">Color of the rectangle.</param>
/// <param name="filled">Is it filled with color, or just the border lines?</param>
public abstract void DrawRect(Box2 rect, Color color, bool filled = true);
/// <summary>
/// Draws an untextured colored rectangle to the world.The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="rect">The four vertices of the quad in object space (or world if the transform is identity.).
/// The rotation of the rectangle is applied before the transform matrix.</param>
/// <param name="color">Color of the rectangle.</param>
/// <param name="filled">Is it filled with color, or just the border lines?</param>
public abstract void DrawRect(in Box2Rotated rect, Color color, bool filled = true);
public abstract void DrawTextureRectRegion(Texture texture, Box2 rect, UIBox2? subRegion = null,
Color? modulate = null);
/// <summary>
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public abstract void DrawTextureRectRegion(Texture texture, Box2 quad,
Color? modulate = null, UIBox2? subRegion = null);
public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated rect, UIBox2? subRegion = null,
Color? modulate = null);
/// <summary>
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).
/// The rotation of the rectangle is applied before the transform matrix.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <param name="subRegion">The four corners of the texture sub region in px.</param>
public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad,
Color? modulate = null, UIBox2? subRegion = null);
/// <summary>
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="position">The coordinates of the quad in object space (or world if the transform is identity.).</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
/// <remarks>
/// The sprite will have it's local dimensions calculated so that it has <see cref="EyeManager.PixelsPerMeter"/> texels per meter in the world.
/// </remarks>
public void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
CheckDisposed();
@@ -22,18 +70,35 @@ namespace Robust.Client.Graphics
DrawTextureRect(texture, Box2.FromDimensions(position, texture.Size / (float) Ppm), modulate);
}
public void DrawTextureRect(Texture texture, Box2 rect, Color? modulate = null)
/// <summary>
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
public void DrawTextureRect(Texture texture, Box2 quad, Color? modulate = null)
{
CheckDisposed();
DrawTextureRectRegion(texture, rect, null, modulate);
DrawTextureRectRegion(texture, quad, modulate);
}
public void DrawTextureRect(Texture texture, in Box2Rotated rect, Color? modulate = null)
/// <summary>
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
/// to set the model matrix if needed.
/// </summary>
/// <param name="texture">Texture to draw.</param>
/// <param name="quad">The four vertices of the quad in object space (or world if the transform is identity.).
/// The rotation of the rectangle is applied before the transform matrix.</param>
/// <param name="modulate">A color to multiply the texture by when shading.</param>
public void DrawTextureRect(Texture texture, in Box2Rotated quad, Color? modulate = null)
{
CheckDisposed();
DrawTextureRectRegion(texture, rect, null, modulate);
DrawTextureRectRegion(texture, in quad, modulate);
}
}
}

View File

@@ -13,6 +13,8 @@ namespace Robust.Client.Graphics
Vector2i ScreenSize { get; }
bool IsFocused { get; }
/// <summary>
/// The default scale ratio for window contents, given to us by the OS.
/// </summary>
@@ -27,6 +29,8 @@ namespace Robust.Client.Graphics
event Action<WindowResizedEventArgs> OnWindowResized;
event Action<WindowFocusedEventArgs> OnWindowFocused;
Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null);

View File

@@ -1,23 +1,26 @@
using System;
using System.Collections.Generic;
using Robust.Client.ResourceManagement;
using Robust.Client.ResourceManagement.ResourceTypes;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
namespace Robust.Client.Graphics
{
[Prototype("shader")]
public sealed class ShaderPrototype : IPrototype, IIndexedPrototype
public sealed class ShaderPrototype : IPrototype, ISerializationHooks
{
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
public string ID { get; private set; } = default!;
[ViewVariables]
[field: DataField("id", required: true)]
public string ID { get; } = default!;
private ShaderKind Kind;
@@ -31,11 +34,14 @@ namespace Robust.Client.Graphics
private ShaderInstance? _cachedInstance;
private bool _stencilEnabled;
private int _stencilRef;
private int _stencilReadMask = unchecked((int) uint.MaxValue);
private int _stencilWriteMask = unchecked((int) uint.MaxValue);
private StencilFunc _stencilFunc = StencilFunc.Always;
private StencilOp _stencilOp = StencilOp.Keep;
private int _stencilRef => StencilDataHolder?.StencilRef ?? 0;
private int _stencilReadMask => StencilDataHolder?.ReadMask ?? unchecked((int) uint.MaxValue);
private int _stencilWriteMask => StencilDataHolder?.WriteMask ?? unchecked((int) uint.MaxValue);
private StencilFunc _stencilFunc => StencilDataHolder?.StencilFunc ?? StencilFunc.Always;
private StencilOp _stencilOp => StencilDataHolder?.StencilOp ?? StencilOp.Keep;
[DataField("stencil")]
private StencilData? StencilDataHolder;
/// <summary>
/// Retrieves a ready-to-use instance of this shader.
@@ -64,12 +70,12 @@ namespace Robust.Client.Graphics
switch (Kind)
{
case ShaderKind.Source:
instance = _clyde.InstanceShader(Source!.ClydeHandle);
instance = IoCManager.Resolve<IClydeInternal>().InstanceShader(Source!.ClydeHandle);
_applyDefaultParameters(instance);
break;
case ShaderKind.Canvas:
instance = _clyde.InstanceShader(CompiledCanvasShader);
instance = IoCManager.Resolve<IClydeInternal>().InstanceShader(CompiledCanvasShader);
break;
default:
@@ -95,135 +101,108 @@ namespace Robust.Client.Graphics
return Instance().Duplicate();
}
public void LoadFrom(YamlMappingNode mapping)
{
ID = mapping.GetNode("id").ToString();
[DataField("kind", readOnly: true, required: true)] private string _rawKind = default!;
[DataField("path", readOnly: true)] private ResourcePath? path;
[DataField("params", readOnly: true)] private Dictionary<string, string>? paramMapping;
[DataField("light_mode", readOnly: true)] private string? rawMode;
[DataField("blend_mode", readOnly: true)] private string? rawBlendMode;
var kind = mapping.GetNode("kind").AsString();
switch (kind)
void ISerializationHooks.AfterDeserialization()
{
switch (_rawKind)
{
case "source":
Kind = ShaderKind.Source;
ReadSourceKind(mapping);
if (path == null) throw new InvalidOperationException();
Source = IoCManager.Resolve<IResourceCache>().GetResource<ShaderSourceResource>(path);
if (paramMapping != null)
{
ShaderParams = new Dictionary<string, object>();
foreach (var item in paramMapping!)
{
var name = item.Key;
if (!Source.ParsedShader.Uniforms.TryGetValue(name, out var uniformDefinition))
{
Logger.ErrorS("shader", "Shader param '{0}' does not exist on shader '{1}'", name, path);
continue;
}
var value = _parseUniformValue(item.Value, uniformDefinition.Type.Type);
ShaderParams.Add(name, value);
}
}
break;
case "canvas":
Kind = ShaderKind.Canvas;
ReadCanvasKind(mapping);
var source = "";
if(rawMode != null)
{
switch (rawMode)
{
case "normal":
break;
case "unshaded":
source += "light_mode unshaded;\n";
break;
default:
throw new InvalidOperationException($"Invalid light mode: '{rawMode}'");
}
}
if(rawBlendMode != null){
switch (rawBlendMode)
{
case "mix":
source += "blend_mode mix;\n";
break;
case "add":
source += "blend_mode add;\n";
break;
case "subtract":
source += "blend_mode subtract;\n";
break;
case "multiply":
source += "blend_mode multiply;\n";
break;
default:
throw new InvalidOperationException($"Invalid blend mode: '{rawBlendMode}'");
}
}
source += "void fragment() {\n COLOR = zTexture(UV);\n}";
var preset = ShaderParser.Parse(source, _resourceCache);
CompiledCanvasShader = IoCManager.Resolve<IClydeInternal>().LoadShader(preset, $"canvas_preset_{ID}");
break;
default:
throw new InvalidOperationException($"Invalid shader kind: '{kind}'");
throw new InvalidOperationException($"Invalid shader kind: '{_rawKind}'");
}
// Load stencil data.
if (mapping.TryGetNode("stencil", out YamlMappingNode? stencilData))
{
ReadStencilData(stencilData);
}
if (StencilDataHolder != null) _stencilEnabled = true;
}
private void ReadStencilData(YamlMappingNode stencilData)
[DataDefinition]
public class StencilData
{
_stencilEnabled = true;
[DataField("ref")] public int StencilRef;
if (stencilData.TryGetNode("ref", out var dataNode))
{
_stencilRef = dataNode.AsInt();
}
[DataField("op")] public StencilOp StencilOp;
if (stencilData.TryGetNode("op", out dataNode))
{
_stencilOp = dataNode.AsEnum<StencilOp>();
}
[DataField("func")] public StencilFunc StencilFunc;
if (stencilData.TryGetNode("func", out dataNode))
{
_stencilFunc = dataNode.AsEnum<StencilFunc>();
}
[DataField("readMask")] public int ReadMask = unchecked((int) uint.MaxValue);
if (stencilData.TryGetNode("readMask", out dataNode))
{
_stencilReadMask = dataNode.AsInt();
}
if (stencilData.TryGetNode("writeMask", out dataNode))
{
_stencilWriteMask = dataNode.AsInt();
}
}
private void ReadSourceKind(YamlMappingNode mapping)
{
var path = mapping.GetNode("path").AsResourcePath();
Source = _resourceCache.GetResource<ShaderSourceResource>(path);
if (mapping.TryGetNode<YamlMappingNode>("params", out var paramMapping))
{
ShaderParams = new Dictionary<string, object>();
foreach (var item in paramMapping)
{
var name = item.Key.AsString();
if (!Source.ParsedShader.Uniforms.TryGetValue(name, out var uniformDefinition))
{
Logger.ErrorS("shader", "Shader param '{0}' does not exist on shader '{1}'", name, path);
continue;
}
var value = _parseUniformValue(item.Value, uniformDefinition.Type.Type);
ShaderParams.Add(name, value);
}
}
}
private void ReadCanvasKind(YamlMappingNode mapping)
{
var source = "";
if (mapping.TryGetNode("light_mode", out var node))
{
switch (node.AsString())
{
case "normal":
break;
case "unshaded":
source += "light_mode unshaded;\n";
break;
default:
throw new InvalidOperationException($"Invalid light mode: '{node.AsString()}'");
}
}
if (mapping.TryGetNode("blend_mode", out node))
{
switch (node.AsString())
{
case "mix":
source += "blend_mode mix;\n";
break;
case "add":
source += "blend_mode add;\n";
break;
case "subtract":
source += "blend_mode subtract;\n";
break;
case "multiply":
source += "blend_mode multiply;\n";
break;
default:
throw new InvalidOperationException($"Invalid blend mode: '{node.AsString()}'");
}
}
source += "void fragment() {\n COLOR = zTexture(UV);\n}";
var preset = ShaderParser.Parse(source, _resourceCache);
CompiledCanvasShader = _clyde.LoadShader(preset, $"canvas_preset_{ID}");
[DataField("writeMask")] public int WriteMask = unchecked((int) uint.MaxValue);
}
private static object _parseUniformValue(YamlNode node, ShaderDataType dataType)

View File

@@ -0,0 +1,14 @@
using System;
namespace Robust.Client.Graphics
{
public class WindowFocusedEventArgs : EventArgs
{
public WindowFocusedEventArgs(bool focused)
{
Focused = focused;
}
public bool Focused { get; }
}
}

View File

@@ -19,6 +19,8 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.Core;
@@ -117,8 +119,8 @@ namespace Robust.Client.Input
public void SaveToUserData()
{
var mapping = new YamlMappingNode();
var ser = YamlObjectSerializer.NewWriter(mapping);
var mapping = new MappingDataNode();
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var modifiedBindings = _modifiedKeyFunctions
.Select(p => _bindingsByFunction[p])
@@ -141,15 +143,13 @@ namespace Robust.Client.Input
.Where(p => _bindingsByFunction[p].Count == 0)
.ToArray();
var version = 1;
ser.DataField(ref version, "version", 1);
ser.DataField(ref modifiedBindings, "binds", Array.Empty<KeyBindingRegistration>());
ser.DataField(ref leaveEmpty, "leaveEmpty", Array.Empty<BoundKeyFunction>());
mapping.AddNode("version", new ValueDataNode("1"));
mapping.AddNode("binds", serializationManager.WriteValue(modifiedBindings));
mapping.AddNode("leaveEmpty", serializationManager.WriteValue(leaveEmpty));
var path = new ResourcePath(KeybindsPath);
using var writer = new StreamWriter(_resourceMan.UserData.Create(path));
var stream = new YamlStream {new(mapping)};
var stream = new YamlStream {new(mapping.ToMappingNode())};
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}
@@ -416,12 +416,14 @@ namespace Robust.Client.Input
var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode;
var baseSerializer = YamlObjectSerializer.NewReader(mapping);
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var robustMapping = mapping.ToDataNode() as MappingDataNode;
if (robustMapping == null) throw new InvalidOperationException();
var foundBinds = baseSerializer.TryReadDataField<KeyBindingRegistration[]>("binds", out var baseKeyRegs);
if (foundBinds && baseKeyRegs != null && baseKeyRegs.Length > 0)
if (robustMapping.TryGetNode("binds", out var BaseKeyRegsNode))
{
var baseKeyRegs = serializationManager.ReadValueOrThrow<KeyBindingRegistration[]>(BaseKeyRegsNode);
foreach (var reg in baseKeyRegs)
{
if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName))
@@ -447,11 +449,11 @@ namespace Robust.Client.Input
}
}
if (userData)
if (userData && robustMapping.TryGetNode("leaveEmpty", out var node))
{
var foundLeaveEmpty = baseSerializer.TryReadDataField<BoundKeyFunction[]>("leaveEmpty", out var leaveEmpty);
var leaveEmpty = serializationManager.ReadValueOrThrow<BoundKeyFunction[]>(node);
if (foundLeaveEmpty && leaveEmpty != null && leaveEmpty.Length > 0)
if (leaveEmpty.Length > 0)
{
// Adding to _modifiedKeyFunctions means that these keybinds won't be loaded from the base file.
// Because they've been explicitly cleared.

View File

@@ -1,33 +1,30 @@
using Robust.Shared.Input;
using Robust.Shared.Serialization;
using Robust.Shared.Input;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Client.Input
{
public struct KeyBindingRegistration : IExposeData
[DataDefinition]
public class KeyBindingRegistration
{
[DataField("function")]
public BoundKeyFunction Function;
public KeyBindingType Type;
[DataField("type")]
public KeyBindingType Type = KeyBindingType.State;
[DataField("key")]
public Keyboard.Key BaseKey;
[DataField("mod1")]
public Keyboard.Key Mod1;
[DataField("mod2")]
public Keyboard.Key Mod2;
[DataField("mod3")]
public Keyboard.Key Mod3;
[DataField("priority")]
public int Priority;
[DataField("canFocus")]
public bool CanFocus;
[DataField("canRepeat")]
public bool CanRepeat;
[DataField("allowSubCombs")]
public bool AllowSubCombs;
void IExposeData.ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref Function, "function", default);
serializer.DataField(ref Type, "type", KeyBindingType.State);
serializer.DataField(ref BaseKey, "key", default);
serializer.DataField(ref Mod1, "mod1", default);
serializer.DataField(ref Mod2, "mod2", default);
serializer.DataField(ref Mod3, "mod3", default);
serializer.DataField(ref Priority, "priority", 0);
serializer.DataField(ref CanFocus, "canFocus", false);
serializer.DataField(ref CanRepeat, "canRepeat", false);
serializer.DataField(ref AllowSubCombs, "allowSubCombs", false);
}
}
}

View File

@@ -1,26 +1,25 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Maths;
using Robust.Shared.Map;
using Robust.Shared.Network.Messages;
using Robust.Client.Graphics;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.Placement
{
@@ -87,7 +86,17 @@ namespace Robust.Client.Placement
public bool Eraser { get; private set; }
/// <summary>
/// The texture we use to show from our placement manager to represent the entity to place
/// Holds the selection rectangle for the eraser
/// </summary>
public Box2? EraserRect { get; set; }
/// <summary>
/// Drawing shader for drawing without being affected by lighting
/// </summary>
private ShaderInstance? _drawingShader { get; set; }
/// <summary>
/// The texture we use to show from our placement manager to represent the entity to place
/// </summary>
public List<IDirectionalTextureProvider>? CurrentTextures { get; set; }
@@ -115,14 +124,6 @@ namespace Robust.Client.Placement
if (value != null)
{
PlacementOffset = value.PlacementOffset;
if (value.Components.ContainsKey("BoundingBox") && value.Components.ContainsKey("Physics"))
{
var map = value.Components["BoundingBox"];
var serializer = YamlObjectSerializer.NewReader(map);
serializer.DataField(ref _colliderAABB, "aabb", new Box2(0f, 0f, 0f, 0f));
return;
}
}
_colliderAABB = new Box2(0f, 0f, 0f, 0f);
@@ -153,6 +154,8 @@ namespace Robust.Client.Placement
public void Initialize()
{
_drawingShader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
NetworkManager.RegisterNetMessage<MsgPlacement>(MsgPlacement.NAME, HandlePlacementMessage);
_modeDictionary.Clear();
@@ -182,7 +185,17 @@ namespace Robust.Client.Placement
.Bind(EngineKeyFunctions.EditorGridPlace, InputCmdHandler.FromDelegate(
session =>
{
if (IsActive && !Eraser) ActivateGridMode();
if (IsActive)
{
if (Eraser)
{
EraseRectMode();
}
else
{
ActivateGridMode();
}
}
}))
.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler(
(session, coords, uid) =>
@@ -190,6 +203,13 @@ namespace Robust.Client.Placement
if (!IsActive)
return false;
if (EraserRect.HasValue)
{
HandleRectDeletion(StartPoint, EraserRect.Value);
EraserRect = null;
return true;
}
if (Eraser)
{
if (HandleDeletion(coords))
@@ -308,6 +328,7 @@ namespace Robust.Client.Placement
_placenextframe = false;
IsActive = false;
Eraser = false;
EraserRect = null;
PlacementOffset = Vector2i.Zero;
}
@@ -384,6 +405,15 @@ namespace Robust.Client.Placement
NetworkManager.ClientSendMessage(msg);
}
public void HandleRectDeletion(EntityCoordinates start, Box2 rect)
{
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
msg.PlaceType = PlacementManagerMessage.RequestRectRemove;
msg.EntityCoordinates = new EntityCoordinates(StartPoint.EntityId, rect.BottomLeft);
msg.RectSize = rect.Size;
NetworkManager.ClientSendMessage(msg);
}
public void ToggleEraser()
{
if (!Eraser && !IsActive)
@@ -459,11 +489,62 @@ namespace Robust.Client.Placement
return true;
}
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)
{
var ent = PlayerManager.LocalPlayer?.ControlledEntity;
if (ent == null)
{
coordinates = new EntityCoordinates();
return false;
}
else
{
var map = ent.Transform.MapID;
if (map == MapId.Nullspace || !Eraser)
{
coordinates = new EntityCoordinates();
return false;
}
coordinates = EntityCoordinates.FromMap(ent.EntityManager, MapManager,
eyeManager.ScreenToMap(new ScreenCoordinates(_inputManager.MouseScreenPosition)));
return true;
}
}
/// <inheritdoc />
public void FrameUpdate(FrameEventArgs e)
{
if (!CurrentMousePosition(out var mouseScreen))
{
if (EraserRect.HasValue)
{
if (!CurrentEraserMouseCoordinates(out EntityCoordinates end))
return;
float b, l, t, r;
if (StartPoint.X < end.X)
{
l = StartPoint.X;
r = end.X;
}
else
{
l = end.X;
r = StartPoint.X;
}
if (StartPoint.Y < end.Y)
{
b = StartPoint.Y;
t = end.Y;
}
else
{
b = end.Y;
t = StartPoint.Y;
}
EraserRect = new Box2(l, b, r, t);
}
return;
}
CurrentMode!.AlignPlacementMode(mouseScreen);
@@ -501,6 +582,15 @@ namespace Robust.Client.Placement
PlacementType = PlacementTypes.Grid;
}
private void EraseRectMode()
{
if (!CurrentEraserMouseCoordinates(out EntityCoordinates coordinates))
return;
StartPoint = coordinates;
EraserRect = new Box2(coordinates.Position, Vector2.Zero);
}
private bool DeactivateSpecialPlacement()
{
if (PlacementType == PlacementTypes.None)
@@ -513,7 +603,14 @@ namespace Robust.Client.Placement
private void Render(DrawingHandleWorld handle)
{
if (CurrentMode == null || !IsActive)
{
if (EraserRect.HasValue)
{
handle.UseShader(_drawingShader);
handle.DrawRect(EraserRect.Value, new Color(255, 0, 0, 50));
}
return;
}
CurrentMode.Render(handle);

View File

@@ -1,19 +1,22 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
namespace Robust.Client.Player
{
public interface IPlayerManager
public interface IPlayerManager : Shared.Players.ISharedPlayerManager
{
IEnumerable<IPlayerSession> Sessions { get; }
new IEnumerable<IPlayerSession> Sessions { get; }
IReadOnlyDictionary<NetUserId, IPlayerSession> SessionsDict { get; }
LocalPlayer? LocalPlayer { get; }
int PlayerCount { get; }
int MaxPlayers { get; }
/// <summary>
/// Invoked after LocalPlayer is changed
/// </summary>
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
event EventHandler PlayerListUpdated;
void Initialize();
@@ -23,4 +26,15 @@ namespace Robust.Client.Player
void ApplyPlayerStates(IEnumerable<PlayerState>? list);
}
public class LocalPlayerChangedEventArgs : EventArgs
{
public readonly LocalPlayer? OldPlayer;
public readonly LocalPlayer? NewPlayer;
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
{
OldPlayer = oldPlayer;
NewPlayer = newPlayer;
}
}
}

View File

@@ -1,20 +1,11 @@
using Robust.Shared.Players;
using System;
using Robust.Shared.Players;
namespace Robust.Client.Player
{
/// <summary>
/// Client side session of a player.
/// Client side session of a player.
/// </summary>
public interface IPlayerSession : ICommonSession
{
/// <summary>
/// Current name of this player.
/// </summary>
new string Name { get; set; }
/// <summary>
/// Current connection latency of this session from the server to their client.
/// </summary>
short Ping { get; set; }
}
[Obsolete("Use the base " + nameof(ICommonSession))]
public interface IPlayerSession : ICommonSession { }
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Configuration;
@@ -8,6 +8,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -30,6 +31,21 @@ namespace Robust.Client.Player
/// </summary>
private readonly Dictionary<NetUserId, IPlayerSession> _sessions = new();
/// <inheritdoc />
public IEnumerable<ICommonSession> NetworkedSessions
{
get
{
if (LocalPlayer is not null)
return new[] {LocalPlayer.Session};
return Enumerable.Empty<ICommonSession>();
}
}
/// <inheritdoc />
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public int PlayerCount => _sessions.Values.Count;
@@ -37,10 +53,24 @@ namespace Robust.Client.Player
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
/// <inheritdoc />
[ViewVariables] public LocalPlayer? LocalPlayer { get; private set; }
[ViewVariables]
public LocalPlayer? LocalPlayer
{
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] public IEnumerable<IPlayerSession> Sessions => _sessions.Values;
[ViewVariables]
IEnumerable<IPlayerSession> IPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public IReadOnlyDictionary<NetUserId, IPlayerSession> SessionsDict => _sessions;
@@ -177,7 +207,7 @@ namespace Robust.Client.Player
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);
}

View File

@@ -1,25 +1,53 @@
using Robust.Shared.Enums;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
namespace Robust.Client.Player
{
internal sealed class PlayerSession : IPlayerSession
{
/// <inheritdoc />
public SessionStatus Status { get; set; } = SessionStatus.Connecting;
internal SessionStatus Status { get; set; } = SessionStatus.Connecting;
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
public IEntity? AttachedEntity { get; set; }
/// <inheritdoc />
public EntityUid? AttachedEntityUid => AttachedEntity?.Uid;
/// <inheritdoc />
public NetUserId UserId { get; }
/// <inheritdoc cref="IPlayerSession" />
public string Name { get; set; } = "<Unknown>";
internal string Name { get; set; } = "<Unknown>";
/// <inheritdoc cref="IPlayerSession" />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
/// <inheritdoc />
public short Ping { get; set; }
internal short Ping { get; set; }
/// <inheritdoc />
public INetChannel ConnectedClient { get; internal set; } = null!;
/// <inheritdoc />
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
/// <summary>
/// Creates an instance of a PlayerSession.

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Robust.Client.Prototypes
{
public sealed class ClientPrototypeManager : PrototypeManager
{
[Dependency] private readonly IClyde _clyde = default!;
private readonly List<FileSystemWatcher> _watchers = new();
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
private CancellationTokenSource _reloadToken = new();
private readonly HashSet<ResourcePath> _reloadQueue = new();
public override void Initialize()
{
base.Initialize();
NetManager.RegisterNetMessage<MsgReloadPrototypes>(MsgReloadPrototypes.NAME, accept: NetMessageAccept.Server);
_clyde.OnWindowFocused += WindowFocusedChanged;
WatchResources();
}
private void WindowFocusedChanged(WindowFocusedEventArgs args)
{
#if !FULL_RELEASE
if (args.Focused && _reloadQueue.Count > 0)
{
Timer.Spawn(_reloadDelay, ReloadPrototypeQueue, _reloadToken.Token);
}
else
{
_reloadToken.Cancel();
_reloadToken = new CancellationTokenSource();
}
#endif
}
private void ReloadPrototypeQueue()
{
#if !FULL_RELEASE
var then = DateTime.Now;
var msg = NetManager.CreateNetMessage<MsgReloadPrototypes>();
msg.Paths = _reloadQueue.ToArray();
NetManager.ClientSendMessage(msg);
foreach (var path in _reloadQueue)
{
ReloadPrototypes(path);
}
_reloadQueue.Clear();
Logger.Info($"Reloaded prototypes in {(int) (DateTime.Now - then).TotalMilliseconds} ms");
#endif
}
private void WatchResources()
{
#if !FULL_RELEASE
foreach (var path in Resources.GetContentRoots().Select(r => r.ToString())
.Where(r => Directory.Exists(r + "/Prototypes")).Select(p => p + "/Prototypes"))
{
var watcher = new FileSystemWatcher(path, "*.yml")
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += (_, args) =>
{
switch (args.ChangeType)
{
case WatcherChangeTypes.Renamed:
case WatcherChangeTypes.Deleted:
return;
case WatcherChangeTypes.Created:
// case WatcherChangeTypes.Deleted:
case WatcherChangeTypes.Changed:
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
TaskManager.RunOnMainThread(() =>
{
var file = new ResourcePath(args.FullPath);
foreach (var root in IoCManager.Resolve<IResourceManager>().GetContentRoots())
{
if (!file.TryRelativeTo(root, out var relative))
{
continue;
}
_reloadQueue.Add(relative);
}
});
};
watcher.EnableRaisingEvents = true;
_watchers.Add(watcher);
}
#endif
}
}
}

View File

@@ -5,7 +5,7 @@ using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement.ResourceTypes
namespace Robust.Client.ResourceManagement
{
/// <summary>
/// Loads the **source code** of a shader.

View File

@@ -11,13 +11,13 @@
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.166" />
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" PrivateAssets="All" />
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="nfluidsynth" Version="0.3.1" />
<PackageReference Include="NJsonSchema" Version="10.3.8" />
<PackageReference Include="NVorbis" Version="0.10.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NJsonSchema" Version="10.3.1" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
<PackageReference Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageReference Include="OpenToolkit.OpenAL" Version="4.0.0-pre9.1" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" />

View File

@@ -0,0 +1,73 @@
using Robust.Client.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Client.Serialization
{
[TypeSerializer]
public class AppearanceVisualizerSerializer : ITypeSerializer<AppearanceVisualizer, MappingDataNode>
{
public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context = null)
{
if (!node.TryGetNode("type", out var typeNode))
throw new InvalidMappingException("No type specified for AppearanceVisualizer!");
if (typeNode is not ValueDataNode typeValueDataNode)
throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!");
var type = IoCManager.Resolve<IReflectionManager>()
.YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value);
if (type == null)
throw new InvalidMappingException(
$"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!");
var newNode = (MappingDataNode)node.Copy();
newNode.RemoveNode("type");
return serializationManager.Read(type, newNode, context, skipHook);
}
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
if (!node.TryGetNode("type", out var typeNode) || typeNode is not ValueDataNode valueNode)
{
return new ErrorNode(node, "Missing/Invalid type", true);
}
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
var type = reflectionManager.YamlTypeTagLookup(typeof(AppearanceVisualizer), valueNode.Value);
if (type == null)
{
return new ErrorNode(node, $"Failed to resolve type: {valueNode.Value}", true);
}
return serializationManager.ValidateNode(type, node.CopyCast<MappingDataNode>().RemoveNode("type"));
}
public DataNode Write(ISerializationManager serializationManager, AppearanceVisualizer value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
var mapping = serializationManager.WriteValueAs<MappingDataNode>(value.GetType(), value, alwaysWrite, context);
mapping.AddNode("type", new ValueDataNode(value.GetType().Name));
return mapping;
}
public AppearanceVisualizer Copy(ISerializationManager serializationManager, AppearanceVisualizer source,
AppearanceVisualizer target, bool skipHook, ISerializationContext? context = null)
{
return serializationManager.Copy(source, target, context)!;
}
}
}

View File

@@ -1,30 +1,54 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface
{
// Code and design heavily inspired by WPF/Avalonia.
public partial class Control
{
public event Action<Control>? OnMinimumSizeChanged;
private Vector2 _size;
[ViewVariables] internal Vector2? PreviousMeasure;
[ViewVariables] internal UIBox2? PreviousArrange;
private float _sizeFlagsStretchRatio = 1;
private Vector2? _calculatedMinimumSize;
private Vector2 _customMinimumSize;
private SizeFlags _sizeFlagsHorizontal = SizeFlags.Fill;
private SizeFlags _sizeFlagsVertical = SizeFlags.Fill;
private bool _layoutDirty;
private float _minWidth;
private float _minHeight;
private float _setWidth = float.NaN;
private float _setHeight = float.NaN;
private float _maxWidth = float.PositiveInfinity;
private float _maxHeight = float.PositiveInfinity;
private bool _horizontalExpand;
private bool _verticalExpand;
private HAlignment _horizontalAlignment;
private VAlignment _verticalAlignment;
private Thickness _margin;
private bool _isLayoutUpdateOverrideUsed;
private bool _measuring;
[ViewVariables] public Vector2 DesiredSize { get; private set; }
[ViewVariables] public Vector2i DesiredPixelSize => (Vector2i) (DesiredSize * UIScale);
[ViewVariables] public bool IsMeasureValid { get; private set; }
[ViewVariables] public bool IsArrangeValid { get; private set; }
[ViewVariables]
public Thickness Margin
{
get => _margin;
set => _margin = value;
}
/// <summary>
/// Called when the <see cref="UIScale"/> for this control changes.
/// </summary>
protected internal virtual void UIScaleChanged()
{
MinimumSizeChanged();
InvalidateMeasure();
}
/// <summary>
@@ -56,7 +80,6 @@ namespace Robust.Client.UserInterface
_size = value;
Resized();
UpdateLayout();
}
}
@@ -179,12 +202,36 @@ namespace Robust.Client.UserInterface
/// Horizontal size flags for container layout.
/// </summary>
[ViewVariables]
[Obsolete("Use HorizontalAlignment and HorizontalExpand instead.")]
public SizeFlags SizeFlagsHorizontal
{
get => _sizeFlagsHorizontal;
get
{
var flags = HorizontalAlignment switch
{
HAlignment.Stretch => SizeFlags.Fill,
HAlignment.Left => SizeFlags.None,
HAlignment.Center => SizeFlags.ShrinkCenter,
HAlignment.Right => SizeFlags.ShrinkEnd,
_ => throw new ArgumentOutOfRangeException()
};
if (_horizontalExpand)
flags |= SizeFlags.Expand;
return flags;
}
set
{
_sizeFlagsHorizontal = value;
HorizontalExpand = (value & SizeFlags.Expand) != 0;
HorizontalAlignment = (value & ~SizeFlags.Expand) switch
{
SizeFlags.None => HAlignment.Left,
SizeFlags.Fill => HAlignment.Stretch,
SizeFlags.ShrinkCenter => HAlignment.Center,
SizeFlags.ShrinkEnd => HAlignment.Right,
_ => throw new ArgumentOutOfRangeException()
};
Parent?.UpdateLayout();
}
@@ -193,18 +240,87 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Vertical size flags for container layout.
/// </summary>
[Obsolete("Use VerticalAlignment and VerticalExpand instead.")]
[ViewVariables]
public SizeFlags SizeFlagsVertical
{
get => _sizeFlagsVertical;
get
{
var flags = _verticalAlignment switch
{
VAlignment.Stretch => SizeFlags.Fill,
VAlignment.Top => SizeFlags.None,
VAlignment.Center => SizeFlags.ShrinkCenter,
VAlignment.Bottom => SizeFlags.ShrinkEnd,
_ => throw new ArgumentOutOfRangeException()
};
if (_verticalExpand)
flags |= SizeFlags.Expand;
return flags;
}
set
{
_sizeFlagsVertical = value;
VerticalExpand = (value & SizeFlags.Expand) != 0;
VerticalAlignment = (value & ~SizeFlags.Expand) switch
{
SizeFlags.None => VAlignment.Top,
SizeFlags.Fill => VAlignment.Stretch,
SizeFlags.ShrinkCenter => VAlignment.Center,
SizeFlags.ShrinkEnd => VAlignment.Bottom,
_ => throw new ArgumentOutOfRangeException()
};
Parent?.UpdateLayout();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public HAlignment HorizontalAlignment
{
get => _horizontalAlignment;
set
{
_horizontalAlignment = value;
InvalidateArrange();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public VAlignment VerticalAlignment
{
get => _verticalAlignment;
set
{
_verticalAlignment = value;
InvalidateArrange();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public bool HorizontalExpand
{
get => _horizontalExpand;
set
{
_horizontalExpand = value;
Parent?.InvalidateMeasure();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public bool VerticalExpand
{
get => _verticalExpand;
set
{
_verticalExpand = value;
Parent?.InvalidateArrange();
}
}
/// <summary>
/// Stretch ratio used to give shared of the available space in case multiple siblings are set to expand
/// in a container
@@ -225,12 +341,12 @@ namespace Robust.Client.UserInterface
_sizeFlagsStretchRatio = value;
Parent?.UpdateLayout();
Parent?.InvalidateArrange();
}
}
/// <summary>
/// A combination of <see cref="CustomMinimumSize" /> and <see cref="CalculateMinimumSize" />,
/// A combination of <see cref="MinSize" /> and <see cref="CalculateMinimumSize" />,
/// Whichever is greater.
/// Use this for whenever you need the *actual* minimum size of something.
/// </summary>
@@ -238,24 +354,13 @@ namespace Robust.Client.UserInterface
/// This is in virtual pixels.
/// </remarks>
/// <seealso cref="CombinedPixelMinimumSize"/>
[ViewVariables]
public Vector2 CombinedMinimumSize
{
get
{
if (!_calculatedMinimumSize.HasValue)
{
_updateMinimumSize();
DebugTools.Assert(_calculatedMinimumSize.HasValue);
}
return Vector2.ComponentMax(CustomMinimumSize, _calculatedMinimumSize!.Value);
}
}
[Obsolete("Use DesiredSize and Measure()")]
public Vector2 CombinedMinimumSize => DesiredSize;
/// <summary>
/// The <see cref="CombinedMinimumSize"/>, in physical pixels.
/// </summary>
[Obsolete("Use DesiredSize and Measure()")]
public Vector2i CombinedPixelMinimumSize => (Vector2i) (CombinedMinimumSize * UIScale);
/// <summary>
@@ -264,24 +369,95 @@ namespace Robust.Client.UserInterface
/// <seealso cref="CalculateMinimumSize" />
/// <seealso cref="CombinedMinimumSize" />
[ViewVariables]
[Obsolete("Use MinSize instead.")]
public Vector2 CustomMinimumSize
{
get => _customMinimumSize;
get => (_minWidth, _minHeight);
set => (MinWidth, MinHeight) = Vector2.ComponentMax(Vector2.Zero, value);
}
public Vector2 MinSize
{
get => (_minWidth, _minHeight);
set => (MinWidth, MinHeight) = Vector2.ComponentMax(Vector2.Zero, value);
}
public Vector2 SetSize
{
get => (_setWidth, _setHeight);
set => (SetWidth, SetHeight) = value;
}
public Vector2 MaxSize
{
get => (_maxWidth, _maxHeight);
set => (MaxWidth, MaxHeight) = value;
}
[ViewVariables(VVAccess.ReadWrite)]
public float MinWidth
{
get => _minWidth;
set
{
_customMinimumSize = Vector2.ComponentMax(Vector2.Zero, value);
MinimumSizeChanged();
_minWidth = value;
InvalidateMeasure();
}
}
private void _updateMinimumSize()
[ViewVariables(VVAccess.ReadWrite)]
public float MinHeight
{
if (_stylingDirty)
get => _minHeight;
set
{
ForceRunStyleUpdate();
_minHeight = value;
InvalidateMeasure();
}
}
_calculatedMinimumSize = Vector2.ComponentMax(Vector2.Zero, CalculateMinimumSize());
[ViewVariables(VVAccess.ReadWrite)]
public float SetWidth
{
get => _setWidth;
set
{
_setWidth = value;
InvalidateMeasure();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public float SetHeight
{
get => _setHeight;
set
{
_setHeight = value;
InvalidateMeasure();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public float MaxWidth
{
get => _maxWidth;
set
{
_maxWidth = value;
InvalidateMeasure();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public float MaxHeight
{
get => _maxHeight;
set
{
_maxHeight = value;
InvalidateMeasure();
}
}
/// <summary>
@@ -289,27 +465,31 @@ namespace Robust.Client.UserInterface
/// Do NOT call this directly to get the minimum size for layout purposes!
/// Use <see cref="CombinedMinimumSize" /> for the ACTUAL minimum size.
/// </summary>
[Obsolete("Implement MeasureOverride instead")]
protected virtual Vector2 CalculateMinimumSize()
{
var min = Vector2.Zero;
foreach (var child in Children)
{
min = Vector2.ComponentMax(min, child.CombinedMinimumSize);
}
return min;
return Vector2.Zero;
}
/// <summary>
/// Tells the GUI system that the minimum size of this control may have changed,
/// so that say containers will re-sort it if necessary.
/// </summary>
[Obsolete("Use InvalidateMeasure()")]
public void MinimumSizeChanged()
{
_calculatedMinimumSize = null;
OnMinimumSizeChanged?.Invoke(this);
InvalidateMeasure();
}
Parent?.MinimumSizeChanged();
UpdateLayout();
public void InvalidateMeasure()
{
if (!IsMeasureValid)
return;
IsMeasureValid = false;
IsArrangeValid = false;
UserInterfaceManagerInternal.QueueMeasureUpdate(this);
}
/// <summary>
@@ -320,89 +500,241 @@ namespace Robust.Client.UserInterface
/// where running the deferred layout updating system in the UI manager can be annoying.
/// If you are forced to use this in regular code, you have found a bug.
/// </remarks>
[Obsolete("Call Arrange manually for unit tests or call Measure manually for early measures.")]
public void ForceRunLayoutUpdate()
{
DoLayoutUpdate();
foreach (var child in Children)
{
child.ForceRunLayoutUpdate();
}
// TODO: Fix or remove this
if (PreviousArrange.HasValue)
Arrange(PreviousArrange.Value);
}
protected void UpdateLayout()
public void InvalidateArrange()
{
if (_layoutDirty)
if (!IsArrangeValid)
{
// Already queued for a layout update, don't bother.
return;
}
_layoutDirty = true;
UserInterfaceManagerInternal.QueueLayoutUpdate(this);
IsArrangeValid = false;
UserInterfaceManagerInternal.QueueArrangeUpdate(this);
}
protected void FitChildInPixelBox(Control child, UIBox2i pixelBox)
[Obsolete("Use InvalidateArrange()")]
protected void UpdateLayout()
{
var topLeft = pixelBox.TopLeft / UIScale;
var bottomRight = pixelBox.BottomRight / UIScale;
FitChildInBox(child, new UIBox2(topLeft, bottomRight));
InvalidateArrange();
}
protected void FitChildInBox(Control child, UIBox2 box)
public void Measure(Vector2 availableSize)
{
DebugTools.Assert(child.Parent == this);
if (!IsMeasureValid || PreviousMeasure != availableSize)
{
IsMeasureValid = true;
var desired = MeasureCore(availableSize);
var (minX, minY) = child.CombinedMinimumSize;
var newPosX = box.Left;
var newSizeX = minX;
if (desired.X < 0 || desired.Y < 0 || !float.IsFinite(desired.X) || !float.IsFinite(desired.Y))
throw new InvalidOperationException("Invalid size returned from Measure()");
if ((child.SizeFlagsHorizontal & SizeFlags.ShrinkEnd) != 0)
{
newPosX += (box.Width - minX);
}
else if ((child.SizeFlagsHorizontal & SizeFlags.ShrinkCenter) != 0)
{
newPosX += (box.Width - minX) / 2;
}
else if ((child.SizeFlagsHorizontal & SizeFlags.Fill) != 0)
{
newSizeX = Math.Max(box.Width, newSizeX);
}
var prev = DesiredSize;
DesiredSize = desired;
PreviousMeasure = availableSize;
var newPosY = box.Top;
var newSizeY = minY;
if ((child.SizeFlagsVertical & SizeFlags.ShrinkEnd) != 0)
{
newPosY += (box.Height - minY);
if (prev != desired && Parent != null && !Parent._measuring)
Parent?.InvalidateMeasure();
}
else if ((child.SizeFlagsVertical & SizeFlags.ShrinkCenter) != 0)
{
newPosY += (box.Height - minY) / 2;
}
else if ((child.SizeFlagsVertical & SizeFlags.Fill) != 0)
{
newSizeY = Math.Max(box.Height, newSizeY);
}
child.Position = new Vector2(newPosX, newPosY);
child.Size = new Vector2(newSizeX, newSizeY);
}
internal void DoLayoutUpdate()
protected virtual Vector2 MeasureCore(Vector2 availableSize)
{
if (!Visible)
return default;
if (_stylingDirty)
ForceRunStyleUpdate();
var withoutMargin = _margin.Deflate(availableSize);
var constrained = ApplySizeConstraints(this, withoutMargin);
Vector2 measured;
try
{
_measuring = true;
measured = Vector2.ComponentMax(
MeasureOverride(constrained),
// For the time being keep the old CalculateMinimumSize around.
#pragma warning disable 618
CalculateMinimumSize());
#pragma warning restore 618
}
finally
{
_measuring = false;
}
if (!float.IsNaN(SetWidth))
{
measured.X = SetWidth;
}
measured.X = Math.Clamp(measured.X, MinWidth, MaxWidth);
if (!float.IsNaN(SetHeight))
{
measured.Y = SetHeight;
}
measured.Y = Math.Clamp(measured.Y, MinHeight, MaxHeight);
measured = _margin.Inflate(measured);
return Vector2.ComponentMin(measured, availableSize);
}
protected virtual Vector2 MeasureOverride(Vector2 availableSize)
{
var min = Vector2.Zero;
foreach (var child in Children)
{
child.Measure(availableSize);
min = Vector2.ComponentMax(min, child.DesiredSize);
}
return min;
}
public void ArrangePixel(UIBox2i finalRect)
{
var topLeft = finalRect.TopLeft / UIScale;
var bottomRight = finalRect.BottomRight / UIScale;
Arrange(new UIBox2(topLeft, bottomRight));
}
public void Arrange(UIBox2 finalRect)
{
if (!IsMeasureValid)
Measure(PreviousMeasure ?? finalRect.Size);
if (!IsArrangeValid || PreviousArrange != finalRect)
{
IsArrangeValid = true;
ArrangeCore(finalRect);
PreviousArrange = finalRect;
}
}
protected virtual void ArrangeCore(UIBox2 finalRect)
{
if (!Visible)
return;
var withoutMargins = _margin.Deflate(finalRect);
var availWithoutMargins = withoutMargins.Size;
var size = availWithoutMargins;
var origin = withoutMargins.TopLeft;
if (_horizontalAlignment != HAlignment.Stretch)
size.X = Math.Min(size.X, DesiredSize.X - _margin.SumHorizontal);
if (_verticalAlignment != VAlignment.Stretch)
size.Y = Math.Min(size.Y, DesiredSize.Y - _margin.SumVertical);
size = ApplySizeConstraints(this, size);
Size = size;
_isLayoutUpdateOverrideUsed = true;
#pragma warning disable 618
LayoutUpdateOverride();
_layoutDirty = false;
#pragma warning restore 618
if (!_isLayoutUpdateOverrideUsed)
{
var arranged = ArrangeOverride(size);
size = Vector2.ComponentMin(arranged, size);
}
switch (HorizontalAlignment)
{
case HAlignment.Stretch:
case HAlignment.Center:
origin.X += (availWithoutMargins.X - size.X) / 2;
break;
case HAlignment.Right:
origin.X += availWithoutMargins.X - size.X;
break;
}
switch (VerticalAlignment)
{
case VAlignment.Stretch:
case VAlignment.Center:
origin.Y += (availWithoutMargins.Y - size.Y) / 2;
break;
case VAlignment.Bottom:
origin.Y += availWithoutMargins.Y - size.Y;
break;
}
Position = origin;
Size = size;
}
protected virtual void LayoutUpdateOverride()
protected virtual Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var child in Children)
{
FitChildInPixelBox(child, PixelSizeBox);
child.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize));
}
return finalSize;
}
[Obsolete("Use Control.ArrangePixel")]
protected void FitChildInPixelBox(Control child, UIBox2i pixelBox)
{
child.ArrangePixel(pixelBox);
}
[Obsolete("Use Control.Arrange")]
protected void FitChildInBox(Control child, UIBox2 box)
{
child.Arrange(box);
}
[Obsolete("Implement ArrangeOverride instead.")]
protected virtual void LayoutUpdateOverride()
{
_isLayoutUpdateOverrideUsed = false;
}
private static Vector2 ApplySizeConstraints(Control control, Vector2 avail)
{
var minW = control._minWidth;
var setW = control._setWidth;
var maxW = control._maxWidth;
var maxConstraint = float.IsNaN(setW) ? float.PositiveInfinity : setW;
maxW = MathHelper.Clamp(maxConstraint, minW, maxW);
var minConstraint = float.IsNaN(setW) ? 0 : setW;
minW = MathHelper.Clamp(maxW, minConstraint, minW);
var minH = control._minHeight;
var setH = control._setHeight;
var maxH = control._maxHeight;
maxConstraint = float.IsNaN(setH) ? float.PositiveInfinity : setH;
maxH = MathHelper.Clamp(maxConstraint, minH, maxH);
minConstraint = float.IsNaN(setH) ? 0 : setH;
minH = MathHelper.Clamp(minW, minConstraint, minH);
return (
Math.Clamp(avail.X, minW, maxW),
Math.Clamp(avail.Y, minH, maxH));
}
/// <summary>
@@ -443,5 +775,21 @@ namespace Robust.Client.UserInterface
/// </summary>
ShrinkEnd = 8,
}
public enum HAlignment
{
Stretch,
Left,
Center,
Right
}
public enum VAlignment
{
Stretch,
Top,
Center,
Bottom
}
}
}

View File

@@ -233,7 +233,7 @@ namespace Robust.Client.UserInterface
protected virtual void StylePropertiesChanged()
{
MinimumSizeChanged();
InvalidateMeasure();
}
public void ForceRunStyleUpdate()

View File

@@ -154,7 +154,8 @@ namespace Robust.Client.UserInterface
_propagateVisibilityChanged(value);
// TODO: unhardcode this.
// Many containers ignore children if they're invisible, so that's why we're replicating that ehre.
Parent?.MinimumSizeChanged();
Parent?.InvalidateMeasure();
InvalidateMeasure();
}
}
@@ -594,7 +595,7 @@ namespace Robust.Client.UserInterface
/// <param name="newChild">The new child.</param>
protected virtual void ChildAdded(Control newChild)
{
MinimumSizeChanged();
InvalidateMeasure();
}
/// <summary>
@@ -604,7 +605,7 @@ namespace Robust.Client.UserInterface
protected virtual void Parented(Control newParent)
{
StylesheetUpdateRecursive();
UpdateLayout();
InvalidateMeasure();
}
/// <summary>
@@ -642,7 +643,7 @@ namespace Robust.Client.UserInterface
/// <param name="child">The former child.</param>
protected virtual void ChildRemoved(Control child)
{
MinimumSizeChanged();
InvalidateMeasure();
}
/// <summary>

View File

@@ -35,8 +35,60 @@ namespace Robust.Client.UserInterface.Controls
public int? SeparationOverride { get; set; }
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var separation = ActualSeparation;
var minSize = Vector2.Zero;
var first = true;
foreach (var child in Children)
{
if (!child.Visible)
{
continue;
}
child.Measure(availableSize);
var childSize = child.DesiredSize;
if (Vertical)
{
var taken = childSize.Y;
if (!first)
{
taken += separation;
}
minSize.Y += taken;
availableSize.Y = Math.Max(0, availableSize.Y - taken);
first = false;
minSize.X = Math.Max(minSize.X, childSize.X);
}
else
{
var taken = childSize.X;
if (!first)
{
taken += separation;
}
minSize.X += taken;
availableSize.X = Math.Max(0, availableSize.X - taken);
first = false;
minSize.Y = Math.Max(minSize.Y, childSize.Y);
}
}
return minSize;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var finalPixel = (Vector2i) (finalSize * UIScale);
var separation = (int) (ActualSeparation * UIScale);
// Step one: figure out the sizes of all our children and whether they want to stretch.
@@ -51,19 +103,20 @@ namespace Robust.Client.UserInterface.Controls
{
continue;
}
var (minX, minY) = child.CombinedPixelMinimumSize;
var (minX, minY) = child.DesiredPixelSize;
int minSize;
bool stretch;
if (Vertical)
{
minSize = minY;
stretch = (child.SizeFlagsVertical & SizeFlags.Expand) == SizeFlags.Expand;
stretch = child.VerticalExpand;
}
else
{
minSize = minX;
stretch = (child.SizeFlagsHorizontal & SizeFlags.Expand) == SizeFlags.Expand;
stretch = child.HorizontalExpand;
}
if (!stretch)
@@ -78,7 +131,7 @@ namespace Robust.Client.UserInterface.Controls
sizeList.Add((child, minSize, minSize, stretch));
}
var stretchMax = Vertical ? PixelHeight : PixelWidth;
var stretchMax = Vertical ? finalPixel.Y : finalPixel.X;
stretchMax -= separation * (ChildCount - 1);
// This is the amount of space allocated for stretchable children.
@@ -152,62 +205,19 @@ namespace Robust.Client.UserInterface.Controls
UIBox2i targetBox;
if (Vertical)
{
targetBox = new UIBox2i(0, offset, PixelWidth, offset+size);
targetBox = new UIBox2i(0, offset, finalPixel.X, offset + size);
}
else
{
targetBox = new UIBox2i(offset, 0, offset+size, PixelHeight);
targetBox = new UIBox2i(offset, 0, offset + size, finalPixel.Y);
}
FitChildInPixelBox(control, targetBox);
control.ArrangePixel(targetBox);
offset += size;
}
}
protected override Vector2 CalculateMinimumSize()
{
var separation = ActualSeparation;
var minWidth = 0f;
var minHeight = 0f;
var first = true;
foreach (var child in Children)
{
if (!child.Visible)
{
continue;
}
var (childWidth, childHeight) = child.CombinedMinimumSize;
if (Vertical)
{
minHeight += childHeight;
if (!first)
{
minHeight += separation;
}
first = false;
minWidth = MathF.Max(minWidth, childWidth);
}
else
{
minWidth += childWidth;
if (!first)
{
minWidth += separation;
}
first = false;
minHeight = MathF.Max(minHeight, childHeight);
}
}
return new Vector2(minWidth, minHeight);
return finalSize;
}
public enum AlignMode : byte

View File

@@ -7,15 +7,19 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public class CenterContainer : Container
{
protected override void LayoutUpdateOverride()
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var max = Vector2.Zero;
foreach (var child in Children)
{
var childSize = child.CombinedMinimumSize;
var childPos = (Size - childSize) / 2;
var childSize = child.DesiredSize;
var childPos = (finalSize - childSize) / 2;
FitChildInBox(child, UIBox2.FromDimensions(childPos, childSize));
child.Arrange(UIBox2.FromDimensions(childPos, childSize));
max = Vector2.ComponentMax(max, childSize);
}
return max;
}
}
}

View File

@@ -31,13 +31,30 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var contentBox = ActualStyleBox.GetContentBox(PixelSizeBox);
var boxSize = ActualStyleBox.MinimumSize / UIScale;
var childBox = Vector2.ComponentMax(availableSize - boxSize, Vector2.Zero);
var min = Vector2.Zero;
foreach (var child in Children)
{
FitChildInPixelBox(child, (UIBox2i) contentBox);
child.Measure(childBox);
min = Vector2.ComponentMax(min, child.DesiredSize);
}
return min + boxSize;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var contentBox = ActualStyleBox.GetContentBox(UIBox2.FromDimensions(Vector2.Zero, finalSize * UIScale));
foreach (var child in Children)
{
child.ArrangePixel((UIBox2i) contentBox);
}
return finalSize;
}
protected internal override void Draw(DrawingHandleScreen handle)
@@ -49,17 +66,6 @@ namespace Robust.Client.UserInterface.Controls
style.Draw(handle, drawBox);
}
protected override Vector2 CalculateMinimumSize()
{
var min = Vector2.Zero;
foreach (var child in Children)
{
min = Vector2.ComponentMax(min, child.CombinedMinimumSize);
}
return min + ActualStyleBox.MinimumSize / UIScale;
}
protected override void DrawModeChanged()
{
switch (DrawMode)

View File

@@ -49,8 +49,8 @@ namespace Robust.Client.UserInterface.Controls
_lineEdit = new LineEdit
{
CustomMinimumSize = new Vector2(40, 0),
SizeFlagsHorizontal = SizeFlags.FillExpand
MinSize = new Vector2(40, 0),
HorizontalExpand = true,
};
AddChild(_lineEdit);

View File

@@ -48,7 +48,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_expandBackwards = value;
UpdateLayout();
InvalidateArrange();
}
}
private bool _expandBackwards;
@@ -100,10 +100,17 @@ namespace Robust.Client.UserInterface.Controls
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxWidth
[Obsolete("Use MaxGridWidth")]
public new float MaxWidth
{
set => MaxGridWidth = value;
}
public float MaxGridWidth
{
set => SetMaxSize(Dimension.Column, value);
}
/// <summary>
/// The max height (in virtual pixels) the grid of elements can have. This dynamically determines
/// the number of rows based on the size of the elements. Setting this puts this grid
@@ -120,12 +127,17 @@ namespace Robust.Client.UserInterface.Controls
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxHeight
[Obsolete("Use MaxGridHeight")]
public new float MaxHeight
{
set => MaxGridHeight = value;
}
public float MaxGridHeight
{
set => SetMaxSize(Dimension.Row, value);
}
private int? _vSeparationOverride;
[SuppressMessage("ReSharper", "StringLiteralTypo")]
@@ -199,8 +211,7 @@ namespace Robust.Client.UserInterface.Controls
_limitType = LimitType.Count;
_limitedDimensionCount = value;
MinimumSizeChanged();
UpdateLayout();
InvalidateMeasure();
}
private void SetMaxSize(Dimension forDimension, float value)
@@ -214,8 +225,7 @@ namespace Robust.Client.UserInterface.Controls
_limitType = LimitType.Size;
_limitSize = value;
MinimumSizeChanged();
UpdateLayout();
InvalidateMeasure();
}
/// <summary>
@@ -263,7 +273,7 @@ namespace Robust.Client.UserInterface.Controls
int maxMinHeight = -1;
foreach (var child in Children)
{
var (minSizeX, minSizeY) = child.CombinedPixelMinimumSize;
var (minSizeX, minSizeY) = child.DesiredPixelSize;
maxMinWidth = Math.Max(maxMinWidth, minSizeX);
maxMinHeight = Math.Max(maxMinHeight, minSizeY);
}
@@ -271,7 +281,7 @@ namespace Robust.Client.UserInterface.Controls
return new Vector2i(maxMinWidth, maxMinHeight);
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
@@ -280,6 +290,12 @@ namespace Robust.Client.UserInterface.Controls
// For the below convention, we pretend that columns have a limit defined, thus
// the amount of rows is not limited (unlimited).
foreach (var child in Children)
{
// TODO: This is not really correct in any fucking way but I CBA to fix this properly.
child.Measure(availableSize);
}
var rows = GetCount(UnlimitedDimension);
var cols = GetCount(LimitedDimension);
var cellSize = CellSize();
@@ -304,7 +320,7 @@ namespace Robust.Client.UserInterface.Controls
// also converting here to our "pretend" scenario where columns have a limit defined.
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.CombinedPixelMinimumSize : cellSize;
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.DesiredPixelSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
@@ -348,9 +364,9 @@ namespace Robust.Client.UserInterface.Controls
return totalSize;
}
protected override void LayoutUpdateOverride()
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
@@ -390,19 +406,19 @@ namespace Robust.Client.UserInterface.Controls
// converting here to our "pretend" scenario where columns have a limit defined
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.CombinedPixelMinimumSize : cellSize;
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.DesiredPixelSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
minRowHeight[row] = Math.Max(minSizeY, minRowHeight[row]);
var colSizeFlag = _limitDimension == Dimension.Column
? child.SizeFlagsHorizontal
: child.SizeFlagsVertical;
var rowSizeFlag = UnlimitedDimension == Dimension.Column
? child.SizeFlagsHorizontal
: child.SizeFlagsVertical;
colExpand[column] = colExpand[column] || (colSizeFlag & SizeFlags.Expand) != 0;
rowExpand[row] = rowExpand[row] || (rowSizeFlag & SizeFlags.Expand) != 0;
var colExpandFlag = _limitDimension == Dimension.Column
? child.HorizontalExpand
: child.VerticalExpand;
var rowExpandFlag = UnlimitedDimension == Dimension.Column
? child.HorizontalExpand
: child.VerticalExpand;
colExpand[column] = colExpand[column] || colExpandFlag;
rowExpand[row] = rowExpand[row] || rowExpandFlag;
index += 1;
}
@@ -550,10 +566,12 @@ namespace Robust.Client.UserInterface.Controls
var boxHeight = _limitDimension == Dimension.Column ? minRowHeight[row] : minColWidth[column];
var box = UIBox2i.FromDimensions(left, top, boxWidth, boxHeight);
FitChildInPixelBox(child, box);
child.ArrangePixel(box);
hOffset += minColWidth[column] + hSep;
}
return finalSize;
}
}

View File

@@ -5,7 +5,7 @@ using System.Diagnostics.Contracts;
using Robust.Client.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timers.Timer;
using Timer = Robust.Shared.Timing.Timer;
namespace Robust.Client.UserInterface.Controls
{
@@ -43,9 +43,7 @@ namespace Robust.Client.UserInterface.Controls
{
Name = "_v_scroll",
SizeFlagsVertical = SizeFlags.Fill,
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
@@ -403,7 +401,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var size = Vector2.Zero;
if (ActualBackground != null)
@@ -475,6 +473,8 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.ValueTarget -= _getScrollSpeed() * args.Delta.Y;
_isAtBottom = _scrollBar.IsAtEnd;
args.Handle();
}
[Pure]

View File

@@ -26,7 +26,7 @@ namespace Robust.Client.UserInterface.Controls
public Label()
{
SizeFlagsVertical = SizeFlags.ShrinkCenter;
VerticalAlignment = VAlignment.Center;
}
/// <summary>
@@ -40,7 +40,7 @@ namespace Robust.Client.UserInterface.Controls
{
_text = value;
_textDimensionCacheValid = false;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -52,7 +52,7 @@ namespace Robust.Client.UserInterface.Controls
{
_clipText = value;
RectClipContent = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -209,7 +209,7 @@ namespace Robust.Client.UserInterface.Controls
Fill = 3
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (!_textDimensionCacheValid)
{
@@ -267,7 +267,7 @@ namespace Robust.Client.UserInterface.Controls
continue;
}
_cachedTextWidths[_cachedTextWidths.Count-1] += metrics.Value.Advance;
_cachedTextWidths[^1] += metrics.Value.Advance;
}
}

View File

@@ -27,7 +27,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_textures = value;
CalculateMinimumSize();
InvalidateMeasure();
}
}
@@ -43,7 +43,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_textureScale = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -60,7 +60,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_canShrink = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -148,7 +148,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_textures.Count == 0 || CanShrink)
{

View File

@@ -1,6 +1,8 @@
using System;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
@@ -17,6 +19,8 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public const float AnchorEnd = 1;
[ViewVariables(VVAccess.ReadWrite)] public bool Debug { get; set; }
public static readonly AttachedProperty MarginLeftProperty = AttachedProperty.Create("MarginLeft",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
@@ -47,6 +51,9 @@ namespace Robust.Client.UserInterface.Controls
public static readonly AttachedProperty GrowVerticalProperty = AttachedProperty.Create("GrowVertical",
typeof(LayoutContainer), typeof(GrowDirection), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty<bool> DebugProperty = AttachedProperty<bool>.Create("Debug",
typeof(LayoutContainer));
public static void SetMarginLeft(Control control, float value)
{
@@ -112,16 +119,10 @@ namespace Robust.Client.UserInterface.Controls
SetMarginBottom(control, diffY + control.GetValue<float>(MarginBottomProperty));
}
public static void SetSize(Control control, Vector2 size)
[Obsolete("Change SetSize on the control instead.")]
public new static void SetSize(Control control, Vector2 size)
{
var (diffX, diffY) = size - control.Size;
// This is just to make subsequent set calls work correctly.
// It should get reset to this exact value next update either way.
control.Size = size;
SetMarginRight(control, diffX + control.GetValue<float>(MarginRightProperty));
SetMarginBottom(control, diffY + control.GetValue<float>(MarginBottomProperty));
control.SetSize = size;
}
/// <summary>
@@ -297,8 +298,9 @@ namespace Robust.Client.UserInterface.Controls
LayoutPresetMode resizeMode = LayoutPresetMode.MinSize,
int margin = 0)
{
control.Measure(Vector2.Infinity);
var newSize = control.Size;
var minSize = control.CombinedMinimumSize;
var minSize = control.DesiredSize;
if ((resizeMode & LayoutPresetMode.KeepWidth) == 0)
{
newSize = new Vector2(minSize.X, newSize.Y);
@@ -445,39 +447,131 @@ namespace Robust.Client.UserInterface.Controls
control.SetValue(MarginBottomProperty, marginBottom);
}
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var min = Vector2.Zero;
var uiScale = UIScale;
foreach (var child in Children)
{
var growH = child.GetValue<GrowDirection>(GrowHorizontalProperty);
var growV = child.GetValue<GrowDirection>(GrowVerticalProperty);
var anchorMargins = CalcAnchorMargins(availableSize, uiScale, child);
var size = availableSize;
if (growH == GrowDirection.Constrain)
size.X = anchorMargins.Width / uiScale;
if (growV == GrowDirection.Constrain)
size.Y = anchorMargins.Height / uiScale;
child.Measure(size);
min = Vector2.ComponentMax(min, child.DesiredSize);
}
return min;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var child in Children)
{
child.Arrange(CalcChildRect(finalSize, UIScale, child, out _));
}
return finalSize;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (!Debug)
return;
var (pSizeX, pSizeY) = PixelSize;
foreach (var child in Children)
{
var anchorLeft = child.GetValue<float>(AnchorLeftProperty);
var anchorTop = child.GetValue<float>(AnchorTopProperty);
var anchorRight = child.GetValue<float>(AnchorRightProperty);
var anchorBottom = child.GetValue<float>(AnchorBottomProperty);
if (!child.GetValue(DebugProperty))
{
continue;
}
var marginLeft = child.GetValue<float>(MarginLeftProperty) * UIScale;
var marginTop = child.GetValue<float>(MarginTopProperty) * UIScale;
var marginRight = child.GetValue<float>(MarginRightProperty) * UIScale;
var marginBottom = child.GetValue<float>(MarginBottomProperty) * UIScale;
var rect = CalcChildRect(Size, UIScale, child, out var anchorSize);
var growHorizontal = child.GetValue<GrowDirection>(GrowHorizontalProperty);
var growVertical = child.GetValue<GrowDirection>(GrowVerticalProperty);
var left = rect.Left * UIScale;
var right = rect.Right * UIScale;
var top = rect.Top * UIScale;
var bottom = rect.Bottom * UIScale;
// Calculate where the control "wants" to be by its anchors/margins.
var left = anchorLeft * pSizeX + marginLeft;
var top = anchorTop * pSizeY + marginTop;
var right = anchorRight * pSizeX + marginRight;
var bottom = anchorBottom * pSizeY + marginBottom;
DrawVLine(anchorSize.Left, Color.Pink);
DrawVLine(anchorSize.Right, Color.Green);
DrawHLine(anchorSize.Top, Color.Pink);
DrawHLine(anchorSize.Bottom, Color.Green);
var (wSizeX, wSizeY) = (right - left, bottom - top);
var (minSizeX, minSizeY) = child.CombinedPixelMinimumSize;
/*
DrawVLine(left, Color.Orange);
DrawVLine(right, Color.Blue);
DrawHLine(top, Color.Orange);
DrawHLine(bottom, Color.Blue);
*/
HandleLayoutOverflow(growHorizontal, minSizeX, left, wSizeX, out var posX, out var sizeX);
HandleLayoutOverflow(growVertical, minSizeY, top, wSizeY, out var posY, out var sizeY);
child.Position = new Vector2(posX, posY) / UserInterfaceManager.UIScale;
child.Size = new Vector2(sizeX, sizeY) / UserInterfaceManager.UIScale;
handle.DrawRect(new UIBox2(left, top, right, bottom), Color.Red, false);
}
void DrawVLine(float x, Color color)
{
handle.DrawLine((x, 0), (x, pSizeY), color);
}
void DrawHLine(float y, Color color)
{
handle.DrawLine((0, y), (pSizeX, y), color);
}
}
private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control child)
{
var (pSizeX, pSizeY) = ourSize * uiScale;
var anchorLeft = child.GetValue<float>(AnchorLeftProperty);
var anchorTop = child.GetValue<float>(AnchorTopProperty);
var anchorRight = child.GetValue<float>(AnchorRightProperty);
var anchorBottom = child.GetValue<float>(AnchorBottomProperty);
var marginLeft = child.GetValue<float>(MarginLeftProperty) * uiScale;
var marginTop = child.GetValue<float>(MarginTopProperty) * uiScale;
var marginRight = child.GetValue<float>(MarginRightProperty) * uiScale;
var marginBottom = child.GetValue<float>(MarginBottomProperty) * uiScale;
var left = anchorLeft * pSizeX + marginLeft;
var top = anchorTop * pSizeY + marginTop;
var right = anchorRight * pSizeX + marginRight;
var bottom = anchorBottom * pSizeY + marginBottom;
// Yes, this can return boxes with left > right (and top > bottom).
// This is "intentional", see comment in CalcChildRect.
return new UIBox2(left, top, right, bottom);
}
private static UIBox2 CalcChildRect(Vector2 ourSize, float uiScale, Control child, out UIBox2 anchorSize)
{
// Calculate where the control "wants" to be by its anchors/margins.
var growHorizontal = child.GetValue<GrowDirection>(GrowHorizontalProperty);
var growVertical = child.GetValue<GrowDirection>(GrowVerticalProperty);
anchorSize = CalcAnchorMargins(ourSize, uiScale, child);
// This intentionally results in negatives if the right bound is < the left bound.
// Which then causes HandleLayoutOverflow to CORRECTLY work from the right bound instead.
var (wSizeX, wSizeY) = (anchorSize.Right - anchorSize.Left, anchorSize.Bottom - anchorSize.Top);
var (minSizeX, minSizeY) = child.DesiredPixelSize;
HandleLayoutOverflow(growHorizontal, minSizeX, anchorSize.Left, wSizeX, out var posX, out var sizeX);
HandleLayoutOverflow(growVertical, minSizeY, anchorSize.Top, wSizeY, out var posY, out var sizeY);
return UIBox2.FromDimensions(posX / uiScale, posY / uiScale, sizeX / uiScale, sizeY / uiScale);
}
private static void HandleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize,
@@ -485,7 +579,7 @@ namespace Robust.Client.UserInterface.Controls
out float size)
{
var overflow = minSize - wSize;
if (overflow <= 0)
if (overflow <= 0 || direction == GrowDirection.Constrain)
{
pos = wPos;
size = wSize;
@@ -514,7 +608,7 @@ namespace Robust.Client.UserInterface.Controls
{
if (owner.Parent is LayoutContainer container)
{
container.UpdateLayout();
container.InvalidateArrange();
}
}
@@ -537,7 +631,12 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// The control will expand on all axes equally to reach its minimum size.
/// </summary>
Both
Both,
/// <summary>
/// The control will not be allowed to grow on this axis.
/// </summary>
Constrain,
}
/// <seealso cref="Control.SetMarginsPreset" />

View File

@@ -242,18 +242,22 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var font = _getFont();
var style = _getStyleBox();
return new Vector2(0, font.GetHeight(UIScale) / UIScale) + style.MinimumSize / UIScale;
}
protected override void LayoutUpdateOverride()
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var style = _getStyleBox();
FitChildInPixelBox(_renderBox, (UIBox2i) style.GetContentBox(PixelSizeBox));
_renderBox.ArrangePixel(
(UIBox2i) style.GetContentBox(
UIBox2.FromDimensions(Vector2.Zero, finalSize * UIScale)));
return finalSize;
}
protected internal override void TextEntered(GUITextEventArgs args)

View File

@@ -1,7 +1,9 @@
using System;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
[Obsolete("Set Margin directly")]
public class MarginContainer : Container
{
public int? MarginBottomOverride { get; set; }
@@ -9,36 +11,41 @@ namespace Robust.Client.UserInterface.Controls
public int? MarginRightOverride { get; set; }
public int? MarginLeftOverride { get; set; }
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var top = MarginTopOverride ?? 0;
var bottom = MarginBottomOverride ?? 0;
var left = MarginLeftOverride ?? 0;
var right = MarginRightOverride ?? 0;
var box = UIBox2.FromDimensions(left, top, Width - right - left, Height - bottom - top);
var margin = GetMargin();
var availWithoutMargin = margin.Deflate(availableSize);
var max = Vector2.Zero;
foreach (var child in Children)
{
FitChildInBox(child, box);
child.Measure(availWithoutMargin);
max = Vector2.ComponentMax(max, child.DesiredSize);
}
return margin.Inflate(max);
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var box = GetMargin().Deflate(UIBox2.FromDimensions(Vector2.Zero, finalSize));
foreach (var child in Children)
{
child.Arrange(box);
}
return finalSize;
}
private Thickness GetMargin()
{
var top = MarginTopOverride ?? 0;
var bottom = MarginBottomOverride ?? 0;
var left = MarginLeftOverride ?? 0;
var right = MarginRightOverride ?? 0;
var childMinSize = Vector2.Zero;
foreach (var child in Children)
{
childMinSize = Vector2.ComponentMax(child.CombinedMinimumSize, childMinSize);
}
return childMinSize + (left + right, top + bottom);
var margin = new Thickness(left, top, right, bottom);
return margin;
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Robust.Client.UserInterface.Controls
{
Children =
{
(_popupVBox = new VBoxContainer {CustomMinimumSize = (300, 0)})
(_popupVBox = new VBoxContainer {MinSize = (300, 0)})
}
};
_popup.OnPopupHide += PopupHidden;
@@ -113,7 +113,7 @@ namespace Robust.Client.UserInterface.Controls
break;
case MenuSeparator _:
var control = new Control {CustomMinimumSize = (0, 6)};
var control = new Control {MinSize = (0, 6)};
container.AddChild(control);
break;
}

View File

@@ -71,14 +71,14 @@ namespace Robust.Client.UserInterface.Controls
_label = new Label
{
StyleClasses = { StyleClassOptionButton },
SizeFlagsHorizontal = SizeFlags.FillExpand,
HorizontalExpand = true,
};
hBox.AddChild(_label);
var textureRect = new TextureRect
{
StyleClasses = { StyleClassOptionTriangle },
SizeFlagsVertical = SizeFlags.ShrinkCenter,
VerticalAlignment = VAlignment.Center,
};
hBox.AddChild(textureRect);
}
@@ -112,7 +112,8 @@ namespace Robust.Client.UserInterface.Controls
if (show)
{
var globalPos = GlobalPosition;
var (minX, minY) = _popupVBox.CombinedMinimumSize;
_popupVBox.Measure(Vector2.Infinity);
var (minX, minY) = _popupVBox.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
_popup.Open(box);

View File

@@ -15,15 +15,36 @@ namespace Robust.Client.UserInterface.Controls
private readonly Popup _popup;
private readonly VBoxContainer _popupVBox;
private readonly Label _label;
private readonly TextureRect _triangle;
public int ItemCount => _buttonData.Count;
/// <summary>
/// If true, hides the triangle that normally appears to the right of the button label
/// </summary>
public bool HideTriangle
{
get => _hideTriangle;
set
{
_hideTriangle = value;
_triangle.Visible = !_hideTriangle;
}
}
private bool _hideTriangle;
/// <summary>
/// StyleClasses to apply to the options that popup when clicking this button.
/// </summary>
public ICollection<string> OptionStyleClasses { get; }
public event Action<ItemSelectedEventArgs>? OnItemSelected;
public string Prefix { get; set; }
public OptionButton()
{
OptionStyleClasses = new List<string>();
AddStyleClass(StyleClassButton);
Prefix = "";
OnPressed += OnPressedInternal;
@@ -39,16 +60,17 @@ namespace Robust.Client.UserInterface.Controls
_label = new Label
{
StyleClasses = { StyleClassOptionButton },
SizeFlagsHorizontal = SizeFlags.FillExpand,
HorizontalExpand = true,
};
hBox.AddChild(_label);
var textureRect = new TextureRect
_triangle = new TextureRect
{
StyleClasses = { StyleClassOptionTriangle },
SizeFlagsVertical = SizeFlags.ShrinkCenter,
VerticalAlignment = VAlignment.Center,
Visible = !HideTriangle
};
hBox.AddChild(textureRect);
hBox.AddChild(_triangle);
}
public void AddItem(Texture icon, string label, int? id = null)
@@ -73,6 +95,10 @@ namespace Robust.Client.UserInterface.Controls
Text = label,
ToggleMode = true
};
foreach (var styleClass in OptionStyleClasses)
{
button.AddStyleClass(styleClass);
}
button.OnPressed += ButtonOnPressed;
var data = new ButtonData(label, button)
{
@@ -92,7 +118,8 @@ namespace Robust.Client.UserInterface.Controls
if (show)
{
var globalPos = GlobalPosition;
var (minX, minY) = _popupVBox.CombinedMinimumSize;
_popupVBox.Measure(Vector2.Infinity);
var (minX, minY) = _popupVBox.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
_popup.Open(box);
@@ -170,6 +197,10 @@ namespace Robust.Client.UserInterface.Controls
}
}
/// <summary>
/// Select by index rather than id. Throws exception if item with that index
/// not in this control.
/// </summary>
public void Select(int idx)
{
if (_idMap.TryGetValue(SelectedId, out var prevIdx))
@@ -182,11 +213,29 @@ namespace Robust.Client.UserInterface.Controls
data.Button.Pressed = true;
}
/// <summary>
/// Select by index rather than id.
/// </summary>
/// <returns>false if item with that index not in this control</returns>
public bool TrySelect(int idx)
{
if (idx < 0 || idx >= _buttonData.Count) return false;
Select(idx);
return true;
}
/// throws exception if item with this ID is not in this control
public void SelectId(int id)
{
Select(GetIdx(id));
}
/// <returns>false if item with id not in this control</returns>
public bool TrySelectId(int id)
{
return _idMap.TryGetValue(id, out var idx) && TrySelect(idx);
}
public int GetIdx(int id)
{
return _idMap[id];

View File

@@ -31,8 +31,7 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar = new VScrollBar
{
Name = "_v_scroll",
SizeFlagsVertical = SizeFlags.Fill,
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
@@ -44,7 +43,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_styleBoxOverride = value;
MinimumSizeChanged();
InvalidateMeasure();
_invalidateEntries();
}
}
@@ -168,7 +167,7 @@ namespace Robust.Client.UserInterface.Controls
_invalidateEntries();
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return _getStyleBox()?.MinimumSize ?? Vector2.Zero;
}

View File

@@ -17,26 +17,32 @@ namespace Robust.Client.UserInterface.Controls
style?.Draw(handle, PixelSizeBox);
}
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var contentBox = _getStyleBox()?.GetContentBox(PixelSizeBox) ?? PixelSizeBox;
foreach (var child in Children)
{
FitChildInPixelBox(child, (UIBox2i) contentBox);
}
}
protected override Vector2 CalculateMinimumSize()
{
var styleSize = _getStyleBox()?.MinimumSize ?? Vector2.Zero;
var styleSize = (_getStyleBox()?.MinimumSize ?? Vector2.Zero) / UIScale;
var measureSize = Vector2.ComponentMax(availableSize - styleSize, Vector2.Zero);
var childSize = Vector2.Zero;
foreach (var child in Children)
{
childSize = Vector2.ComponentMax(childSize, child.CombinedMinimumSize);
child.Measure(measureSize);
childSize = Vector2.ComponentMax(childSize, child.DesiredSize);
}
return styleSize / UIScale + childSize;
return styleSize + childSize;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var pixelSize = finalSize * UIScale;
var ourSize = UIBox2.FromDimensions(Vector2.Zero, pixelSize);
var contentBox = _getStyleBox()?.GetContentBox(ourSize) ?? ourSize;
foreach (var child in Children)
{
child.ArrangePixel((UIBox2i) contentBox);
}
return finalSize;
}
[System.Diagnostics.Contracts.Pure]

View File

@@ -30,7 +30,7 @@ namespace Robust.Client.UserInterface.Controls
PopupContainer.SetAltOrigin(this, altPos);
_desiredSize = box.Value.Size;
MinimumSizeChanged();
InvalidateMeasure();
}
Visible = true;
@@ -52,9 +52,11 @@ namespace Robust.Client.UserInterface.Controls
OnPopupHide?.Invoke();
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return Vector2.ComponentMax(_desiredSize, base.CalculateMinimumSize());
return Vector2.ComponentMax(
_desiredSize,
base.MeasureOverride(Vector2.ComponentMax(availableSize, _desiredSize)));
}
}
}

View File

@@ -54,15 +54,15 @@ namespace Robust.Client.UserInterface.Controls
{
if (owner.Parent is PopupContainer container)
{
container.UpdateLayout();
container.InvalidateArrange();
}
}
protected override void LayoutUpdateOverride()
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var child in Children)
{
var size = child.CombinedMinimumSize;
var size = child.DesiredSize;
var offset = child.GetValue<Vector2>(PopupOriginProperty);
var altPos = child.GetValue<Vector2?>(AltOriginProperty);
@@ -105,22 +105,25 @@ namespace Robust.Client.UserInterface.Controls
offset -= (0, offset.Y);
}
FitChildInBox(child, UIBox2.FromDimensions(offset, size));
child.Arrange(UIBox2.FromDimensions(offset, size));
}
return finalSize;
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// Do NOT inherit minimum size from contents!
// Just clip 'em.
return (0, 0);
// Measure to availableSize so that child controls never get too large to fit the whole screen.
base.MeasureOverride(availableSize);
return availableSize;
}
protected override void Resized()
{
base.Resized();
UpdateLayout();
InvalidateArrange();
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_backgroundStyleBoxOverride = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -28,7 +28,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_foregroundStyleBoxOverride = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -76,7 +76,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var bgSize = _getBackground()?.MinimumSize ?? Vector2.Zero;
var fgSize = _getForeground()?.MinimumSize ?? Vector2.Zero;

View File

@@ -11,13 +11,11 @@ namespace Robust.Client.UserInterface.Controls
private FormattedMessage? _message;
private RichTextEntry _entry;
public float? MaxWidth { get; set; }
public void SetMessage(FormattedMessage message)
{
_message = message;
_entry = new RichTextEntry(_message);
_updateEntry();
InvalidateMeasure();
}
public void SetMessage(string message)
@@ -27,42 +25,17 @@ namespace Robust.Client.UserInterface.Controls
SetMessage(msg);
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_message == null)
{
return Vector2.Zero;
}
var width = 0f;
if (MaxWidth.HasValue)
{
width = _entry.Width / UIScale;
}
return (width, _entry.Height / UIScale);
}
private void _updateEntry()
{
var font = _getFont();
_entry.Update(font, availableSize.X * UIScale, UIScale);
if (_message != null)
{
var oldHeight = _entry.Height;
var oldWidth = _entry.Width;
_entry.Update(font, (MaxWidth ?? Width) * UIScale, UIScale);
if (oldHeight != _entry.Height || MaxWidth != null && _entry.Width != oldWidth)
{
MinimumSizeChanged();
}
}
}
protected override void StylePropertiesChanged()
{
base.StylePropertiesChanged();
_updateEntry();
return (_entry.Width / UIScale, _entry.Height / UIScale);
}
protected internal override void Draw(DrawingHandleScreen handle)
@@ -77,13 +50,6 @@ namespace Robust.Client.UserInterface.Controls
_entry.Draw(handle, _getFont(), SizeBox, 0, new Stack<FormattedMessage.Tag>(), UIScale);
}
protected override void Resized()
{
base.Resized();
_updateEntry();
}
[Pure]
private Font _getFont()
{

View File

@@ -101,12 +101,12 @@ namespace Robust.Client.UserInterface.Controls
}
var box = _getGrabberBox();
if (!box.Contains(args.RelativePosition))
if (!box.Contains(args.RelativePixelPosition))
{
return;
}
_grabData = (args.RelativePosition, Value);
_grabData = (args.RelativePixelPosition, Value);
_updatePseudoClass();
args.Handle();
}
@@ -129,13 +129,13 @@ namespace Robust.Client.UserInterface.Controls
if (_grabData == null)
{
var box = _getGrabberBox();
_isHovered = box.Contains(args.RelativePosition);
_isHovered = box.Contains(args.RelativePixelPosition);
_updatePseudoClass();
return;
}
var (grabPos, grabValue) = _grabData.Value;
var (grabRelX, grabRelY) = args.RelativePosition - grabPos;
var (grabRelX, grabRelY) = args.RelativePixelPosition - grabPos;
float moved;
if (_orientation == OrientationMode.Horizontal)
@@ -219,7 +219,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
protected override Vector2 CalculateMinimumSize()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return _getGrabberStyleBox()?.MinimumSize ?? Vector2.Zero;
}

View File

@@ -17,6 +17,9 @@ namespace Robust.Client.UserInterface.Controls
private bool _suppressScrollValueChanged;
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
public ScrollContainer()
{
MouseFilter = MouseFilterMode.Pass;
@@ -26,14 +29,14 @@ namespace Robust.Client.UserInterface.Controls
_hScrollBar = new HScrollBar
{
Visible = false,
SizeFlagsVertical = SizeFlags.ShrinkEnd,
SizeFlagsHorizontal = SizeFlags.Fill
VerticalAlignment = VAlignment.Bottom,
HorizontalAlignment = HAlignment.Stretch
};
_vScrollBar = new VScrollBar
{
Visible = false,
SizeFlagsVertical = SizeFlags.Fill,
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
VerticalAlignment = VAlignment.Stretch,
HorizontalAlignment = HAlignment.Right
};
AddChild(_hScrollBar);
AddChild(_vScrollBar);
@@ -47,7 +50,7 @@ namespace Robust.Client.UserInterface.Controls
set
{
_vScrollEnabled = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
@@ -57,17 +60,50 @@ namespace Robust.Client.UserInterface.Controls
set
{
_hScrollEnabled = value;
MinimumSizeChanged();
InvalidateMeasure();
}
}
protected override void LayoutUpdateOverride()
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_vScrollEnabled)
{
_vScrollBar.Measure(availableSize);
availableSize.X -= _vScrollBar.DesiredSize.X;
}
if (_hScrollEnabled)
{
_hScrollBar.Measure(availableSize);
availableSize.Y -= _hScrollBar.DesiredSize.Y;
}
var constraint = new Vector2(
_hScrollEnabled ? float.PositiveInfinity : availableSize.X,
_vScrollEnabled ? float.PositiveInfinity : availableSize.Y);
var size = Vector2.Zero;
foreach (var child in Children)
{
child.Measure(constraint);
size = Vector2.ComponentMax(size, child.DesiredSize);
}
// Unlike WPF/Avalonia we report ZERO here instead of available size.
// This is to fix a bunch of jank with e.g. BoxContainer.
// Tbh this might be a mistake.
// DockPanel when.
return Vector2.Zero;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
if (_vScrollBar?.Parent == null || _hScrollBar?.Parent == null)
{
// Just don't run this before we're properly initialized.
return;
return Vector2.Zero;
}
var maxChildMinSize = Vector2.Zero;
foreach (var child in Children)
@@ -77,31 +113,31 @@ namespace Robust.Client.UserInterface.Controls
continue;
}
maxChildMinSize = Vector2.ComponentMax(child.CombinedMinimumSize, maxChildMinSize);
maxChildMinSize = Vector2.ComponentMax(child.DesiredSize, maxChildMinSize);
}
var (cWidth, cHeight) = maxChildMinSize;
var hBarSize = _hScrollBar.CombinedMinimumSize.Y;
var vBarSize = _vScrollBar.CombinedMinimumSize.X;
var hBarSize = _hScrollBar.DesiredSize.Y;
var vBarSize = _vScrollBar.DesiredSize.X;
var (sWidth, sHeight) = Size;
var (sWidth, sHeight) = finalSize;
try
{
// Suppress events to avoid weird recursion.
_suppressScrollValueChanged = true;
if (Width < cWidth)
if (sWidth < cWidth && _hScrollEnabled)
{
sHeight -= hBarSize;
}
if (Height < cHeight)
if (sHeight < cHeight && _vScrollEnabled)
{
sWidth -= vBarSize;
}
if (sWidth < cWidth)
if (sWidth < cWidth && _hScrollEnabled)
{
_hScrollBar.Visible = _hScrollVisible = true;
_hScrollBar.Page = sWidth;
@@ -112,7 +148,7 @@ namespace Robust.Client.UserInterface.Controls
_hScrollBar.Visible = _hScrollVisible = false;
}
if (sHeight < cHeight)
if (sHeight < cHeight && _vScrollEnabled)
{
_vScrollBar.Visible = _vScrollVisible = true;
_vScrollBar.Page = sHeight;
@@ -129,10 +165,19 @@ namespace Robust.Client.UserInterface.Controls
_suppressScrollValueChanged = false;
}
var sSize = (sWidth, sHeight);
if (_vScrollVisible)
{
_vScrollBar.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize));
}
FitChildInPixelBox(_vScrollBar, PixelSizeBox);
FitChildInPixelBox(_hScrollBar, PixelSizeBox);
if (_hScrollVisible)
{
_hScrollBar.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize));
}
var realFinalSize = (
_hScrollEnabled ? Math.Max(cWidth, sWidth) : sWidth,
_vScrollEnabled ? Math.Max(cHeight, sHeight) : sHeight);
foreach (var child in Children)
{
@@ -142,47 +187,11 @@ namespace Robust.Client.UserInterface.Controls
}
var position = -_getScrollValue();
var rect = UIBox2.FromDimensions(position, Vector2.ComponentMax(child.CombinedMinimumSize, sSize));
FitChildInBox(child, rect);
}
}
protected override Vector2 CalculateMinimumSize()
{
var totalX = 0f;
var totalY = 0f;
foreach (var child in Children)
{
if (child == _hScrollBar || child == _vScrollBar)
{
continue;
}
if (!_vScrollEnabled)
{
totalY = Math.Max(totalY, child.CombinedMinimumSize.Y);
}
if (!_hScrollEnabled)
{
totalX = Math.Max(totalX, child.CombinedMinimumSize.X);
}
var rect = UIBox2.FromDimensions(position, realFinalSize);
child.Arrange(rect);
}
if (_vScrollEnabled)
{
totalX += _vScrollBar.CombinedMinimumSize.X;
totalY = Math.Max(_vScrollBar.CombinedMinimumSize.Y, totalY);
}
if (_hScrollEnabled)
{
totalY += _hScrollBar.CombinedMinimumSize.Y;
totalX = Math.Max(_vScrollBar.CombinedMinimumSize.X, totalX);
}
return new Vector2(totalX, totalY);
return finalSize;
}
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
@@ -191,13 +200,15 @@ namespace Robust.Client.UserInterface.Controls
if (_vScrollEnabled)
{
_vScrollBar.ValueTarget -= args.Delta.Y * 50;
_vScrollBar.ValueTarget -= args.Delta.Y * ScrollSpeedY;
}
if (_hScrollEnabled)
{
_hScrollBar.ValueTarget += args.Delta.X * 50;
_hScrollBar.ValueTarget += args.Delta.X * ScrollSpeedX;
}
args.Handle();
}
protected override void ChildAdded(Control newChild)
@@ -223,10 +234,12 @@ namespace Robust.Client.UserInterface.Controls
{
h = 0;
}
if (!_vScrollVisible)
{
v = 0;
}
return new Vector2(h, v);
}
@@ -237,7 +250,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
UpdateLayout();
InvalidateArrange();
}
}
}

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