Compare commits

...

99 Commits

Author SHA1 Message Date
Paul Ritter
17869c16cd when the bool 2021-04-06 11:58:17 +02:00
metalgearsloth
d8aad89c2f Split entity management from entity queries (#1665)
* Split entity lookups from entitymanager

* Helps if you subscribe dingus

* Handle map changes

* Stacks instead

* Make mapchanges use a queue because it's probably better

Moves likely only care about the latest position

* IoC what you did there

* IoC refactor

* Minor optimisations

* Apply feedback

* My IQ dropped 3 sizes that day

* Rest of acruid's feedback

* final_no_actual commit

* enlightenment?

* Liftoff

* final_commit_v2_actual

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-04-06 13:29:48 +10:00
DrSmugleaf
2a349eb023 Optimize serialization reading, create benchmarks (#1679)
* Add Robust.Benchmarks and read string benchmark

* Separate serialization manager methods, use compiled lambdas to call manager read

4 us > 200 ns

* Add int and data definition with string benchmarks

* Make serialization population use expressions to create definitions

* Make benchmark classes internal and create seed data definition

* Add complex data definition read benchmark

* Create primitive serializers, remove primitive special case

|                 Method |        Mean |     Error |    StdDev |
|----------------------- |------------:|----------:|----------:|
|             ReadString |    227.1 ns |   4.47 ns |   5.65 ns |
|            ReadInteger |    245.4 ns |   4.82 ns |   6.26 ns |
|  ReadDataDefWithString |    804.7 ns |  15.27 ns |  16.34 ns |
| ReadSeedDataDefinition | 15,846.8 ns | 312.89 ns | 773.39 ns |

* Remove testing code

* Setup delegates during initialize

* Revert "Setup delegates during initialize"

This reverts commit 7ff4d4eaaa.

* Store delegates in a concurrent dictionary because I really cannot be arsed to generate them on initialize at this point
2021-04-05 14:50:33 +02:00
Vera Aguilera Puerto
47ad07b3d2 Adds directed event for when an entity's BodyType changes. (#1681)
Removes old Anchored C# event.
2021-04-05 13:17:09 +02:00
ShadowCommander
aacf6522b4 Remove NetId requirement for local event subscriptions (#1675) 2021-04-02 18:45:59 -07:00
ShadowCommander
c73d27b9ae Add RSI path to error log (#1676) 2021-04-02 16:45:21 -07:00
Vera Aguilera Puerto
f068b30a7c Adds Prototype Id Validator for Dictionaries whose keys are prototype IDs. (#1673) 2021-04-02 15:47:48 +02:00
Vera Aguilera Puerto
5400dddcfc Fix ComponentDependencies tests for debug & release 2021-04-02 13:56:42 +02:00
metalgearsloth
6cf5fdc5d6 Grid-trees for rendering (#1666) 2021-04-02 20:25:16 +11:00
Vera Aguilera Puerto
5d46663881 Fix ComponentDependencies tests 2021-03-31 22:17:55 +02:00
Acruid
8e0f227940 PVS Bugfixes 1: The Debuggening (#1671)
* Wrapped the parallel GetMail function in a try/catch.
Added a hack to the ViewCulling leave message that skips ents that don't exist.
Always send ALL map and grid entities to the client.
More info logging about adding/removing maps/grids.

* Will now still send required map critical entities even if client is not attached to an entity.
PvsEnabled and PvsRange are now writeable.
2021-03-31 21:56:11 +02:00
metalgearsloth
73a13fff9a Fix grid bounds upon deserialization 2021-03-31 19:33:19 +11:00
DrSmugleaf
de2e505a12 Make content able to choose which log level leads to test failures (#1670)
* Make content able to choose which log level leads to test failures

* Now make it make sense
2021-03-31 19:26:38 +11:00
DrSmugleaf
a9f7c7a76f Fix no HWId userdata error in integration tests (#1667) 2021-03-30 15:39:43 +02:00
Vera Aguilera Puerto
37401c26c9 Adds a custom editor for Prototypes to ViewVariables. (#1663)
Also improves VV a bit.
2021-03-30 15:33:15 +02:00
Vera Aguilera Puerto
528cd1e0e5 Fix fullscreen crash 2021-03-30 15:32:15 +02:00
Pieter-Jan Briers
2959456bec Fix integration test networking. 2021-03-30 13:28:26 +02:00
Metal Gear Sloth
8951712495 Add NaN guards to physics 2021-03-30 21:59:15 +11:00
metalgearsloth
d8612aff64 Re-implement inertia (#1652)
* Implement inertia

* actual SPEEN

* Sync mass

* bitcoin miner

* I am le dumb

* also dis

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-30 21:42:05 +11:00
Acruid
e16732eb7b Network View Bubble (#1629)
* Adds barbones culling.

* Visibility culling and recursive parent ent additions.
DebugEntityNetView improvements.
Visibility moved from session to eyecomponent.

* Multiple viewport support.

* Perf improvements.

* Removed old netbubble system from ServerEntityManager.
Supports old NaN system for entities leaving view.
Supports old SendFullMap optimization for anchored, non-updating Entities.

* Fixes size of netView box.

* Remove empty EntityManager.Update method.
Switching ViewCulling back to PLINQ.
2021-03-29 16:17:34 -07:00
Acruid
91f61bb9de Reverts component NetId storage in ComponentManager back to the way Acruid originally designed it.
Removes NetId methods from IEntity, content does not need to be messing with them.
Fixes bug in DeleteComponent where the ComponentDeleted event was not being raised if a component did not have a NetId.
2021-03-29 03:40:48 -07:00
Pieter-Jan Briers
ddc91d05ec Some work towards multi-monitor support in Clyde.
Most of this was me experimenting with GLFW, but I figured I'd still commit it.
2021-03-28 21:23:38 +02:00
Acruid
ef22842b90 Fixes bug where FirstTimePredicted was not being set properly for the first predicted frame. 2021-03-27 20:16:04 -07:00
Pieter-Jan Briers
303e2152d2 UIScale now updates dynamically.
So if you move the window between different monitors with different scaling, the game updates.
2021-03-28 01:55:35 +01:00
Vera Aguilera Puerto
37fc0d0d2a Set correct class constrains for prototype id list serializers 2021-03-27 22:47:24 +01:00
Vera Aguilera Puerto
53987e1e5d Adds prototype "Variant" helper methods to IPrototypeManager (#1662) 2021-03-27 22:40:07 +01:00
metalgearsloth
3216d7770b Fix net.rate cvar warning (#1659)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-27 02:14:45 -07:00
Acruid
3203ca2ff4 Removed Control.Update from the UI system. UI Controls have no business running code in simulation updates.
Refactored the client update loop so that the GameStateManager is in full control of the simulation update.
2021-03-26 17:46:34 -07:00
metalgearsloth
e22254cd51 Clear velocities on container insertion (#1653)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-26 22:39:11 +01:00
Acruid
7ed722f669 Visibility moved from session to EyeComponent (#1657) 2021-03-26 22:38:45 +01:00
DrSmugleaf
4864096b2a Add prototype id list serializer and tests (#1658)
* Add prototype id list serializer and tests

* Bring old .Value code back

* Paul made me do this
2021-03-26 20:52:22 +01:00
Acruid
5161385de4 Removed unused Update and Resize code from GameStates. Presenters can get resize events from the interface manager (hint: you won't ever need to), and there is no reason for a UI Presenter to do anything in simulation ticks (UI should be event driven, not polling data every frame). 2021-03-25 14:01:51 -07:00
Acruid
98e009b38f Removed the GameController dependency from Clyde.
Removed the ConfigurationManager dependency from FontManager.
2021-03-25 11:36:57 -07:00
Vera Aguilera Puerto
3863ab8f62 Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation (#1656)
* Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation

* Paul changes

* cleanup, better stuff
2021-03-25 14:26:14 +01:00
Metal Gear Sloth
f576eb5125 Optimise showbb 2021-03-25 23:32:06 +11:00
Acruid
314742ccd8 NullableHelper tests now properly set up their required DI container instead of reusing the container from whatever test was ran before it. Service Locator anti-pattern :( 2021-03-25 02:02:09 -07:00
Acruid
f9074811f9 Adds constructor injection to the IoCManager & DependencyCollection. 2021-03-25 01:16:08 -07:00
Pieter-Jan Briers
5f3e1eb378 Frame graph now shows when GCs occur. 2021-03-25 02:24:38 +01:00
Pieter-Jan Briers
3c1ee20ca1 A 2021-03-25 02:05:28 +01:00
Pieter-Jan Briers
3768f5e68e Remove allocs from ContainerSlot.ContainedEntities. 2021-03-25 01:56:06 +01:00
Pieter-Jan Briers
765a560380 Fix integer overflow breaking Lidgren metrics. 2021-03-25 01:47:45 +01:00
Metal Gear Sloth
39ae3ac653 Optimise physics do not research 2021-03-24 22:35:17 +11:00
Pieter-Jan Briers
e48f4027e5 Probably fix running Robust directly for some people. 2021-03-23 21:28:36 +01:00
ShadowCommander
2fa1e98faf Fix CopyWithTypeSerializer not copying when null (#1651) 2021-03-22 11:02:32 +01:00
Pieter-Jan Briers
cedfa0ee2f Nothing to see here. 2021-03-21 20:37:10 +01:00
Acruid
92f44b390e SoundSystem Improvements (#1649) 2021-03-21 16:35:52 +01:00
Pieter-Jan Briers
65a42f9209 Prototype reloading now fires an event. 2021-03-21 16:25:52 +01:00
Acruid
ebf53248cf TestLogHandler now fails the test if a warning or higher is logged. 2021-03-19 13:42:13 -07:00
Acruid
289f637e8a Entity Lifetime Levels (#1644)
* Added an entity lifetime levels property.
Added exception when recursively deleting an entity.

* Add a directed event 'EntityTerminatingEvent' for right before an entity is deleted.

* Added MapInit lifestage to entities.
2021-03-18 22:53:05 -07:00
metalgearsloth
d7c13f30c8 Fix showbb awake (#1632)
* Fix showbb awake

* Slight tweak

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-17 13:58:26 -07:00
Vera Aguilera Puerto
0dac17ae5e ConfigurationManager.OnValueChanged's invokeImmediately now accounts for overriden values correctly. 2021-03-17 20:22:44 +01:00
Pieter-Jan Briers
9a19a774fa Use stencil test to cull FOV-hidden lights early.
Massive shader optimization.
2021-03-17 13:16:47 +01:00
Pieter-Jan Briers
81f49d5eb2 Fix moving to the end of a textbox. 2021-03-17 01:22:45 +01:00
DrSmugleaf
4f3b4ac2d2 Changes for content server nullability (#1642) 2021-03-16 15:47:49 +01:00
Pieter-Jan Briers
e428056b52 Rldrsc now works with textures. 2021-03-16 12:39:19 +01:00
DrSmugleaf
8dc9d2989a Fix not being able to use shared entity systems in update order (#1638) 2021-03-16 12:38:31 +01:00
Paul
fd8c90dcbb reverting cringe (moved controller metrics cvar get to server) 2021-03-16 12:05:56 +01:00
Vera Aguilera Puerto
ffe4e5a8ab Add Enabled property to CollisionWake component. (#1641)
* Add Enabled property to CollisionWake component.

* Set property in HandleComponentState
2021-03-16 11:38:46 +01:00
Paul Ritter
6e5026d270 adds prometheus logging to physicscontrollers (#1640) 2021-03-16 11:32:46 +01:00
metalgearsloth
946c4166dc Move RootControl frameupdate to after queue (#1625)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-16 09:15:18 +01:00
Paul Ritter
7d2fb85a04 adds custom typeserializers (#1636)
retires DataFieldWithConstantAttribute & DataFieldWithFlagAttribute in favor of new customtypeserializers
adds prototypeidvalidation, just needs to be added to the corresponding fields
fixes some behaviour in yamllinter
2021-03-15 13:24:29 +01:00
metalgearsloth
d6ec078519 Fix static sleeping crash (#1630)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-14 12:44:49 +11:00
DrSmugleaf
32256fc4d9 Remove printing ticks in integration tests (#1627) 2021-03-13 20:12:49 +01:00
DrSmugleaf
37bbdfe7ff Fix serialization logging not printing messages (#1628) 2021-03-13 20:12:41 +01:00
metalgearsloth
c906675cdf Set collidable on CollisionWake removal (#1626)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-13 20:13:00 +11:00
DrSmugleaf
90bb5574c1 Add a CVar to disable texture preloading for tests (#1623) 2021-03-13 13:25:10 +11:00
Acruid
7b50dcd969 Removed IEntityManager.SpawnEntityNoMapInit. Every entity spawned into an uninitialized map does not have mapinit ran, so this is useless. 2021-03-11 22:17:31 -08:00
Pieter-Jan Briers
8d82f48a8f Various GLES fixes. 2021-03-11 13:06:30 +01:00
Pieter-Jan Briers
469f9fd219 Remove #line directives from shaders.
They hurt debugging more than they helped.
2021-03-11 13:06:18 +01:00
Pieter-Jan Briers
1a5783ab4e Probably fix tests 2021-03-11 11:47:58 +01:00
Clyybber
3d25886d79 Set velocity for audio sources, enabling doppler effect (#1622) 2021-03-11 11:44:01 +01:00
Pieter-Jan Briers
516b2cd372 Handle surrogate pairs correctly in LineEdit. 2021-03-10 16:55:12 +01:00
Pieter-Jan Briers
3cfcfa0be2 Render fallback character for unavailable characters. 2021-03-10 16:54:52 +01:00
Pieter-Jan Briers
69328087bd Added AsRune property to TextEventArgs 2021-03-10 16:54:13 +01:00
Pieter-Jan Briers
1bf8b2a52b Use Rune for rendering text instead of char.
Fixes crashes with surrogates.
2021-03-09 23:25:27 +01:00
Pieter-Jan Briers
fc6dc6f4e1 Add/fix Rune APIs for sandbox. 2021-03-09 23:24:33 +01:00
Pieter-Jan Briers
31c1feca4e Debug console history improvements.
No longer blows up if history cannot be read/written thanks to file locking.

Made it more async so it won't waste main thread init time.
2021-03-09 22:28:58 +01:00
Pieter-Jan Briers
3ed1eef2ab Fix build. 2021-03-09 21:44:46 +01:00
Pieter-Jan Briers
1394a017bb Fix IL verification throwing if a verifier error does not need to be formatted. 2021-03-09 21:41:50 +01:00
metalgearsloth
6b0670d5f1 Break joints on container insertion; semi-related to break pulling on container insertion (#1620)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:57 -08:00
metalgearsloth
f573331541 Fix physics joint disconnect spam (#1619)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:35 -08:00
metalgearsloth
a7218cd3b8 Make Visible a Shared Property for sprites (#1615)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:16 -08:00
Acruid
f7e8178736 Added new ComponentEvents system in IEventBus. (#1601)
* Added new ComponentEventBus, combined it with IEventBus.

* Removed all traces of IEntity from ComponentDependencies.
Removed IEntityManager dependency from ComponentManager.

* Added entity create/delete events to IEntityManager.

* ComponentEvents now use EntitySystemMessages instead of their custom ComponentEvent class.

* Component events are now just overloads of entity events.

* Removed obsolete EntitySystemMessage, now everything uses the base EntityEventArgs.

* Add a bool argument for if the message should be broadcast as well as directed.
Fix ordering and init issues of events in EntityManager.

* Changed names from Component/Entity events to Directed/Broadcast.

* Fix bugs and unit tests.
2021-03-09 11:02:24 -08:00
Pieter-Jan Briers
31f921e4aa Use ProfileOptimization to speed up startup. 2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
aa1c25637c Allow disabling nvidia optimus via env var. 2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
71f2c48463 Call GC.Collect after game init.
Cleans up any gen 2 garbage from init and the stutter shouldn't be the end of the world.
2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
d65f4ca898 RSI & texture preloading.
All RSIs and textures are now loaded ahead of time in client startup. This is well threaded and is extremely fast.
2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
b35568ffe5 Disable path case checks by default.
The idea was that these are Task.Run'd so don't influence performance. That was before we started threading the hell out of startup.

We're getting more stuff like YAML linting now which should hopefully be able to catch 99% of this. And louder because it was always just a warning before.
2021-03-09 12:29:59 +01:00
Acruid
a0d241e551 Removes some things that should not have been in the last PR. 2021-03-09 02:06:13 -08:00
GraniteSidewalk
33a6934582 Large number of changes to Shaders and Overlays (#1387)
* AAAAAAAAAAAAA

* Organization

* Still doesnt work

* Formatting

* It works!!

* More changes to everything

* Beginning of changes to overlays

* Makes the overlay manager GUID based (also it was very messy, still messy but i fixed some of it)

* Stencils are easy

* Questionable changes to overlays

* Minor change to HLR

* Fixed duplicate overlays when calling some commands (Like showbb)

* Fixes misleading message

* Adds a variety of worldspaces for overlays to choose from

* Caching

* Address reviews

* Merging pains

* ah.

* ahhhhh

* minor overlaymanager changes

* Work

* fix

* Merge??

* Fixes null errors

* Force update

* Delete whatever the fuck this is?

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-03-09 01:52:16 -08:00
metalgearsloth
f237a8bbbc Optimise static body sleeping (#1618)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 19:47:51 +11:00
Pieter-Jan Briers
4bc775c01c RSI loader improvements:
1. Stop using NJsonSchema, it didn't do anything useful.
2. Use System.Text.Json instead of Newtonsoft.Json.
3. General cleanup of the code, using arrays instead of lists, etc...
2021-03-08 11:18:19 +01:00
Pieter-Jan Briers
93b4d81505 Optimize ImageSharp blitting. 2021-03-08 11:15:33 +01:00
Pieter-Jan Briers
0afb85a09e Fix some missing re-pooling of ImageSharp images. 2021-03-08 09:45:22 +01:00
Metal Gear Sloth
7b9315cea4 Significantly lower physics speedcap 2021-03-08 15:46:50 +11:00
Metal Gear Sloth
dc3af45096 Fix anchored message 2021-03-08 15:00:08 +11:00
metalgearsloth
00ce0179ae Allow kinematic controllers to have an impulse applied (#1612)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-08 12:10:20 +11:00
ShadowCommander
81947ba3d8 Fix buckling (#1611) 2021-03-07 15:25:11 -08:00
DrSmugleaf
49327279d0 Fix nullability errors in physics ContactHead code (#1609) 2021-03-07 23:15:55 +01:00
320 changed files with 8642 additions and 4483 deletions

View File

@@ -28,5 +28,10 @@ void fragment()
highp float occlusion = ChebyshevUpperBound(occlDist, ourDist);
if (occlusion >= 1.0)
{
discard;
}
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - occlusion);
}

View File

@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;
namespace Robust.Benchmarks
{
internal class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Benchmarks</OutputPath>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
</Project>

View File

@@ -0,0 +1,11 @@
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Benchmarks.Serialization
{
[DataDefinition]
public class DataDefinitionWithString
{
[field: DataField("string")]
private string StringField { get; } = default!;
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using BenchmarkDotNet.Attributes;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using YamlDotNet.RepresentationModel;
namespace Robust.Benchmarks.Serialization.Read
{
public class SerializationReadBenchmark
{
public SerializationReadBenchmark()
{
IoCManager.InitThread();
ServerIoC.RegisterIoC();
IoCManager.BuildGraph();
var assemblies = new[]
{
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Shared"),
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Server"),
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Benchmarks")
};
foreach (var assembly in assemblies)
{
IoCManager.Resolve<IConfigurationManagerInternal>().LoadCVarsFromAssembly(assembly);
}
IoCManager.Resolve<IReflectionManager>().LoadAssemblies(assemblies);
SerializationManager = IoCManager.Resolve<ISerializationManager>();
SerializationManager.Initialize();
StringDataDefNode = new MappingDataNode();
StringDataDefNode.AddNode(new ValueDataNode("string"), new ValueDataNode("ABC"));
var yamlStream = new YamlStream();
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
SeedNode = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
}
private ISerializationManager SerializationManager { get; }
private ValueDataNode StringNode { get; } = new("ABC");
private ValueDataNode IntNode { get; } = new("1");
private MappingDataNode StringDataDefNode { get; }
private MappingDataNode SeedNode { get; }
[Benchmark]
public string? ReadString()
{
return SerializationManager.ReadValue<string>(StringNode);
}
[Benchmark]
public int? ReadInteger()
{
return SerializationManager.ReadValue<int>(IntNode);
}
[Benchmark]
public DataDefinitionWithString? ReadDataDefinitionWithString()
{
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
}
[Benchmark]
public SeedDataDefinition? ReadSeedDataDefinition()
{
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
}
}
}

View File

@@ -0,0 +1,184 @@
using System.Collections.Generic;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Benchmarks.Serialization
{
[Prototype("seed")]
public class SeedDataDefinition : IPrototype
{
public const string Prototype = @"
- type: seed
id: tobacco
name: tobacco
seedName: tobacco
displayName: tobacco plant
productPrototypes:
- LeavesTobacco
harvestRepeat: Repeat
lifespan: 75
maturation: 5
production: 5
yield: 2
potency: 20
growthStages: 3
idealLight: 9
idealHeat: 298
chemicals:
chem.Nicotine:
Min: 1
Max: 10
PotencyDivisor: 10";
private const string SeedPrototype = "SeedBase";
[ViewVariables]
[field: DataField("id", required: true)]
public string ID { get; private init; } = default!;
/// <summary>
/// Unique identifier of this seed. Do NOT set this.
/// </summary>
public int Uid { get; internal set; } = -1;
#region Tracking
[ViewVariables] [DataField("name")] public string Name { get; set; } = string.Empty;
[ViewVariables] [DataField("seedName")] public string SeedName { get; set; } = string.Empty;
[ViewVariables]
[DataField("seedNoun")]
public string SeedNoun { get; set; } = "seeds";
[ViewVariables] [DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
[ViewVariables]
[DataField("roundStart")]
public bool RoundStart { get; private set; } = true;
[ViewVariables] [DataField("mysterious")] public bool Mysterious { get; set; }
[ViewVariables] [DataField("immutable")] public bool Immutable { get; set; }
#endregion
#region Output
[ViewVariables]
[DataField("productPrototypes")]
public List<string> ProductPrototypes { get; set; } = new();
[ViewVariables]
[DataField("chemicals")]
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
[ViewVariables]
[DataField("consumeGasses")]
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
[ViewVariables]
[DataField("exudeGasses")]
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
#endregion
#region Tolerances
[ViewVariables]
[DataField("nutrientConsumption")]
public float NutrientConsumption { get; set; } = 0.25f;
[ViewVariables] [DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
[ViewVariables] [DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
[ViewVariables] [DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
[ViewVariables] [DataField("idealLight")] public float IdealLight { get; set; } = 7f;
[ViewVariables] [DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
[ViewVariables] [DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
[ViewVariables]
[DataField("lowPressureTolerance")]
public float LowPressureTolerance { get; set; } = 25f;
[ViewVariables]
[DataField("highPressureTolerance")]
public float HighPressureTolerance { get; set; } = 200f;
[ViewVariables]
[DataField("pestTolerance")]
public float PestTolerance { get; set; } = 5f;
[ViewVariables]
[DataField("weedTolerance")]
public float WeedTolerance { get; set; } = 5f;
#endregion
#region General traits
[ViewVariables]
[DataField("endurance")]
public float Endurance { get; set; } = 100f;
[ViewVariables] [DataField("yield")] public int Yield { get; set; }
[ViewVariables] [DataField("lifespan")] public float Lifespan { get; set; }
[ViewVariables] [DataField("maturation")] public float Maturation { get; set; }
[ViewVariables] [DataField("production")] public float Production { get; set; }
[ViewVariables] [DataField("growthStages")] public int GrowthStages { get; set; } = 6;
[ViewVariables] [DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
[ViewVariables] [DataField("potency")] public float Potency { get; set; } = 1f;
// No, I'm not removing these.
//public PlantSpread Spread { get; set; }
//public PlantMutation Mutation { get; set; }
//public float AlterTemperature { get; set; }
//public PlantCarnivorous Carnivorous { get; set; }
//public bool Parasite { get; set; }
//public bool Hematophage { get; set; }
//public bool Thorny { get; set; }
//public bool Stinging { get; set; }
[DataField("ligneous")]
public bool Ligneous { get; set; }
// public bool Teleporting { get; set; }
// public PlantJuicy Juicy { get; set; }
#endregion
#region Cosmetics
[ViewVariables]
[DataField("plantRsi", required: true)]
public ResourcePath PlantRsi { get; set; } = default!;
[ViewVariables]
[DataField("plantIconState")]
public string PlantIconState { get; set; } = "produce";
[ViewVariables]
[DataField("bioluminescent")]
public bool Bioluminescent { get; set; }
[ViewVariables]
[DataField("bioluminescentColor")]
public Color BioluminescentColor { get; set; } = Color.White;
[ViewVariables]
[DataField("splatPrototype")]
public string? SplatPrototype { get; set; }
#endregion
}
public enum HarvestType
{
NoRepeat,
Repeat
}
public enum Gas {}
[DataDefinition]
public struct SeedChemQuantity
{
[DataField("Min")]
public int Min;
[DataField("Max")]
public int Max;
[DataField("PotencyDivisor")]
public int PotencyDivisor;
}
}

View File

@@ -11,7 +11,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Utility;
@@ -321,6 +320,11 @@ namespace Robust.Client.Audio.Midi
continue;
}
if (renderer.TrackingEntity != null)
{
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
}
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
{
// just duck out instead of move to NaN

View File

@@ -8,6 +8,7 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -193,6 +194,7 @@ namespace Robust.Client
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_mapManager.Shutdown();
_discord.ClearPresence();

View File

@@ -27,8 +27,6 @@ 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
{
@@ -40,6 +38,7 @@ namespace Robust.Client
IoCManager.Register<IPrototypeManager, ClientPrototypeManager>();
IoCManager.Register<IEntityManager, ClientEntityManager>();
IoCManager.Register<IEntityLookup, SharedEntityLookup>();
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
IoCManager.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
@@ -66,8 +65,6 @@ namespace Robust.Client
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IMidiManager, MidiManager>();
IoCManager.Register<IAuthManager, AuthManager>();
switch (mode)
@@ -94,8 +91,9 @@ namespace Robust.Client
throw new ArgumentOutOfRangeException();
}
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IEyeManager, EyeManager>();
IoCManager.Register<IPlacementManager, PlacementManager>();
IoCManager.Register<IOverlayManager, OverlayManager>();
IoCManager.Register<IOverlayManagerInternal, OverlayManager>();

View File

@@ -0,0 +1,44 @@
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
public sealed class LsMonitorCommand : IConsoleCommand
{
public string Command => "lsmonitor";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
foreach (var monitor in clyde.EnumerateMonitors())
{
shell.WriteLine(
$"[{monitor.Id}] {monitor.Name}: {monitor.Size.X}x{monitor.Size.Y}@{monitor.RefreshRate}Hz");
}
}
}
[UsedImplicitly]
public sealed class SetMonitorCommand : IConsoleCommand
{
public string Command => "setmonitor";
public string Description => "";
public string Help => "Usage: setmonitor <id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var id = int.Parse(args[0]);
var monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
clyde.SetWindowMonitor(monitor);
}
}
}

View File

@@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
@@ -41,14 +42,14 @@ namespace Robust.Client.Debugging
_debugColliders = value;
if (value)
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
{
_overlayManager.AddOverlay(new PhysicsOverlay(_componentManager, _eyeManager,
_prototypeManager, _inputManager, _physicsManager));
}
else
{
_overlayManager.RemoveOverlay(nameof(PhysicsOverlay));
_overlayManager.RemoveOverlay<PhysicsOverlay>();
}
}
}
@@ -66,13 +67,13 @@ namespace Robust.Client.Debugging
_debugPositions = value;
if (value)
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_entityManager, _eyeManager));
}
else
{
_overlayManager.RemoveOverlay(nameof(EntityPositionOverlay));
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
@@ -91,8 +92,8 @@ namespace Robust.Client.Debugging
private Vector2 _hoverStartScreen = Vector2.Zero;
private List<IPhysBody> _hoverBodies = new();
public PhysicsOverlay(IComponentManager compMan, IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IPhysicsManager physicsManager)
: base(nameof(PhysicsOverlay))
{
_componentManager = compMan;
_eyeManager = eyeMan;
@@ -160,6 +161,8 @@ namespace Robust.Client.Debugging
if (viewport.IsEmpty()) return;
var mapId = _eyeManager.CurrentMap;
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
var colorEdge = Color.Red.WithAlpha(0.33f);
foreach (var physBody in EntitySystem.Get<SharedBroadPhaseSystem>().GetCollidingEntities(mapId, viewport))
{
@@ -169,9 +172,6 @@ namespace Robust.Client.Debugging
var worldBox = physBody.GetWorldAABB();
if (worldBox.IsEmpty()) continue;
var colorEdge = Color.Red.WithAlpha(0.33f);
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
foreach (var fixture in physBody.Fixtures)
{
var shape = fixture.Shape;
@@ -193,9 +193,9 @@ namespace Robust.Client.Debugging
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
@@ -265,7 +265,7 @@ namespace Robust.Client.Debugging
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager) : base(nameof(EntityPositionOverlay))
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager)
{
_entityManager = entityManager;
_eyeManager = eyeManager;

View File

@@ -1,10 +1,11 @@
using Robust.Shared.IoC;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Enums;
using Robust.Shared.Network;
namespace Robust.Client.Debugging
@@ -38,13 +39,13 @@ namespace Robust.Client.Debugging
_debugDrawRays = value;
if (value)
if (value && !_overlayManager.HasOverlay<DebugDrawRayOverlay>())
{
_overlayManager.AddOverlay(new DebugDrawRayOverlay(this));
}
else
{
_overlayManager.RemoveOverlay(nameof(DebugDrawRayOverlay));
_overlayManager.RemoveOverlay<DebugDrawRayOverlay>();
}
}
}
@@ -81,7 +82,7 @@ namespace Robust.Client.Debugging
private readonly DebugDrawingManager _owner;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugDrawRayOverlay(DebugDrawingManager owner) : base(nameof(DebugDrawRayOverlay))
public DebugDrawRayOverlay(DebugDrawingManager owner)
{
_owner = owner;
}

View File

@@ -24,6 +24,7 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -55,7 +56,7 @@ namespace Robust.Client.Debugging
IoCManager.Resolve<IOverlayManager>().AddOverlay(new PhysicsDebugOverlay(this));
if (value == PhysicsDebugFlags.None)
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(nameof(PhysicsDebugOverlay));
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsDebugOverlay));
_flags = value;
}
@@ -119,7 +120,7 @@ namespace Robust.Client.Debugging
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public PhysicsDebugOverlay(DebugPhysicsSystem system) : base(nameof(PhysicsDebugOverlay))
public PhysicsDebugOverlay(DebugPhysicsSystem system)
{
_physics = system;
}

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.IO;
using System.Management;
using System.Net;
using System.Threading.Tasks;
using Robust.Client.Audio.Midi;
@@ -80,6 +81,75 @@ namespace Robust.Client
}
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
{
if (!StartupSystemSplash(logHandlerFactory))
return false;
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
_modLoader.SetEnableSandboxing(true);
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
{
Logger.Fatal("Errors while loading content assemblies.");
return false;
}
foreach (var loadedModule in _modLoader.LoadedModules)
{
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
IoCManager.Resolve<ISerializationManager>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_resourceCache.PreloadTextures();
_userInterfaceManager.Initialize();
_networkManager.Initialize(false);
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
_prototypeManager.Resync();
_mapManager.Initialize();
_entityManager.Initialize();
IoCManager.Resolve<IEntityLookup>().Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
if (_commandLineArgs?.Username != null)
{
_client.PlayerNameOverride = _commandLineArgs.Username;
}
_authManager.LoadFromEnv();
GC.Collect();
_clyde.Ready();
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
return true;
}
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
{
ReadInitialLaunchState();
@@ -119,6 +189,8 @@ namespace Robust.Client
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
}
ProfileOptSetup.Setup(_configurationManager);
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
@@ -129,6 +201,13 @@ namespace Robust.Client
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_clyde.TextEntered += TextEntered;
_clyde.MouseMove += MouseMove;
_clyde.KeyUp += KeyUp;
_clyde.KeyDown += KeyDown;
_clyde.MouseWheel += MouseWheel;
_clyde.CloseWindow += Shutdown;
// Bring display up as soon as resources are mounted.
if (!_clyde.Initialize())
{
@@ -137,65 +216,7 @@ namespace Robust.Client
_clyde.SetWindowTitle("Space Station 14");
_fontManager.Initialize();
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
_modLoader.SetEnableSandboxing(true);
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
{
Logger.Fatal("Errors while loading content assemblies.");
return false;
}
foreach (var loadedModule in _modLoader.LoadedModules)
{
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
IoCManager.Resolve<ISerializationManager>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_userInterfaceManager.Initialize();
_networkManager.Initialize(false);
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
_prototypeManager.Resync();
_mapManager.Initialize();
_entityManager.Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
if (_commandLineArgs?.Username != null)
{
_client.PlayerNameOverride = _commandLineArgs.Username;
}
_authManager.LoadFromEnv();
_clyde.Ready();
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
return true;
}
@@ -272,17 +293,13 @@ namespace Robust.Client
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
_userInterfaceManager.Update(frameEventArgs);
// GameStateManager is in full control of the simulation update.
if (_client.RunLevel >= ClientRunLevel.Connected)
{
_componentManager.CullRemovedComponents();
_gameStateManager.ApplyGameState();
_entityManager.Update(frameEventArgs.DeltaSeconds);
_playerManager.Update(frameEventArgs.DeltaSeconds);
}
_stateManager.Update(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
}
@@ -384,6 +401,7 @@ namespace Robust.Client
{
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
}

View File

@@ -25,14 +25,13 @@ namespace Robust.Client.GameObjects
public override void Startup()
{
base.Startup();
if (Started)
{
throw new InvalidOperationException("Startup() called multiple times");
}
EntitySystemManager.Initialize();
base.Startup();
Started = true;
}
@@ -96,11 +95,6 @@ namespace Robust.Client.GameObjects
kvStates.Value.Item2);
}
foreach (var kvp in toApply)
{
UpdateEntityTree(kvp.Key);
}
foreach (var id in deletions)
{
DeleteEntity(id);
@@ -130,7 +124,7 @@ namespace Robust.Client.GameObjects
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if(brokenEnts.Contains(entity))
if (brokenEnts.Contains(entity))
continue;
try
@@ -150,10 +144,9 @@ namespace Robust.Client.GameObjects
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if(brokenEnts.Contains(entity))
if (brokenEnts.Contains(entity))
continue;
#endif
UpdateEntityTree(entity);
}
#if EXCEPTION_TOLERANCE
foreach (var entity in brokenEnts)
@@ -199,7 +192,6 @@ namespace Robust.Client.GameObjects
{
var newEnt = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) newEnt);
UpdateEntityTree(newEnt);
return newEnt;
}
@@ -208,16 +200,9 @@ namespace Robust.Client.GameObjects
{
var entity = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) entity);
UpdateEntityTree(entity);
return entity;
}
/// <inheritdoc />
public override IEntity SpawnEntityNoMapInit(string? protoName, EntityCoordinates coordinates)
{
return SpawnEntity(protoName, coordinates);
}
protected override EntityUid GenerateEntityUid()
{
return new(_nextClientEntityUid++);

View File

@@ -34,7 +34,7 @@ namespace Robust.Client.GameObjects
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
}
public void Update()
public void TickUpdate()
{
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
{
@@ -45,12 +45,12 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntitySystemMessage message)
public void SendSystemNetworkMessage(EntityEventArgs message)
{
SendSystemNetworkMessage(message, default(uint));
}
public void SendSystemNetworkMessage(EntitySystemMessage message, uint sequence)
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.SystemMessage;
@@ -62,7 +62,7 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntitySystemMessage message, INetChannel channel)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
{
throw new NotSupportedException();
}

View File

@@ -1,4 +1,4 @@
using Robust.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -152,6 +152,7 @@ namespace Robust.Client.GameObjects
Zoom = state.Zoom;
Offset = state.Offset;
Rotation = state.Rotation;
VisibilityMask = state.VisibilityMask;
}
public override void OnRemove()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -19,6 +19,7 @@ using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -36,13 +37,13 @@ namespace Robust.Client.GameObjects
private bool _visible = true;
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible
public override bool Visible
{
get => _visible;
set => _visible = value;
}
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
[DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
private int drawDepth = DrawDepthTag.Default;
/// <summary>
@@ -847,8 +848,8 @@ namespace Robust.Client.GameObjects
}
else
{
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId,
Environment.StackTrace);
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI {1}. Trace:\n{2}", stateId,
actualRsi.Path, Environment.StackTrace);
theLayer.Texture = null;
}
}
@@ -2084,6 +2085,7 @@ namespace Robust.Client.GameObjects
public IEntityManager EntityManager { get; } = null!;
public string Name { get; set; } = string.Empty;
public EntityUid Uid { get; } = EntityUid.Invalid;
EntityLifeStage IEntity.LifeStage { get => _lifeStage; set => _lifeStage = value; }
public bool Initialized { get; } = false;
public bool Initializing { get; } = false;
public bool Deleted { get; } = true;
@@ -2100,6 +2102,7 @@ namespace Robust.Client.GameObjects
public IMetaDataComponent MetaData { get; } = null!;
private Dictionary<Type, IComponent> _components = new();
private EntityLifeStage _lifeStage;
public T AddComponent<T>() where T : Component, new()
{
@@ -2147,11 +2150,6 @@ namespace Robust.Client.GameObjects
return null!;
}
public IComponent GetComponent(uint netID)
{
return null!;
}
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
{
component = null;
@@ -2178,21 +2176,6 @@ namespace Robust.Client.GameObjects
return null;
}
public bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component)
{
component = null;
return false;
}
public IComponent? GetComponentOrNull(uint netId)
{
return null;
}
public void Shutdown()
{
}
public void Delete()
{
}

View File

@@ -9,7 +9,7 @@ namespace Robust.Client.GameObjects
public static class EntityManagerExt
{
public static void RaisePredictiveEvent<T>(this IEntityManager entityManager, T msg)
where T : EntitySystemMessage
where T : EntityEventArgs
{
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;
DebugTools.AssertNotNull(localPlayer);

View File

@@ -8,7 +8,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Player;
@@ -165,6 +164,12 @@ namespace Robust.Client.GameObjects
Logger.Warning("Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
}
if (stream.TrackingEntity != null)
{
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
}
}
}
}
@@ -188,7 +193,7 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -204,7 +209,7 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
private IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
ApplyAudioParams(audioParams, source);
@@ -226,7 +231,7 @@ namespace Robust.Client.GameObjects
/// <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>
public IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -243,7 +248,7 @@ namespace Robust.Client.GameObjects
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
if (!source.SetPosition(entity.Transform.WorldPosition))
@@ -272,7 +277,7 @@ namespace Robust.Client.GameObjects
/// <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>
public IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -289,7 +294,7 @@ namespace Robust.Client.GameObjects
/// <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>
public IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);

View File

@@ -106,7 +106,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Event raised by a <see cref="ClientOccluderComponent"/> when it needs to be recalculated.
/// </summary>
internal sealed class OccluderDirtyEvent : EntitySystemMessage
internal sealed class OccluderDirtyEvent : EntityEventArgs
{
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition, SnapGridOffset offset)
{

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Enums;
namespace Robust.Client.GameObjects
{
@@ -42,7 +43,7 @@ namespace Robust.Client.GameObjects
{
base.Shutdown();
overlayManager.RemoveOverlay("EffectSystem");
overlayManager.RemoveOverlay(typeof(EffectOverlay));
}
public void CreateEffect(EffectSystemMessage message)
@@ -329,7 +330,6 @@ namespace Robust.Client.GameObjects
{
private readonly IPlayerManager _playerManager;
public override bool AlwaysDirty => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _unshadedShader;
@@ -337,8 +337,7 @@ namespace Robust.Client.GameObjects
private readonly IMapManager _mapManager;
private readonly IEntityManager _entityManager;
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager) : base(
"EffectSystem")
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager)
{
_owner = owner;
_unshadedShader = protoMan.Index<ShaderPrototype>("unshaded").Instance();

View File

@@ -157,7 +157,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public class PlayerAttachSysMessage : EntitySystemMessage
public class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.

View File

@@ -19,19 +19,19 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
private readonly Dictionary<MapId, MapTrees> _mapTrees = new();
private readonly Dictionary<MapId, Dictionary<GridId, MapTrees>> _gridTrees = new();
private readonly List<SpriteComponent> _spriteQueue = new();
private readonly List<PointLightComponent> _lightQueue = new();
internal DynamicTree<SpriteComponent> GetSpriteTreeForMap(MapId map)
internal DynamicTree<SpriteComponent> GetSpriteTreeForMap(MapId map, GridId grid)
{
return _mapTrees[map].SpriteTree;
return _gridTrees[map][grid].SpriteTree;
}
internal DynamicTree<PointLightComponent> GetLightTreeForMap(MapId map)
internal DynamicTree<PointLightComponent> GetLightTreeForMap(MapId map, GridId grid)
{
return _mapTrees[map].LightTree;
return _gridTrees[map][grid].LightTree;
}
public override void Initialize()
@@ -44,6 +44,8 @@ namespace Robust.Client.GameObjects
_mapManager.MapCreated += MapManagerOnMapCreated;
_mapManager.MapDestroyed += MapManagerOnMapDestroyed;
_mapManager.OnGridCreated += MapManagerOnGridCreated;
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
SubscribeLocalEvent<EntMapIdChangedMessage>(EntMapIdChanged);
SubscribeLocalEvent<MoveEvent>(EntMoved);
@@ -53,18 +55,40 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<RenderTreeRemoveLightMessage>(RemoveLight);
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.MapCreated -= MapManagerOnMapCreated;
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
UnsubscribeLocalEvent<EntMapIdChangedMessage>();
UnsubscribeLocalEvent<MoveEvent>();
UnsubscribeLocalEvent<EntParentChangedMessage>();
UnsubscribeLocalEvent<PointLightRadiusChangedMessage>();
UnsubscribeLocalEvent<RenderTreeRemoveSpriteMessage>();
UnsubscribeLocalEvent<RenderTreeRemoveLightMessage>();
}
// For these next 2 methods (the Remove* ones):
// If the Transform is removed BEFORE the Sprite/Light,
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
// Otherwise these will still have their past MapId and that's all we need..
private void RemoveLight(RenderTreeRemoveLightMessage ev)
{
_mapTrees[ev.Map].LightTree.Remove(ev.Light);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.LightAabbFunc(ev.Light), true))
{
_gridTrees[ev.Map][gridId].LightTree.Remove(ev.Light);
}
}
private void RemoveSprite(RenderTreeRemoveSpriteMessage ev)
{
_mapTrees[ev.Map].SpriteTree.Remove(ev.Sprite);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.SpriteAabbFunc(ev.Sprite), true))
{
_gridTrees[ev.Map][gridId].SpriteTree.Remove(ev.Sprite);
}
}
private void PointLightRadiusChanged(PointLightRadiusChangedMessage ev)
@@ -119,27 +143,69 @@ namespace Robust.Client.GameObjects
{
// Nullspace is a valid map ID for stuff to have but we also aren't gonna bother indexing it.
// So that's why there's a GetValueOrDefault.
var oldMapTrees = _mapTrees.GetValueOrDefault(ev.OldMapId);
var newMapTrees = _mapTrees.GetValueOrDefault(ev.Entity.Transform.MapID);
var oldMapTrees = _gridTrees.GetValueOrDefault(ev.OldMapId);
// TODO: MMMM probably a better way to do this.
if (ev.Entity.TryGetComponent(out SpriteComponent? sprite))
{
oldMapTrees?.SpriteTree.Remove(sprite);
if (oldMapTrees != null)
{
foreach (var (_, gridTree) in oldMapTrees)
{
gridTree.SpriteTree.Remove(sprite);
}
}
newMapTrees?.SpriteTree.AddOrUpdate(sprite);
var bounds = MapTrees.SpriteAabbFunc(sprite);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
{
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
gridBounds = bounds;
}
else
{
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
_gridTrees[ev.Entity.Transform.MapID][gridId].SpriteTree.AddOrUpdate(sprite, gridBounds);
}
}
if (ev.Entity.TryGetComponent(out PointLightComponent? light))
{
oldMapTrees?.LightTree.Remove(light);
if (oldMapTrees != null)
{
foreach (var (_, gridTree) in oldMapTrees)
{
gridTree.LightTree.Remove(light);
}
}
newMapTrees?.LightTree.AddOrUpdate(light);
var bounds = MapTrees.LightAabbFunc(light);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
{
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
gridBounds = bounds;
}
else
{
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
_gridTrees[ev.Entity.Transform.MapID][gridId].LightTree.AddOrUpdate(light, gridBounds);
}
}
}
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
{
_mapTrees.Remove(e.Map);
_gridTrees.Remove(e.Map);
}
private void MapManagerOnMapCreated(object? sender, MapEventArgs e)
@@ -149,36 +215,59 @@ namespace Robust.Client.GameObjects
return;
}
_mapTrees.Add(e.Map, new MapTrees());
_gridTrees.Add(e.Map, new Dictionary<GridId, MapTrees>
{
{GridId.Invalid, new MapTrees()}
});
}
private void MapManagerOnGridCreated(MapId mapId, GridId gridId)
{
_gridTrees[mapId].Add(gridId, new MapTrees());
}
private void MapManagerOnGridRemoved(MapId mapId, GridId gridId)
{
_gridTrees[mapId].Remove(gridId);
}
public override void FrameUpdate(float frameTime)
{
foreach (var queuedUpdateSprite in _spriteQueue)
{
var transform = queuedUpdateSprite.Owner.Transform;
var map = transform.MapID;
var map = queuedUpdateSprite.Owner.Transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].SpriteTree;
updateMapTree.AddOrUpdate(queuedUpdateSprite);
var mapTree = _gridTrees[map];
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
MapTrees.SpriteAabbFunc(queuedUpdateSprite), true))
{
mapTree[gridId].SpriteTree.AddOrUpdate(queuedUpdateSprite);
}
queuedUpdateSprite.TreeUpdateQueued = false;
}
foreach (var queuedUpdateLight in _lightQueue)
{
var transform = queuedUpdateLight.Owner.Transform;
var map = transform.MapID;
var map = queuedUpdateLight.Owner.Transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].LightTree;
updateMapTree.AddOrUpdate(queuedUpdateLight);
var mapTree = _gridTrees[map];
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
MapTrees.LightAabbFunc(queuedUpdateLight), true))
{
mapTree[gridId].LightTree.AddOrUpdate(queuedUpdateLight);
}
queuedUpdateLight.TreeUpdateQueued = false;
}
@@ -197,14 +286,14 @@ namespace Robust.Client.GameObjects
LightTree = new DynamicTree<PointLightComponent>(LightAabbFunc);
}
private static Box2 SpriteAabbFunc(in SpriteComponent value)
internal static Box2 SpriteAabbFunc(in SpriteComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
return new Box2(worldPos, worldPos);
}
private static Box2 LightAabbFunc(in PointLightComponent value)
internal static Box2 LightAabbFunc(in PointLightComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;

View File

@@ -3,6 +3,7 @@ using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
@@ -13,6 +14,7 @@ namespace Robust.Client.GameObjects
public class SpriteSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
@@ -29,18 +31,32 @@ namespace Robust.Client.GameObjects
return;
}
var mapTree = renderTreeSystem.GetSpriteTreeForMap(currentMap);
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(currentMap, pvsBounds, true))
{
if (value.IsInert)
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
return true;
gridBounds = pvsBounds;
}
else
{
gridBounds = pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
value.FrameUpdate(state);
return true;
}, pvsBounds, approx: true);
var mapTree = renderTreeSystem.GetSpriteTreeForMap(currentMap, gridId);
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
{
if (value.IsInert)
{
return true;
}
value.FrameUpdate(state);
return true;
}, gridBounds, approx: true);
}
}
}
}

View File

@@ -27,11 +27,12 @@ namespace Robust.Client.GameStates
private uint _nextInputCmdSeq = 1;
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
private readonly Queue<(uint sequence, GameTick sourceTick, EntitySystemMessage msg, object sessionMsg)>
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
_pendingSystemMessages
= new();
[Dependency] private readonly IClientEntityManager _entities = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
@@ -126,7 +127,7 @@ namespace Robust.Client.GameStates
_nextInputCmdSeq++;
}
public uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage
public uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs
{
if (!Predicting)
{
@@ -242,65 +243,69 @@ namespace Robust.Client.GameStates
if (!Predicting) return;
using var _ = _timing.StartPastPredictionArea();
if (_pendingInputs.Count > 0)
using(var _ = _timing.StartPastPredictionArea())
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
if (_pendingInputs.Count > 0)
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.Update if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.Update((float) _timing.TickPeriod.TotalSeconds);
((IEntityEventBus) _entities.EventBus).ProcessEventQueue();
}
}
_lookup.Update();
}
private void ResetPredictedEntities(GameTick curTick)

View File

@@ -70,6 +70,6 @@ namespace Robust.Client.GameStates
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage;
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
}
}

View File

@@ -1,13 +1,16 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -27,13 +30,13 @@ namespace Robust.Client.GameStates
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
private readonly Font _font;
private readonly int _lineHeight;
private readonly List<NetEntity> _netEnts = new();
public NetEntityOverlay() : base(nameof(NetEntityOverlay))
public NetEntityOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
@@ -42,12 +45,12 @@ namespace Robust.Client.GameStates
_gameStateManager.GameStateApplied += HandleGameStateApplied;
}
private void HandleGameStateApplied(GameStateAppliedArgs args)
{
if(_gameTiming.InPrediction) // we only care about real server states.
return;
// Shift traffic history down one
for (var i = 0; i < _netEnts.Count; i++)
{
@@ -74,7 +77,7 @@ namespace Robust.Client.GameStates
if (netEnt.Id != entityState.Uid)
continue;
//TODO: calculate size of state and record it here.
netEnt.Traffic[^1] = 1;
netEnt.LastUpdate = gameState.ToSequence;
@@ -94,15 +97,15 @@ namespace Robust.Client.GameStates
}
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize*2, pvsSize*2));
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
int timeout = _gameTiming.TickRate * 3;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if(_entityManager.EntityExists(netEnt.Id))
{
//TODO: Whoever is working on PVS remake, change the InPVS detection.
@@ -123,22 +126,58 @@ namespace Robust.Client.GameStates
_netEnts[i] = netEnt; // copy struct back
}
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
{
if (!_netManager.IsConnected)
return;
switch (currentSpace)
{
case OverlaySpace.ScreenSpace:
DrawScreen(handle);
break;
case OverlaySpace.WorldSpace:
DrawWorld(handle);
break;
}
}
private void DrawWorld(DrawingHandleBase handle)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
var worldHandle = (DrawingHandleWorld)handle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(DrawingHandleBase handle)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = (DrawingHandleScreen)handle;
var screenHandle = (DrawingHandleScreen) handle;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if (!_entityManager.TryGetEntity(netEnt.Id, out var ent))
{
_netEnts.RemoveSwap(i);
i--;
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetEntity(netEnt.Id).Prototype?.ID}";
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
var color = CalcTextColor(ref netEnt);
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
@@ -179,20 +218,19 @@ namespace Robust.Client.GameStates
return Color.Green; // Entity in PVS, but not updated recently.
}
protected override void Dispose(bool disposing)
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
base.Dispose(disposing);
base.DisposeBehavior();
}
private static void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str, Color textColor)
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, textColor);
var advance = font.DrawChar(handle, rune, baseLine, 1, textColor);
baseLine += new Vector2(advance, 0);
}
}
@@ -225,7 +263,7 @@ namespace Robust.Client.GameStates
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}
@@ -238,14 +276,14 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(nameof(NetEntityOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.AddOverlay(new NetEntityOverlay());
shell.WriteLine("Enabled network entity report overlay.");
}
else if(!bValue && overlayMan.HasOverlay(nameof(NetEntityOverlay)))
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.RemoveOverlay(nameof(NetEntityOverlay));
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
shell.WriteLine("Disabled network entity report overlay.");
}
}

View File

@@ -1,7 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
@@ -34,7 +38,11 @@ namespace Robust.Client.GameStates
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
public NetGraphOverlay() : base(nameof(NetGraphOverlay))
private int _totalHistoryPayload; // sum of all data point sizes in bytes
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
@@ -58,7 +66,73 @@ namespace Robust.Client.GameStates
// calc interp info
var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize;
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, interpBuff));
// not watching an ent
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
return;
string? entStateString = null;
string? entDelString = null;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates is not null)
{
var sb = new StringBuilder();
foreach (var entState in entStates)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges is not null)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges)
{
var del = compChange.Deleted ? 'D' : 'C';
sb.Append($"\n [{del}]{compChange.NetID}:{compChange.ComponentName}");
}
}
if (entState.ComponentStates is not null)
{
sb.Append($"\n States:");
foreach (var compState in entState.ComponentStates)
{
sb.Append($"\n {compState.NetID}:{compState.GetType().Name}");
}
}
}
}
entStateString = sb.ToString();
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes is not null)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
{
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
if (!string.IsNullOrWhiteSpace(entStateString))
fullString += entStateString;
if (!string.IsNullOrWhiteSpace(entDelString))
fullString += entDelString;
conShell.WriteLine(fullString + "\n");
}
}
/// <inheritdoc />
@@ -67,10 +141,16 @@ namespace Robust.Client.GameStates
base.FrameUpdate(args);
var over = _history.Count - HistorySize;
if (over > 0)
if (over <= 0)
return;
for (int i = 0; i < over; i++)
{
_history.RemoveRange(0, over);
var point = _history[i];
_totalHistoryPayload -= point.Payload;
}
_history.RemoveRange(0, over);
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
@@ -80,6 +160,7 @@ namespace Robust.Client.GameStates
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
// bottom payload line
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
@@ -99,6 +180,12 @@ namespace Robust.Client.GameStates
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
// Draw size if above average
if (drawSizeThreshold * 1.5 < state.Payload)
{
DrawString((DrawingHandleScreen) handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
}
// second tick marks
if (state.Tick.Value % _gameTiming.TickRate == 0)
{
@@ -123,6 +210,10 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f));
}
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
@@ -142,20 +233,20 @@ namespace Robust.Client.GameStates
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void Dispose(bool disposing)
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
base.Dispose(disposing);
base.DisposeBehavior();
}
private void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str)
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
@@ -183,17 +274,48 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(nameof(NetGraphOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.AddOverlay(new NetGraphOverlay());
shell.WriteLine("Enabled network overlay.");
}
else if(overlayMan.HasOverlay(nameof(NetGraphOverlay)))
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.RemoveOverlay(nameof(NetGraphOverlay));
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
shell.WriteLine("Disabled network overlay.");
}
}
}
private class NetWatchEntCommand : IConsoleCommand
{
public string Command => "net_watchent";
public string Help => "net_watchent <0|EntityUid>";
public string Description => "Dumps all network updates for an EntityId to the console.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
}
if (!EntityUid.TryParse(args[0], out var eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
netOverlay.WatchEntId = eValue;
}
}
}
}
}

View File

@@ -1,3 +1,4 @@
using Robust.Shared.Enums;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
@@ -5,6 +6,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using System;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
@@ -18,7 +20,7 @@ namespace Robust.Client.GameStates
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _shader;
public NetInterpOverlay() : base(nameof(NetInterpOverlay))
public NetInterpOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
@@ -85,14 +87,14 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (bValue && !overlayMan.HasOverlay(nameof(NetInterpOverlay)))
if (bValue && !overlayMan.HasOverlay<NetInterpOverlay>())
{
overlayMan.AddOverlay(new NetInterpOverlay());
shell.WriteLine("Enabled network interp overlay.");
}
else if (overlayMan.HasOverlay(nameof(NetInterpOverlay)))
else if (overlayMan.HasOverlay<NetInterpOverlay>())
{
overlayMan.RemoveOverlay(nameof(NetInterpOverlay));
overlayMan.RemoveOverlay<NetInterpOverlay>();
shell.WriteLine("Disabled network interp overlay.");
}
}

View File

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

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics.Clyde
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_configurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
ConfigurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
}
private void _audioCreateContext()
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
private void _audioOpenDevice()
{
var preferredDevice = _configurationManager.GetCVar(CVars.AudioDevice);
var preferredDevice = ConfigurationManager.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
@@ -482,7 +482,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -503,7 +503,7 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private static bool ValidatePosition(float x, float y)
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
@@ -513,6 +513,22 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
_checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
@@ -667,7 +683,6 @@ namespace Robust.Client.Graphics.Clyde
_checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
@@ -694,7 +709,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -706,7 +721,7 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private static bool ValidatePosition(float x, float y)
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
@@ -716,6 +731,22 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
_checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();

View File

@@ -12,7 +12,8 @@ namespace Robust.Client.Graphics.Clyde
static Clyde()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
RuntimeInformation.ProcessArchitecture == Architecture.X64)
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
{
try
{

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Log;
@@ -115,7 +115,7 @@ namespace Robust.Client.Graphics.Clyde
var prev = cap;
var cVarName = $"display.ogl_block_{capName}";
var block = _configurationManager.GetCVar<bool>(cVarName);
var block = ConfigurationManager.GetCVar<bool>(cVarName);
if (block)
{
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
foreach (var cvar in cvars)
{
_configurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
ConfigurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
}
}

View File

@@ -208,12 +208,12 @@ namespace Robust.Client.Graphics.Clyde
_setChunkDirty(grid, chunk);
}
private void _updateOnGridCreated(GridId gridId)
private void _updateOnGridCreated(MapId mapId, GridId gridId)
{
_mapChunkData.Add(gridId, new Dictionary<Vector2i, MapChunkData>());
}
private void _updateOnGridRemoved(GridId gridId)
private void _updateOnGridRemoved(MapId mapId, GridId gridId)
{
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)

View File

@@ -7,6 +7,8 @@ using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Enums;
namespace Robust.Client.Graphics.Clyde
{
@@ -68,8 +70,7 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(OverlaySpace.ScreenSpaceBelowWorld);
_mainViewport.Eye = _eyeManager.CurrentEye;
RenderViewport(_mainViewport);
RenderViewport(_mainViewport); //Worldspace overlays are rendered here.
{
var handle = _renderHandle.DrawingHandleScreen;
var tex = _mainViewport.RenderTarget.Texture;
@@ -107,25 +108,67 @@ namespace Robust.Client.Graphics.Clyde
list.Add(overlay);
}
}
list.Sort(OverlayComparer.Instance);
foreach (var overlay in list)
{
overlay.ClydeRender(_renderHandle, space);
}
FlushRenderQueue();
list.Sort(OverlayComparer.Instance);
foreach (var overlay in list) {
if (overlay.RequestScreenTexture) {
FlushRenderQueue();
UpdateOverlayScreenTexture(space, _mainViewport.RenderTarget);
}
if (overlay.OverwriteTargetFrameBuffer()) {
ClearFramebuffer(default);
}
overlay.ClydeRender(_renderHandle, space);
FlushRenderQueue();
}
}
}
private void DrawEntitiesAndWorldOverlay(Viewport viewport, Box2 worldBounds)
private ClydeTexture? ScreenBufferTexture;
private GLHandle screenBufferHandle;
private Vector2 lastFrameSize;
/// <summary>
/// Sends SCREEN_TEXTURE to all overlays in the given OverlaySpace that request it.
/// </summary>
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture) {
//This currently does NOT consider viewports and just grabs the current screen framebuffer. This will need to be improved upon in the future.
List<Overlay> oTargets = new List<Overlay>();
foreach (var overlay in _overlayManager.AllOverlays) {
if (overlay.RequestScreenTexture && overlay.Space == space) {
oTargets.Add(overlay);
}
}
if (oTargets.Count > 0 && ScreenBufferTexture != null) {
if (lastFrameSize != _framebufferSize) {
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
GL.TexImage2D(TextureTarget.Texture2D, 0, _hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, _framebufferSize.X, _framebufferSize.Y, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
}
lastFrameSize = _framebufferSize;
CopyRenderTextureToTexture(texture, ScreenBufferTexture);
foreach (Overlay overlay in oTargets) {
overlay.ScreenTexture = ScreenBufferTexture;
}
oTargets.Clear();
return true;
}
return false;
}
private void DrawEntities(Viewport viewport, Box2 worldBounds)
{
if (_eyeManager.CurrentMap == MapId.Nullspace || !_mapManager.HasMapEntity(_eyeManager.CurrentMap))
{
return;
}
RenderOverlays(OverlaySpace.WorldSpaceBelowEntities);
var screenSize = viewport.Size;
// So we could calculate the correct size of the entities based on the contents of their sprite...
@@ -267,14 +310,6 @@ namespace Robust.Client.Graphics.Clyde
_drawingSpriteList.Clear();
FlushRenderQueue();
// Cleanup remainders
foreach (var overlay in worldOverlays)
{
overlay.ClydeRender(_renderHandle, OverlaySpace.WorldSpace);
}
FlushRenderQueue();
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -283,29 +318,44 @@ namespace Robust.Client.Graphics.Clyde
{
var spriteSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var tree = spriteSystem.GetSpriteTreeForMap(map);
tree.QueryAabb(ref list, ((
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
in SpriteComponent value) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
{
if (value.ContainerOccluded || !value.Visible)
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
return true;
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
var entity = value.Owner;
var transform = entity.Transform;
var tree = spriteSystem.GetSpriteTreeForMap(map, gridId);
ref var entry = ref state.AllocAdd();
entry.sprite = value;
entry.worldRot = transform.WorldRotation;
entry.matrix = transform.WorldMatrix;
var worldPos = entry.matrix.Transform(transform.LocalPosition);
entry.yWorldPos = worldPos.Y;
return true;
tree.QueryAabb(ref list, ((
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
in SpriteComponent value) =>
{
// TODO: Probably value in storing this as its own DynamicTree
if (value.ContainerOccluded || !value.Visible)
{
return true;
}
}), worldBounds, approx: true);
var entity = value.Owner;
var transform = entity.Transform;
ref var entry = ref state.AllocAdd();
entry.sprite = value;
entry.worldRot = transform.WorldRotation;
entry.matrix = transform.WorldMatrix;
var worldPos = entry.matrix.Transform(transform.LocalPosition);
entry.yWorldPos = worldPos.Y;
return true;
}), gridBounds, approx: true);
}
}
private void DrawSplash(IRenderHandle handle)
@@ -368,12 +418,21 @@ namespace Robust.Client.Graphics.Clyde
// We will also render worldspace overlays here so we can do them under / above entities as necessary
using (DebugGroup("Entities"))
{
DrawEntitiesAndWorldOverlay(viewport, worldBounds);
DrawEntities(viewport, worldBounds);
}
RenderOverlays(OverlaySpace.WorldSpaceBelowFOV);
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
{
GL.Clear(ClearBufferMask.StencilBufferBit);
GL.Enable(EnableCap.StencilTest);
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
GL.StencilMask(0xFF);
ApplyFovToBuffer(viewport, eye);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
}
}
@@ -401,6 +460,14 @@ namespace Robust.Client.Graphics.Clyde
viewport.WallBleedIntermediateRenderTarget2.Texture,
UIBox2.FromDimensions(Vector2.Zero, ScreenSize), new Color(1, 1, 1, 0.5f));
}
RenderOverlays(OverlaySpace.WorldSpace);
GL.StencilFunc(StencilFunction.Notequal, 1, 0xFF);
GL.Disable(EnableCap.DepthTest);
RenderOverlays(OverlaySpace.WorldSpaceFOVStencil);
GL.Disable(EnableCap.StencilTest);
}
PopRenderStateFull(state);

View File

@@ -5,6 +5,7 @@ using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
using ES20 = OpenToolkit.Graphics.ES20;
namespace Robust.Client.Graphics.Clyde
@@ -31,6 +32,27 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, glHandle.Handle);
CheckGlError();
GL.ActiveTexture(TextureUnit.Texture0);
}
private void CopyRenderTextureToTexture(RenderTexture source, ClydeTexture target) {
LoadedRenderTarget sourceLoaded = RtToLoaded(source);
bool pause = sourceLoaded != _currentBoundRenderTarget;
FullStoredRendererState? store = null;
if (pause) {
store = PushRenderStateFull();
BindRenderTargetFull(source);
CheckGlError();
}
GL.BindTexture(TextureTarget.Texture2D, _loadedTextures[target.TextureId].OpenGLObject.Handle);
CheckGlError();
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, _framebufferSize.X, _framebufferSize.Y);
CheckGlError();
if (pause && store != null) {
PopRenderStateFull((FullStoredRendererState)store);
}
}
private static long EstPixelSize(PixelInternalFormat format)

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using static Robust.Client.GameObjects.ClientOccluderComponent;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
namespace Robust.Client.Graphics.Clyde
{
@@ -195,7 +196,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
_lightSoftShaderHandle = LoadShaderHandle("/Shaders/Internal/light-soft.swsl");
_lightHardShaderHandle = LoadShaderHandle("/Shaders/Internal/light-hard.swsl");
_fovShaderHandle = LoadShaderHandle("/Shaders/Internal/fov.swsl");
@@ -361,16 +361,23 @@ namespace Robust.Client.Graphics.Clyde
FinalizeDepthDraw();
}
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
CheckGlError();
GLClearColor(_lightManager.AmbientLightColor);
GL.Clear(ClearBufferMask.ColorBufferBit);
CheckGlError();
GL.Enable(EnableCap.StencilTest);
_isStencilling = true;
var (lightW, lightH) = GetLightMapSize(viewport.Size);
GL.Viewport(0, 0, lightW, lightH);
CheckGlError();
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
CheckGlError();
GLClearColor(_lightManager.AmbientLightColor);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
CheckGlError();
ApplyLightingFovToBuffer(viewport, eye);
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle].Program;
lightShader.Use();
@@ -382,6 +389,11 @@ namespace Robust.Client.Graphics.Clyde
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
CheckGlError();
GL.StencilFunc(StencilFunction.Equal, 0xFF, 0xFF);
CheckGlError();
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Keep);
CheckGlError();
var lastRange = float.NaN;
var lastPower = float.NaN;
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
@@ -463,11 +475,11 @@ namespace Robust.Client.Graphics.Clyde
}
ResetBlendFunc();
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
CheckGlError();
ApplyLightingFovToBuffer(viewport, eye);
BlurOntoWalls(viewport, eye);
MergeWallLayer(viewport);
@@ -485,40 +497,55 @@ namespace Robust.Client.Graphics.Clyde
GetLightsToRender(MapId map, in Box2 worldBounds)
{
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var lightTree = renderingTreeSystem.GetLightTreeForMap(map);
var state = (this, worldBounds, count: 0);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
{
var transform = light.Owner.Transform;
Box2 gridBounds;
if (state.count >= LightsToRenderListSize)
if (gridId == GridId.Invalid)
{
// There are too many lights to fit in the static memory.
return false;
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
if (!light.Enabled || light.ContainerOccluded)
var lightTree = renderingTreeSystem.GetLightTreeForMap(map, gridId);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
{
var transform = light.Owner.Transform;
if (state.count >= LightsToRenderListSize)
{
// There are too many lights to fit in the static memory.
return false;
}
// TODO: Don't insert into trees for these, same as sprites.
if (!light.Enabled || light.ContainerOccluded)
{
return true;
}
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var circle = new Circle(lightPos, light.Radius);
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldBounds))
{
return true;
}
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var circle = new Circle(lightPos, light.Radius);
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldBounds))
{
return true;
}
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}, worldBounds);
}, gridBounds);
}
if (state.count > _maxLightsPerScene)
{
@@ -690,6 +717,13 @@ namespace Robust.Client.Graphics.Clyde
fovShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
fovShader.SetUniformMaybe("center", eye.Position.Position);
GL.StencilMask(0xFF);
CheckGlError();
GL.StencilFunc(StencilFunction.Always, 0, 0);
CheckGlError();
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Replace);
CheckGlError();
DrawBlit(viewport, fovShader);
if (_hasGLSamplerObjects)
@@ -939,7 +973,8 @@ namespace Robust.Client.Graphics.Clyde
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize, lightMapColorFormat,
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

View File

@@ -280,7 +280,7 @@ namespace Robust.Client.Graphics.Clyde
}
/// <summary>
/// Flush the render handle, processing and re-pooling all the command lists.
/// Flushes the render handle, processing and re-pooling all the command lists.
/// </summary>
private void FlushRenderQueue()
{
@@ -371,6 +371,7 @@ namespace Robust.Client.Graphics.Clyde
program.Use();
int textureUnitVal = 0;
// Assign shader parameters to uniform since they may be dirty.
foreach (var (name, value) in instance.Parameters)
{
@@ -413,6 +414,15 @@ namespace Robust.Client.Graphics.Clyde
case Matrix4 matrix4:
program.SetUniform(name, matrix4);
break;
case ClydeTexture clydeTexture:
//It's important to start at Texture6 here since DrawCommandBatch uses Texture0 and Texture1 immediately after calling this
//function! If passing in textures as uniforms ever stops working it might be since someone made it use all the way up to Texture6 too.
//Might change this in the future?
TextureUnit cTarget = TextureUnit.Texture6+textureUnitVal;
SetTexture(cTarget, ((ClydeTexture)clydeTexture).TextureId);
program.SetUniformTexture(name, cTarget);
textureUnitVal++;
break;
default:
throw new InvalidOperationException($"Unable to handle shader parameter {name}: {value}");
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -428,7 +428,8 @@ namespace Robust.Client.Graphics.Clyde
private protected override void SetParameterImpl(string name, Texture value)
{
throw new NotImplementedException();
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetStencilOpImpl(StencilOp op)

View File

@@ -26,7 +26,7 @@ namespace Robust.Client.Graphics.Clyde
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null)
{
DebugTools.Assert(_mainThread == Thread.CurrentThread);
@@ -37,7 +37,7 @@ namespace Robust.Client.Graphics.Clyde
return LoadTextureFromImage(image, name, loadParams);
}
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
{
DebugTools.Assert(_mainThread == Thread.CurrentThread);
@@ -56,19 +56,19 @@ namespace Robust.Client.Graphics.Clyde
{
// Disable sRGB so stuff doesn't get interpreter wrong.
actualParams.Srgb = false;
var img = ApplyA8Swizzle((Image<A8>) (object) image);
using var img = ApplyA8Swizzle((Image<A8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
if (pixelType == typeof(L8) && !actualParams.Srgb)
{
var img = ApplyL8Swizzle((Image<L8>) (object) image);
using var img = ApplyL8Swizzle((Image<L8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
}
// Flip image because OpenGL reads images upside down.
var copy = FlipClone(image);
using var copy = FlipClone(image);
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
@@ -324,11 +324,13 @@ namespace Robust.Client.Graphics.Clyde
if (typeof(T) == typeof(A8))
{
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
return;
}
if (typeof(T) == typeof(L8))
{
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
return;
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
@@ -14,6 +15,7 @@ using Robust.Shared;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static Robust.Client.Utility.LiterallyJustMessageBox;
@@ -46,6 +48,7 @@ namespace Robust.Client.Graphics.Clyde
{
// Keep delegates around to prevent GC issues.
private GLFWCallbacks.ErrorCallback _errorCallback = default!;
private GLFWCallbacks.MonitorCallback _monitorCallback = default!;
private GLFWCallbacks.CharCallback _charCallback = default!;
private GLFWCallbacks.CursorPosCallback _cursorPosCallback = default!;
private GLFWCallbacks.KeyCallback _keyCallback = default!;
@@ -73,6 +76,18 @@ namespace Robust.Client.Graphics.Clyde
private Vector2 _lastMousePos;
// Can't use ClydeHandle because it's 64 bit.
private int _nextWindowId = 1;
private readonly Dictionary<int, MonitorReg> _monitors = new();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public event Action? OnWindowScaleChanged;
// NOTE: in engine we pretend the framebuffer size is the screen size..
// For practical reasons like UI rendering.
public override Vector2i ScreenSize => _framebufferSize;
@@ -148,15 +163,60 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
InitMonitors();
InitCursors();
return InitWindow();
}
private void InitMonitors()
{
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
SetupMonitor(monitors[i]);
}
}
private void SetupMonitor(Monitor* monitor)
{
var handle = _nextWindowId++;
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, "GLFW window already has user pointer??");
var name = GLFW.GetMonitorName(monitor);
var videoMode = GLFW.GetVideoMode(monitor);
var impl = new ClydeMonitorImpl(handle, name, (videoMode->Width, videoMode->Height), videoMode->RefreshRate);
GLFW.SetMonitorUserPointer(monitor, (void*) handle);
_monitors[handle] = new MonitorReg
{
Id = handle,
Impl = impl,
Monitor = monitor
};
}
private void DestroyMonitor(Monitor* monitor)
{
var ptr = GLFW.GetMonitorUserPointer(monitor);
if (ptr == null)
{
var name = GLFW.GetMonitorName(monitor);
Logger.WarningS("clyde.win", $"Monitor '{name}' had no user pointer set??");
return;
}
_monitors.Remove((int) ptr);
GLFW.SetMonitorUserPointer(monitor, null);
}
private bool InitWindow()
{
var width = _configurationManager.GetCVar(CVars.DisplayWidth);
var height = _configurationManager.GetCVar(CVars.DisplayHeight);
var width = ConfigurationManager.GetCVar(CVars.DisplayWidth);
var height = ConfigurationManager.GetCVar(CVars.DisplayHeight);
Monitor* monitor = null;
@@ -166,6 +226,8 @@ namespace Robust.Client.Graphics.Clyde
var mode = GLFW.GetVideoMode(monitor);
width = mode->Width;
height = mode->Height;
GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate);
}
#if DEBUG
@@ -174,13 +236,16 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
var renderer = (Renderer) _configurationManager.GetCVar<int>(CVars.DisplayRenderer);
var renderer = (Renderer) ConfigurationManager.GetCVar<int>(CVars.DisplayRenderer);
Span<Renderer> renderers = (renderer == Renderer.Default) ? stackalloc Renderer[] {
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
} : stackalloc Renderer[] {renderer};
Span<Renderer> renderers = (renderer == Renderer.Default)
? stackalloc Renderer[]
{
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
}
: stackalloc Renderer[] {renderer};
foreach (Renderer r in renderers)
{
@@ -193,6 +258,7 @@ namespace Robust.Client.Graphics.Clyde
_isCore = renderer == Renderer.OpenGL33;
break;
}
// Window failed to init due to error.
// Try not to treat the error code seriously.
var code = GLFW.GetError(out string desc);
@@ -206,9 +272,9 @@ namespace Robust.Client.Graphics.Clyde
var code = GLFW.GetError(out string desc);
var errorContent = "Failed to create the game window. " +
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
MessageBoxW(null,
errorContent,
@@ -301,6 +367,7 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
}
_glfwWindow = GLFW.CreateWindow(width, height, string.Empty, monitor, null);
}
}
@@ -393,11 +460,30 @@ namespace Robust.Client.Graphics.Clyde
Logger.ErrorS("clyde.win.glfw", "GLFW Error: [{0}] {1}", code, description);
}
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
{
try
{
if (state == ConnectedState.Connected)
{
SetupMonitor(monitor);
}
else
{
DestroyMonitor(monitor);
}
}
catch (Exception e)
{
CatchCallbackException(e);
}
}
private void OnGlfwChar(Window* window, uint codepoint)
{
try
{
_gameController.TextEntered(new TextEventArgs(codepoint));
TextEntered?.Invoke(new TextEventArgs(codepoint));
}
catch (Exception e)
{
@@ -413,8 +499,7 @@ namespace Robust.Client.Graphics.Clyde
var delta = newPos - _lastMousePos;
_lastMousePos = newPos;
var ev = new MouseMoveEventArgs(delta, newPos);
_gameController.MouseMove(ev);
MouseMove?.Invoke(new MouseMoveEventArgs(delta, newPos));
}
catch (Exception e)
{
@@ -461,11 +546,11 @@ namespace Robust.Client.Graphics.Clyde
switch (action)
{
case InputAction.Release:
_gameController.KeyUp(ev);
KeyUp?.Invoke(ev);
break;
case InputAction.Press:
case InputAction.Repeat:
_gameController.KeyDown(ev);
KeyDown?.Invoke(ev);
break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, null);
@@ -477,7 +562,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
var ev = new MouseWheelEventArgs(((float) offsetX, (float) offsetY), _lastMousePos);
_gameController.MouseWheel(ev);
MouseWheel?.Invoke(ev);
}
catch (Exception e)
{
@@ -489,7 +574,7 @@ namespace Robust.Client.Graphics.Clyde
{
try
{
_gameController.Shutdown("Window closed");
CloseWindow?.Invoke("Window closed");
}
catch (Exception e)
{
@@ -533,6 +618,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
_windowScale = (xScale, yScale);
OnWindowScaleChanged?.Invoke();
}
catch (Exception e)
{
@@ -568,6 +654,7 @@ namespace Robust.Client.Graphics.Clyde
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
_monitorCallback = OnGlfwMonitor;
_charCallback = OnGlfwChar;
_cursorPosCallback = OnGlfwCursorPos;
_keyCallback = OnGlfwKey;
@@ -590,6 +677,19 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetWindowTitle(_glfwWindow, title);
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
var monitorImpl = (ClydeMonitorImpl) monitor;
var reg = _monitors[monitorImpl.Id];
GLFW.SetWindowMonitor(
_glfwWindow,
reg.Monitor,
0, 0,
monitorImpl.Size.X, monitorImpl.Size.Y,
monitorImpl.RefreshRate);
}
public void RequestWindowAttention()
{
GLFW.RequestWindowAttention(_glfwWindow);
@@ -654,18 +754,40 @@ namespace Robust.Client.Graphics.Clyde
GLFW.GetWindowPos(_glfwWindow, out var x, out var y);
_prevWindowPos = (x, y);
var monitor = GLFW.GetPrimaryMonitor();
var monitor = MonitorForWindow(_glfwWindow);
var mode = GLFW.GetVideoMode(monitor);
GLFW.SetWindowMonitor(_glfwWindow, GLFW.GetPrimaryMonitor(), 0, 0, mode->Width, mode->Height,
GLFW.SetWindowMonitor(_glfwWindow, monitor, 0, 0, mode->Width, mode->Height,
mode->RefreshRate);
}
else
{
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X, _prevWindowSize.Y, 0);
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X,
_prevWindowSize.Y, 0);
}
}
// glfwGetWindowMonitor only works for fullscreen windows.
// Picks the monitor with the top-left corner of the window.
private Monitor* MonitorForWindow(Window* window)
{
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
var monitor = monitors[i];
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
var videoMode = GLFW.GetVideoMode(monitor);
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
if (box.Contains(winPosX, winPosY))
return monitor;
}
// Fallback
return GLFW.GetPrimaryMonitor();
}
string IClipboardManager.GetText()
{
return GLFW.GetClipboardString(_glfwWindow);
@@ -676,6 +798,11 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetClipboardString(_glfwWindow, text);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
return _monitors.Values.Select(c => c.Impl);
}
// We can't let exceptions unwind into GLFW, as that can cause the CLR to crash.
// And it probably messes up GLFW too.
// So all the callbacks are passed to this method.
@@ -689,5 +816,28 @@ namespace Robust.Client.Graphics.Clyde
_glfwExceptionList.Add(e);
}
private sealed class MonitorReg
{
public int Id;
public Monitor* Monitor;
public ClydeMonitorImpl Impl = default!;
}
private sealed class ClydeMonitorImpl : IClydeMonitor
{
public ClydeMonitorImpl(int id, string name, Vector2i size, int refreshRate)
{
Id = id;
Name = name;
Size = size;
RefreshRate = refreshRate;
}
public int Id { get; }
public string Name { get; }
public Vector2i Size { get; }
public int RefreshRate { get; }
}
}
}

View File

@@ -87,7 +87,7 @@ namespace Robust.Client.Graphics.Clyde
{
base.Initialize();
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
ConfigurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
if (!InitWindowing())
{
@@ -124,9 +124,9 @@ namespace Robust.Client.Graphics.Clyde
protected override void ReadConfig()
{
base.ReadConfig();
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
_lightmapDivider = ConfigurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = ConfigurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = ConfigurationManager.GetCVar(CVars.DisplaySoftShadows);
}
protected override void ReloadConfig()
@@ -238,7 +238,7 @@ namespace Robust.Client.Graphics.Clyde
private (int major, int minor)? ParseGLOverrideVersion()
{
var overrideGLVersion = _configurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
var overrideGLVersion = ConfigurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
if (string.IsNullOrEmpty(overrideGLVersion))
{
return null;
@@ -315,6 +315,11 @@ namespace Robust.Client.Graphics.Clyde
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
CreateMainViewport();
screenBufferHandle = new GLHandle(GL.GenTexture());
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
ApplySampleParameters(TextureSampleParameters.Default);
ScreenBufferTexture = GenTexture(screenBufferHandle, _framebufferSize, true, null, TexturePixelType.Rgba32);
}
private void CreateMainViewport()

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using Robust.Client.Audio;
@@ -37,6 +38,13 @@ namespace Robust.Client.Graphics.Clyde
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public Texture GetStockTexture(ClydeStockTexture stockTexture)
{
return new DummyTexture((1, 1));
@@ -63,6 +71,11 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
// Nada.
}
public void RequestWindowAttention()
{
// Nada.
@@ -86,6 +99,12 @@ namespace Robust.Client.Graphics.Clyde
remove { }
}
public event Action OnWindowScaleChanged
{
add { }
remove { }
}
public void Render()
{
// Nada.
@@ -101,7 +120,7 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null)
{
using (var image = Image.Load<Rgba32>(stream))
@@ -110,7 +129,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
{
return new DummyTexture((image.Width, image.Height));
@@ -156,6 +175,12 @@ namespace Robust.Client.Graphics.Clyde
return new Viewport();
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
// TODO: Actually return something.
yield break;
}
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
{
return default;
@@ -267,6 +292,11 @@ namespace Robust.Client.Graphics.Clyde
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;

View File

@@ -4,7 +4,6 @@ varying highp vec2 Pos;
uniform sampler2D lightMap;
uniform highp vec4 modulate;
#line 1000
// [SHADER_HEADER_CODE]
void main()
@@ -13,7 +12,6 @@ void main()
lowp vec4 COLOR;
#line 10000
// [SHADER_CODE]
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;

View File

@@ -89,9 +89,10 @@ uniform highp vec2 TEXTURE_PIXEL_SIZE;
// -- srgb emulation --
#ifdef HAS_SRGB
highp vec4 zTexture(highp vec2 uv)
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
{
return texture2D(TEXTURE, uv);
return texture2D(tex, uv);
}
highp vec4 zAdjustResult(highp vec4 col)
@@ -101,9 +102,9 @@ highp vec4 zAdjustResult(highp vec4 col)
#else
uniform lowp vec2 SRGB_EMU_CONFIG;
highp vec4 zTexture(highp vec2 uv)
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
{
highp vec4 col = texture2D(TEXTURE, uv);
highp vec4 col = texture2D(tex, uv);
if (SRGB_EMU_CONFIG.x > 0.5)
{
return zFromSrgb(col);
@@ -121,5 +122,10 @@ highp vec4 zAdjustResult(highp vec4 col)
}
#endif
highp vec4 zTexture(highp vec2 uv)
{
return zTextureSpec(TEXTURE, uv);
}
// -- Utilities End --

View File

@@ -18,8 +18,7 @@ namespace Robust.Client.Graphics
/// </summary>
internal abstract class ClydeBase
{
[Dependency] protected readonly IConfigurationManager _configurationManager = default!;
[Dependency] protected readonly IGameControllerInternal _gameController = default!;
[Dependency] protected readonly IConfigurationManager ConfigurationManager = default!;
protected WindowMode WindowMode { get; private set; } = WindowMode.Windowed;
protected bool VSync { get; private set; } = true;
@@ -31,11 +30,11 @@ namespace Robust.Client.Graphics
public virtual bool Initialize()
{
_configurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
_configurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
return true;
}
@@ -51,8 +50,8 @@ namespace Robust.Client.Graphics
protected virtual void ReadConfig()
{
WindowMode = (WindowMode) _configurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = _configurationManager.GetCVar(CVars.DisplayVSync);
WindowMode = (WindowMode) ConfigurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = ConfigurationManager.GetCVar(CVars.DisplayVSync);
}
private void _vSyncChanged(bool newValue)

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -40,63 +41,48 @@ namespace Robust.Client.Graphics
return GetLineHeight(scale) - GetHeight(scale);
}
[Obsolete("Use GetAscent")] public int Ascent => GetAscent(1);
[Obsolete("Use GetHeight")] public int Height => GetHeight(1);
[Obsolete("Use GetDescent")] public int Descent => GetDescent(1);
[Obsolete("Use GetLineHeight")] public int LineHeight => GetLineHeight(1);
[Obsolete("Use GetLineSeparation")] public int LineSeparation => GetLineSeparation(1);
// Yes, I am aware that using char is bad.
// At the same time the font system is nowhere close to rendering Unicode so...
/// <summary>
/// Draw a character at a certain baseline position on screen.
/// </summary>
/// <param name="handle">The drawing handle to draw to.</param>
/// <param name="chr">
/// The Unicode code point to draw. Yes I'm aware about UTF-16 being crap,
/// do you think this system can draw anything except ASCII?
/// </param>
/// <param name="rune">The Unicode code point to draw.</param>
/// <param name="baseline">The baseline from which to draw the character.</param>
/// <param name="scale">DPI scale factor to render the font at.</param>
/// <param name="color">The color of the character to draw.</param>
/// <param name="fallback">If the character is not available, render "<22>" instead.</param>
/// <returns>How much to advance the cursor to draw the next character.</returns>
[Obsolete("Use DrawChar with scale support.")]
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
{
return DrawChar(handle, chr, baseline, 1, color);
}
public abstract float DrawChar(
DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale,
Color color, bool fallback=true);
public abstract float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale,
Color color);
[Obsolete("Use Rune variant instead")]
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
{
return DrawChar(handle, (Rune) chr, baseline, scale, color);
}
/// <summary>
/// Gets metrics describing the dimensions and positioning of a single glyph in the font.
/// </summary>
/// <param name="chr">The character to fetch the glyph metrics for.</param>
/// <param name="rune">The unicode codepoint to fetch the glyph metrics for.</param>
/// <param name="scale">DPI scale factor to render the font at.</param>
/// <param name="fallback">
/// If the character is not available, return data for "<22>" instead.
/// This can still fail if the font does not define <20> itself.
/// </param>
/// <returns>
/// <c>null</c> if this font does not have a glyph for the specified character,
/// otherwise the metrics you asked for.
/// </returns>
/// <seealso cref="TryGetCharMetrics"/>
[Obsolete("Use GetCharMetrics with scale support.")]
public CharMetrics? GetCharMetrics(char chr)
{
return GetCharMetrics(chr, 1);
}
public abstract CharMetrics? GetCharMetrics(char chr, float scale);
public abstract CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true);
/// <summary>
/// Try-pattern version of <see cref="GetCharMetrics"/>.
/// </summary>
[Obsolete("Use TryGetCharMetrics with scale support.")]
public bool TryGetCharMetrics(char chr, out CharMetrics metrics)
public bool TryGetCharMetrics(Rune rune, float scale, out CharMetrics metrics, bool fallback=true)
{
return TryGetCharMetrics(chr, 1, out metrics);
}
public bool TryGetCharMetrics(char chr, float scale, out CharMetrics metrics)
{
var maybe = GetCharMetrics(chr, scale);
var maybe = GetCharMetrics(rune, scale);
if (maybe.HasValue)
{
metrics = maybe.Value;
@@ -128,15 +114,23 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => Handle.GetDescent(scale);
public override int GetLineHeight(float scale) => Handle.GetLineHeight(scale);
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
var metrics = Handle.GetCharMetrics(chr, scale);
var metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
{
return 0;
if (fallback && !Rune.IsWhiteSpace(rune))
{
rune = new Rune('<27>');
metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
return 0;
}
else
return 0;
}
var texture = Handle.GetCharTexture(chr, scale);
var texture = Handle.GetCharTexture(rune, scale);
if (texture == null)
{
return metrics.Value.Advance;
@@ -147,9 +141,12 @@ namespace Robust.Client.Graphics
return metrics.Value.Advance;
}
public override CharMetrics? GetCharMetrics(char chr, float scale)
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
{
return Handle.GetCharMetrics(chr, scale);
var metrics = Handle.GetCharMetrics(rune, scale);
if (metrics == null && !Rune.IsWhiteSpace(rune) && fallback)
return Handle.GetCharMetrics(new Rune('<27>'), scale);
return metrics;
}
}
@@ -160,13 +157,13 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => default;
public override int GetLineHeight(float scale) => default;
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
// Nada, it's a dummy after all.
return 0;
}
public override CharMetrics? GetCharMetrics(char chr, float scale)
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
{
// Nada, it's a dummy after all.
return null;

View File

@@ -4,9 +4,6 @@ using System.IO;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;
@@ -20,18 +17,18 @@ namespace Robust.Client.Graphics
private const int SheetWidth = 256;
private const int SheetHeight = 256;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IClyde _clyde = default!;
private readonly IClyde _clyde;
private uint BaseFontDPI;
private uint _baseFontDpi = 96;
private readonly Library _library;
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
new();
public FontManager()
public FontManager(IClyde clyde)
{
_clyde = clyde;
_library = new Library();
}
@@ -42,9 +39,9 @@ namespace Robust.Client.Graphics
return handle;
}
void IFontManagerInternal.Initialize()
void IFontManagerInternal.SetFontDpi(uint fontDpi)
{
BaseFontDPI = (uint) _configuration.GetCVar(CVars.DisplayFontDpi);
_baseFontDpi = fontDpi;
}
public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size)
@@ -64,7 +61,7 @@ namespace Robust.Client.Graphics
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
{
var ftFace = instance.FaceHandle.Face;
ftFace.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
ftFace.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
var ascent = ftFace.Size.Metrics.Ascender.ToInt32();
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
@@ -83,7 +80,7 @@ namespace Robust.Client.Graphics
return;
var face = instance.FaceHandle.Face;
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
face.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
@@ -189,7 +186,7 @@ namespace Robust.Client.Graphics
OwnedTexture GenSheet()
{
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
$"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}");
scaled.AtlasTextures.Add(sheet);
return sheet;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Robust.Shared.Maths;
@@ -21,6 +22,7 @@ namespace Robust.Client.Graphics
Vector2 DefaultWindowScale { get; }
void SetWindowTitle(string title);
void SetWindowMonitor(IClydeMonitor monitor);
/// <summary>
/// This is the magic method to make the game window ping you in the task bar.
@@ -31,10 +33,12 @@ namespace Robust.Client.Graphics
event Action<WindowFocusedEventArgs> OnWindowFocused;
Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
event Action OnWindowScaleChanged;
OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null);
Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>;
/// <summary>
@@ -104,6 +108,8 @@ namespace Robust.Client.Graphics
}
IClydeViewport CreateViewport(Vector2i size, string? name = null);
IEnumerable<IClydeMonitor> EnumerateMonitors();
}
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.

View File

@@ -20,5 +20,6 @@ namespace Robust.Client.Graphics
void SetVolume(float decibels);
void SetOcclusion(float blocks);
void SetPlaybackPosition(float seconds);
void SetVelocity(Vector2 velocity);
}
}

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Maths;
@@ -16,6 +17,13 @@ namespace Robust.Client.Graphics
bool Initialize();
void Ready();
event Action<TextEventArgs> TextEntered;
event Action<MouseMoveEventArgs> MouseMove;
event Action<KeyEventArgs> KeyUp;
event Action<KeyEventArgs> KeyDown;
event Action<MouseWheelEventArgs> MouseWheel;
event Action<string> CloseWindow;
ClydeHandle LoadShader(ParsedShader shader, string? name = null);
void ReloadShader(ClydeHandle handle, ParsedShader newShader);

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
/// <summary>
/// Represents a connected monitor on the user's system.
/// </summary>
public interface IClydeMonitor
{
/// <summary>
/// This ID is not consistent between startups of the game.
/// </summary>
int Id { get; }
string Name { get; }
Vector2i Size { get; }
int RefreshRate { get; }
}
}

View File

@@ -12,7 +12,7 @@ namespace Robust.Client.Graphics
{
IFontFaceHandle Load(Stream stream);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void Initialize();
void SetFontDpi(uint fontDpi);
}
internal interface IFontFaceHandle

View File

@@ -1,22 +1,29 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
[PublicAPI]
public interface IOverlayManager
{
void AddOverlay(Overlay overlay);
void RemoveOverlay(string id);
bool HasOverlay(string id);
bool AddOverlay(Overlay overlay);
Overlay GetOverlay(string id);
T GetOverlay<T>(string id) where T : Overlay;
bool RemoveOverlay(Overlay overlay);
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay;
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;
bool HasOverlay(Type overlayClass);
bool HasOverlay<T>() where T : Overlay;
IEnumerable<Overlay> AllOverlays { get; }
}

View File

@@ -1,65 +1,54 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.IoC;
using JetBrains.Annotations;
using Robust.Shared.Timing;
using Robust.Shared.Enums;
using System;
namespace Robust.Client.Graphics
{
/// <summary>
/// An overlay is used for fullscreen drawing in the game, for example parallax.
/// An overlay is used for fullscreen drawing in the game. This can range from drawing parallax to a full screen shader.
/// </summary>
[PublicAPI]
public abstract class Overlay
{
/// <summary>
/// The ID of this overlay. This is used to identify it inside the <see cref="IOverlayManager"/>.
/// Determines when this overlay is drawn in the rendering queue.
/// </summary>
public string ID { get; }
public virtual bool AlwaysDirty => false;
public bool IsDirty => AlwaysDirty || _isDirty;
public bool Drawing { get; private set; }
public virtual OverlaySpace Space => OverlaySpace.ScreenSpace;
/// <summary>
/// If set to true, <see cref="ScreenTexture"/> will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but
/// some shaders will require it as a passed in uniform to operate.
/// </summary>
public virtual bool RequestScreenTexture => false;
/// <summary>
/// If <see cref="RequestScreenTexture"> is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null.
/// </summary>
public Texture? ScreenTexture = null;
/// <summary>
/// Overlays on the same OverlaySpace will be drawn from lowest ZIndex to highest ZIndex. As an example, ZIndex -1 will be drawn before ZIndex 2.
/// This value is 0 by default. Overlays with same ZIndex will be drawn in an random order.
/// </summary>
public int? ZIndex { get; set; }
protected IOverlayManager OverlayManager { get; }
public int? ZIndex { get; set; }
private bool Disposed = false;
public virtual bool SubHandlesUseMainShader { get; } = true;
private bool _isDirty = true;
private readonly List<DrawingHandleBase> TempHandles = new();
private bool Disposed;
protected Overlay(string id)
public Overlay()
{
OverlayManager = IoCManager.Resolve<IOverlayManager>();
ID = id;
}
public void Dispose()
{
if (Disposed)
{
return;
}
Dispose(true);
Disposed = true;
GC.SuppressFinalize(this);
}
~Overlay()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
/// <summary>
/// If this function returns true, the target framebuffer will be wiped before applying this overlay to it.
/// </summary>
public virtual bool OverwriteTargetFrameBuffer(){
return false;
}
/// <summary>
@@ -69,50 +58,34 @@ namespace Robust.Client.Graphics
/// <param name="currentSpace">Current space that is being drawn. This function is called for every space that was set up in initialization.</param>
protected abstract void Draw(DrawingHandleBase handle, OverlaySpace currentSpace);
public void Dirty()
{
_isDirty = true;
protected internal virtual void FrameUpdate(FrameEventArgs args) { }
~Overlay() {
Dispose();
}
public void Dispose() {
if (Disposed)
return;
else
DisposeBehavior();
}
protected virtual void DisposeBehavior(){
Disposed = true;
GC.SuppressFinalize(this);
}
protected internal virtual void FrameUpdate(FrameEventArgs args) { }
internal void ClydeRender(IRenderHandle renderHandle, OverlaySpace currentSpace)
{
DrawingHandleBase handle;
if (currentSpace == OverlaySpace.WorldSpace)
handle = renderHandle.DrawingHandleWorld;
else
if (currentSpace == OverlaySpace.ScreenSpace || currentSpace == OverlaySpace.ScreenSpaceBelowWorld)
handle = renderHandle.DrawingHandleScreen;
else
handle = renderHandle.DrawingHandleWorld;
Draw(handle, currentSpace);
}
}
/// <summary>
/// Determines in which canvas layers an overlay gets drawn.
/// </summary>
[Flags]
public enum OverlaySpace : byte
{
/// <summary>
/// Used for matching bit flags.
/// </summary>
None = 0b0000,
/// <summary>
/// This overlay will be drawn in the UI root, thus being in screen space.
/// </summary>
ScreenSpace = 0b0001,
/// <summary>
/// This overlay will be drawn in the world root, thus being in world space.
/// </summary>
WorldSpace = 0b0010,
/// <summary>
/// Drawn in screen coordinates, but behind the world.
/// </summary>
ScreenSpaceBelowWorld = 0b0100,
}
}

View File

@@ -1,13 +1,15 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
internal class OverlayManager : IOverlayManagerInternal
{
private readonly Dictionary<string, Overlay> _overlays = new();
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
{
@@ -17,59 +19,80 @@ namespace Robust.Client.Graphics
}
}
public void AddOverlay(Overlay overlay)
public bool AddOverlay(Overlay overlay)
{
if (_overlays.ContainsKey(overlay.ID))
{
throw new InvalidOperationException($"We already have an overlay with ID '{overlay.ID}'");
if(_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if(!overlayClass.IsSubclassOf(typeof(Overlay))){
Logger.Error("RemoveOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return false;
}
_overlays.Add(overlay.ID, overlay);
return _overlays.Remove(overlayClass);
}
public Overlay GetOverlay(string id)
{
return _overlays[id];
public bool RemoveOverlay<T>() where T : Overlay{
return RemoveOverlay(typeof(T));
}
public T GetOverlay<T>(string id) where T : Overlay
{
return (T) GetOverlay(id);
public bool RemoveOverlay(Overlay overlay) {
return _overlays.Remove(overlay.GetType());
}
public bool HasOverlay(string id)
{
return _overlays.ContainsKey(id);
}
public void RemoveOverlay(string id)
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
if (!_overlays.TryGetValue(id, out var overlay))
{
return;
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay))){
Logger.Error("TryGetOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return false;
}
overlay.Dispose();
_overlays.Remove(id);
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay)
{
return _overlays.TryGetValue(id, out overlay);
}
public bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay
{
if (_overlays.TryGetValue(id, out var value))
{
overlay = (T) value;
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay {
overlay = null;
if(_overlays.TryGetValue(typeof(T), out Overlay? toReturn)){
overlay = (T)toReturn;
return true;
}
overlay = default;
return false;
}
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public Overlay GetOverlay(Type overlayClass) {
return _overlays[overlayClass];
}
public T GetOverlay<T>() where T : Overlay {
return (T)_overlays[typeof(T)];
}
public bool HasOverlay(Type overlayClass) {
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
Logger.Error("HasOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return _overlays.ContainsKey(overlayClass);
}
public bool HasOverlay<T>() where T : Overlay {
return _overlays.ContainsKey(typeof(T));
}
}
}

View File

@@ -19,10 +19,10 @@ namespace Robust.Client.Graphics
public sealed class State : IRsiStateLike
{
// List of delays for the frame to reach the next frame.
private readonly float[] Delays;
public readonly float[] Delays;
// 2D array for the texture to use for each animation frame at each direction.
private readonly Texture[][] Icons;
public readonly Texture[][] Icons;
internal State(Vector2i size, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
{

View File

@@ -1,47 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "RSI Image Format Validation Schema V1",
"description": "Robust Station Image",
"type": "object",
"definitions": {
"size": {
"type": "object",
"properties": {
"x": {"type": "integer", "minimum": 1},
"y": {"type": "integer", "minimum": 1}
},
"required": ["x","y"]
},
"directions": {
"type": "integer",
"enum": [1,4,8]
},
"state": {
"type": "object",
"properties": {
"name": {"type": "string"},
"flags": {"type": "object"}, //To be de-serialized as a Dictionary
"directions": {"$ref": "#/definitions/directions"},
"delays": {
"type": "array",
"minItems": 1,
"items": {
"type": "array",
"items": {"type": "number", "minimum": 0, "exclusiveMinimum": true} //number == float
}
}
},
"required": ["name"] //'delays' is marked as optional in the spec
}
},
"properties": {
"version": {"type": "integer", "minimum": 1, "maximum": 1},
"size": {"$ref": "#/definitions/size"},
"states": {
"type": "array",
"items": {"$ref": "#/definitions/state"},
"minItems": 1
}
},
"required": ["version","size","states"]
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Robust.Shared.Maths;
namespace Robust.Client.Input
@@ -58,6 +59,7 @@ namespace Robust.Client.Input
}
public uint CodePoint { get; }
public Rune AsRune => new Rune(CodePoint);
}
public class KeyEventArgs : ModifierInputEventArgs

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -67,7 +68,7 @@ namespace Robust.Client.Physics
public override void Shutdown()
{
base.Shutdown();
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(nameof(PhysicsIslandOverlay));
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsIslandOverlay));
}
private void HandleIslandSolveMessage(IslandSolveMessage message)
@@ -94,7 +95,7 @@ namespace Robust.Client.Physics
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public PhysicsIslandOverlay() : base(nameof(PhysicsIslandOverlay))
public PhysicsIslandOverlay()
{
_islandSystem = EntitySystem.Get<DebugPhysicsIslandSystem>();
_eyeManager = IoCManager.Resolve<IEyeManager>();

View File

@@ -1,5 +1,7 @@
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -28,7 +30,7 @@ namespace Robust.Client.Placement.Modes
var mapId = MouseCoords.GetMapId(pManager.EntityManager);
var snapToEntities = pManager.EntityManager.GetEntitiesInRange(MouseCoords, SnapToRange)
var snapToEntities = IoCManager.Resolve<IEntityLookup>().GetEntitiesInRange(MouseCoords, SnapToRange)
.Where(entity => entity.Prototype == pManager.CurrentPrototype && entity.Transform.MapID == mapId)
.OrderBy(entity => (entity.Transform.WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager)).LengthSquared)
.ToList();

View File

@@ -1,4 +1,6 @@
using Robust.Shared.Map;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Placement.Modes
@@ -51,7 +53,7 @@ namespace Robust.Client.Placement.Modes
var topRight = new Vector2(CurrentTile.X + 0.99f, CurrentTile.Y + 0.99f);
var box = new Box2(bottomLeft, topRight);
return !pManager.EntityManager.AnyEntitiesIntersecting(map, box);
return !IoCManager.Resolve<IEntityLookup>().AnyEntitiesIntersecting(map, box);
}
}
}

View File

@@ -1,5 +1,7 @@
using Robust.Shared.Enums;
using Robust.Client.Graphics;
namespace Robust.Client.Placement
{
public partial class PlacementManager
@@ -7,10 +9,9 @@ namespace Robust.Client.Placement
internal class PlacementOverlay : Overlay
{
private readonly PlacementManager _manager;
public override bool AlwaysDirty => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public PlacementOverlay(PlacementManager manager) : base("placement")
public PlacementOverlay(PlacementManager manager)
{
_manager = manager;
ZIndex = 100;

View File

@@ -21,7 +21,6 @@ namespace Robust.Client.Player
void Initialize();
void Startup(INetChannel channel);
void Update(float frameTime);
void Shutdown();
void ApplyPlayerStates(IEnumerable<PlayerState>? list);

View File

@@ -95,12 +95,6 @@ namespace Robust.Client.Player
_network.ClientSendMessage(msgList);
}
/// <inheritdoc />
public void Update(float frameTime)
{
// Uh, nothing anymore I guess.
}
/// <inheritdoc />
public void Shutdown()
{

View File

@@ -2,6 +2,8 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Robust.Lite")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
#if NET5_0
[module: SkipLocalsInit]

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
@@ -53,20 +54,17 @@ namespace Robust.Client.Prototypes
private void ReloadPrototypeQueue()
{
#if !FULL_RELEASE
var then = DateTime.Now;
var sw = Stopwatch.StartNew();
var msg = NetManager.CreateNetMessage<MsgReloadPrototypes>();
msg.Paths = _reloadQueue.ToArray();
NetManager.ClientSendMessage(msg);
foreach (var path in _reloadQueue)
{
ReloadPrototypes(path);
}
ReloadPrototypes(_reloadQueue);
_reloadQueue.Clear();
Logger.Info($"Reloaded prototypes in {(int) (DateTime.Now - then).TotalMilliseconds} ms");
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
#endif
}

View File

@@ -10,5 +10,6 @@ namespace Robust.Client.ResourceManagement
void RsiLoaded(RsiLoadedEventArgs eventArgs);
void MountLoaderApi(IFileApi api, string apiPrefix, ResourcePath? prefix=null);
void PreloadTextures();
}
}

View File

@@ -0,0 +1,196 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Robust.Client.ResourceManagement
{
internal partial class ResourceCache
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public void PreloadTextures()
{
var sawmill = _logManager.GetSawmill("res.preload");
if (!_configurationManager.GetCVar(CVars.TexturePreloadingEnabled))
{
sawmill.Debug($"Skipping texture preloading due to CVar value.");
return;
}
PreloadTextures(sawmill);
PreloadRsis(sawmill);
}
private void PreloadTextures(ISawmill sawmill)
{
sawmill.Debug("Preloading textures...");
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var texList = ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
.Select(p => new TextureResource.LoadStepData {Path = p})
.ToArray();
Parallel.ForEach(texList, data =>
{
try
{
TextureResource.LoadPreTexture(this, data);
}
catch (Exception e)
{
// Mark failed loads as bad and skip them in the next few stages.
// Avoids any silly array resizing or similar.
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
});
foreach (var data in texList)
{
if (data.Bad)
continue;
try
{
TextureResource.LoadTexture(_clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}
var errors = 0;
foreach (var data in texList)
{
if (data.Bad)
{
errors += 1;
continue;
}
try
{
var texResource = new TextureResource();
texResource.LoadFinish(this, data);
resList[data.Path] = texResource;
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
errors += 1;
}
}
sawmill.Debug(
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
texList.Length,
errors,
sw.Elapsed);
}
private void PreloadRsis(ISawmill sawmill)
{
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<RSIResource>();
var rsiList = ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
.Select(c => c.Directory)
.Where(p => !resList.ContainsKey(p))
.Select(p => new RSIResource.LoadStepData {Path = p})
.ToArray();
Parallel.ForEach(rsiList, data =>
{
try
{
RSIResource.LoadPreTexture(this, data);
}
catch (Exception e)
{
// Mark failed loads as bad and skip them in the next few stages.
// Avoids any silly array resizing or similar.
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
});
foreach (var data in rsiList)
{
if (data.Bad)
continue;
try
{
RSIResource.LoadTexture(_clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}
Parallel.ForEach(rsiList, data =>
{
if (data.Bad)
return;
try
{
RSIResource.LoadPostTexture(data);
}
catch (Exception e)
{
data.Bad = true;
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
}
});
var errors = 0;
foreach (var data in rsiList)
{
if (data.Bad)
{
errors += 1;
continue;
}
try
{
var rsiRes = new RSIResource();
rsiRes.LoadFinish(this, data);
resList[data.Path] = rsiRes;
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
errors += 1;
}
}
sawmill.Debug(
"Preloaded {CountLoaded} RSIs ({CountErrored} errored) in {LoadTime}",
rsiList.Length,
errors,
sw.Elapsed);
}
}
}

View File

@@ -2,17 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.Log;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Newtonsoft.Json.Linq;
#if DEBUG
using NJsonSchema;
#endif
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -24,6 +20,14 @@ namespace Robust.Client.ResourceManagement
/// </summary>
public sealed class RSIResource : BaseResource
{
private static readonly float[] OneArray = {1};
private static readonly JsonSerializerOptions SerializerOptions =
new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
AllowTrailingCommas = true
};
public RSI RSI { get; private set; } = default!;
/// <summary>
@@ -38,64 +42,59 @@ namespace Robust.Client.ResourceManagement
public override void Load(IResourceCache cache, ResourcePath path)
{
var manifestPath = path / "meta.json";
string manifestContents;
var clyde = IoCManager.Resolve<IClyde>();
using (var manifestFile = cache.ContentFileRead(manifestPath))
using (var reader = new StreamReader(manifestFile))
{
manifestContents = reader.ReadToEnd();
}
var loadStepData = new LoadStepData {Path = path};
LoadPreTexture(cache, loadStepData);
#if DEBUG
if (RSISchema != null)
{
var errors = RSISchema.Validate(manifestContents);
if (errors.Count != 0)
{
Logger.Error($"Unable to load RSI from '{path}', {errors.Count} errors:");
// Load atlas.
LoadTexture(clyde, loadStepData);
foreach (var error in errors)
{
Logger.Error("{0}", error.ToString());
}
LoadPostTexture(loadStepData);
LoadFinish(cache, loadStepData);
throw new RSILoadException($"{errors.Count} errors while loading RSI. See console.");
}
}
#endif
loadStepData.AtlasSheet.Dispose();
}
// Ok schema validated just fine.
var manifestJson = JObject.Parse(manifestContents);
internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
{
loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
loadStepData.AtlasSheet,
loadStepData.Path.ToString());
}
var toAtlas = new List<(Image<Rgba32> src, Texture[][] output, int[][] indices, Vector2i[][] offsets, int totalFrameCount)>();
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
{
var metadata = LoadRsiMetadata(cache, data.Path);
var metaData = ParseMetaData(manifestJson);
var frameSize = metaData.Size;
var rsi = new RSI(frameSize, path);
var stateCount = metadata.States.Length;
var toAtlas = new StateReg[stateCount];
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>();
var frameSize = metadata.Size;
var rsi = new RSI(frameSize, data.Path);
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>(stateCount);
// Do every state.
foreach (var stateObject in metaData.States)
for (var index = 0; index < metadata.States.Length; index++)
{
// Load image from disk.
var texPath = path / (stateObject.StateId + ".png");
var stream = cache.ContentFileRead(texPath);
Image<Rgba32> image;
using (stream)
{
image = Image.Load<Rgba32>(stream);
}
var sheetSize = new Vector2i(image.Width, image.Height);
ref var reg = ref toAtlas[index];
if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0)
var stateObject = metadata.States[index];
// Load image from disk.
var texPath = data.Path / (stateObject.StateId + ".png");
using (var stream = cache.ContentFileRead(texPath))
{
reg.Src = Image.Load<Rgba32>(stream);
}
if (reg.Src.Width % frameSize.X != 0 || reg.Src.Height % frameSize.Y != 0)
{
throw new RSILoadException("State image size is not a multiple of the icon size.");
}
// Load all frames into a list so we can operate on it more sanely.
var frameCount = stateObject.Delays.Sum(delayList => delayList.Length);
reg.TotalFrameCount = stateObject.Delays.Sum(delayList => delayList.Length);
var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays);
@@ -108,29 +107,34 @@ namespace Robust.Client.ResourceManagement
callbackOffset[i] = new Vector2i[foldedIndices[0].Length];
}
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays, textures);
reg.Output = textures;
reg.Indices = foldedIndices;
reg.Offsets = callbackOffset;
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays,
textures);
rsi.AddState(state);
toAtlas.Add((image, textures, foldedIndices, callbackOffset, frameCount));
callbackOffsets[stateObject.StateId] = callbackOffset;
}
// Poorly hacked in texture atlas support here.
var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount);
var totalFrameCount = toAtlas.Sum(p => p.TotalFrameCount);
// Generate atlas.
var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount));
var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX);
using var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
var sheetIndex = 0;
foreach (var (src, _, _, _, frameCount) in toAtlas)
for (var index = 0; index < toAtlas.Length; index++)
{
ref var reg = ref toAtlas[index];
// Blit all the frames over.
for (var i = 0; i < frameCount; i++)
for (var i = 0; i < reg.TotalFrameCount; i++)
{
var srcWidth = (src.Width / frameSize.X);
var srcWidth = (reg.Src.Width / frameSize.X);
var srcColumn = i % srcWidth;
var srcRow = i / srcWidth;
var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y);
@@ -141,30 +145,49 @@ namespace Robust.Client.ResourceManagement
var srcBox = UIBox2i.FromDimensions(srcPos, frameSize);
src.Blit(srcBox, sheet, sheetPos);
reg.Src.Blit(srcBox, sheet, sheetPos);
}
sheetIndex += frameCount;
sheetIndex += reg.TotalFrameCount;
}
// Load atlas.
var texture = Texture.LoadFromImage(sheet, path.ToString());
for (var i = 0; i < toAtlas.Length; i++)
{
ref var reg = ref toAtlas[i];
reg.Src.Dispose();
}
data.Rsi = rsi;
data.AtlasSheet = sheet;
data.AtlasList = toAtlas;
data.FrameSize = frameSize;
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
}
internal static void LoadPostTexture(LoadStepData data)
{
var dimX = data.DimX;
var toAtlas = data.AtlasList;
var frameSize = data.FrameSize;
var texture = data.AtlasTexture;
var sheetOffset = 0;
foreach (var (_, output, indices, offsets, frameCount) in toAtlas)
for (var toAtlasIndex = 0; toAtlasIndex < toAtlas.Length; toAtlasIndex++)
{
for (var i = 0; i < indices.Length; i++)
ref var reg = ref toAtlas[toAtlasIndex];
for (var i = 0; i < reg.Indices.Length; i++)
{
var dirIndices = indices[i];
var dirOutput = output[i];
var dirOffsets = offsets[i];
var dirIndices = reg.Indices[i];
var dirOutput = reg.Output[i];
var dirOffsets = reg.Offsets[i];
for (var j = 0; j < dirIndices.Length; j++)
{
var index = sheetOffset + dirIndices[j];
var sheetColumn = index % dimensionX;
var sheetRow = index / dimensionX;
var sheetColumn = index % dimX;
var sheetRow = index / dimX;
var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y);
dirOffsets[j] = sheetPos;
@@ -172,22 +195,104 @@ namespace Robust.Client.ResourceManagement
}
}
sheetOffset += frameCount;
sheetOffset += reg.TotalFrameCount;
}
}
foreach (var (image, _, _, _, _) in toAtlas)
{
image.Dispose();
}
RSI = rsi;
internal void LoadFinish(IResourceCache cache, LoadStepData data)
{
RSI = data.Rsi;
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(path, this, sheet, callbackOffsets));
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
}
}
private static RsiMetadata LoadRsiMetadata(IResourceCache cache, ResourcePath path)
{
var manifestPath = path / "meta.json";
string manifestContents;
using (var manifestFile = cache.ContentFileRead(manifestPath))
using (var reader = new StreamReader(manifestFile))
{
manifestContents = reader.ReadToEnd();
}
// Ok schema validated just fine.
var manifestJson = JsonSerializer.Deserialize<RsiJsonMetadata>(manifestContents, SerializerOptions);
if (manifestJson == null)
throw new RSILoadException("Manifest JSON was null!");
var size = manifestJson.Size;
var states = new StateMetadata[manifestJson.States.Length];
for (var stateI = 0; stateI < manifestJson.States.Length; stateI++)
{
var stateObject = manifestJson.States[stateI];
var stateName = stateObject.Name;
RSI.State.DirectionType directions;
int dirValue;
if (stateObject.Directions is { } dirVal)
{
dirValue = dirVal;
directions = dirVal switch
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
};
}
else
{
dirValue = 1;
directions = RSI.State.DirectionType.Dir1;
}
// We can ignore selectors and flags for now,
// because they're not used yet!
// Get the lists of delays.
float[][] delays;
if (stateObject.Delays != null)
{
delays = stateObject.Delays;
if (delays.Length != dirValue)
{
throw new RSILoadException(
"DirectionsdirectionFramesList count does not match amount of delays specified.");
}
for (var i = 0; i < delays.Length; i++)
{
var delayList = delays[i];
if (delayList.Length == 0)
{
delays[i] = OneArray;
}
}
}
else
{
delays = new float[dirValue][];
// No delays specified, default to 1 frame per dir.
for (var i = 0; i < dirValue; i++)
{
delays[i] = OneArray;
}
}
states[stateI] = new StateMetadata(new RSI.StateId(stateName), directions, delays);
}
return new RsiMetadata(size, states);
}
/// <summary>
/// Folds a per-directional sets of animation delays
/// into an equivalent set of animation delays and indices that works for every direction.
@@ -336,109 +441,38 @@ namespace Robust.Client.ResourceManagement
return (floatDelays, arrayIndices);
}
internal static RsiMetadata ParseMetaData(JObject manifestJson)
internal sealed class LoadStepData
{
var size = manifestJson["size"]!.ToObject<Vector2i>();
var states = new List<StateMetadata>();
foreach (var stateObject in manifestJson["states"]!.Cast<JObject>())
{
var stateName = stateObject["name"]!.ToObject<string>()!;
RSI.State.DirectionType directions;
int dirValue;
if (stateObject.TryGetValue("directions", out var dirJToken))
{
dirValue= dirJToken.ToObject<int>();
directions = dirValue switch
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
};
}
else
{
dirValue = 1;
directions = RSI.State.DirectionType.Dir1;
}
// We can ignore selectors and flags for now,
// because they're not used yet!
// Get the lists of delays.
float[][] delays;
if (stateObject.TryGetValue("delays", out var delayToken))
{
delays = delayToken.ToObject<float[][]>()!;
if (delays.Length != dirValue)
{
throw new RSILoadException(
"DirectionsdirectionFramesList count does not match amount of delays specified.");
}
for (var i = 0; i < delays.Length; i++)
{
var delayList = delays[i];
if (delayList.Length == 0)
{
delays[i] = new float[] {1};
}
}
}
else
{
delays = new float[dirValue][];
// No delays specified, default to 1 frame per dir.
for (var i = 0; i < dirValue; i++)
{
delays[i] = new float[] {1};
}
}
states.Add(new StateMetadata(new RSI.StateId(stateName), directions, delays));
}
return new RsiMetadata(size, states);
public bool Bad;
public ResourcePath Path = default!;
public Image<Rgba32> AtlasSheet = default!;
public int DimX;
public StateReg[] AtlasList = default!;
public Vector2i FrameSize;
public Dictionary<RSI.StateId, Vector2i[][]> CallbackOffsets = default!;
public Texture AtlasTexture = default!;
public RSI Rsi = default!;
}
#if DEBUG
private static readonly JsonSchema? RSISchema = GetSchema();
private static JsonSchema? GetSchema()
internal struct StateReg
{
try
{
string schema;
using (var schemaStream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("Robust.Client.Graphics.RSI.RSISchema.json")!)
using (var schemaReader = new StreamReader(schemaStream))
{
schema = schemaReader.ReadToEnd();
}
return JsonSchema.FromJsonAsync(schema).Result;
}
catch (Exception e)
{
System.Console.WriteLine("Failed to load RSI JSON Schema!\n{0}", e);
return null;
}
public Image<Rgba32> Src;
public Texture[][] Output;
public int[][] Indices;
public Vector2i[][] Offsets;
public int TotalFrameCount;
}
#endif
internal sealed class RsiMetadata
{
public RsiMetadata(Vector2i size, List<StateMetadata> states)
public RsiMetadata(Vector2i size, StateMetadata[] states)
{
Size = size;
States = states;
}
public Vector2i Size { get; }
public List<StateMetadata> States { get; }
public StateMetadata[] States { get; }
}
internal sealed class StateMetadata
@@ -466,6 +500,17 @@ namespace Robust.Client.ResourceManagement
public float[][] Delays { get; }
}
// To be directly deserialized.
[UsedImplicitly]
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States)
{
}
[UsedImplicitly]
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays)
{
}
}
[Serializable]

View File

@@ -1,7 +1,8 @@
using System.IO;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -9,41 +10,52 @@ using YamlDotNet.RepresentationModel;
namespace Robust.Client.ResourceManagement
{
public class TextureResource : BaseResource
public sealed class TextureResource : BaseResource
{
public const float ClickThreshold = 0.25f;
private OwnedTexture _texture = default!;
public override ResourcePath Fallback => new("/Textures/noSprite.png");
public override ResourcePath? Fallback => new("/Textures/noSprite.png");
public Texture Texture { get; private set; } = default!;
public Texture Texture => _texture;
public override void Load(IResourceCache cache, ResourcePath path)
{
if (!cache.TryContentFileRead(path, out var stream))
{
throw new FileNotFoundException("Content file does not exist for texture");
}
var clyde = IoCManager.Resolve<IClyde>();
using (stream)
{
// Primarily for tracking down iCCP sRGB errors in the image files.
Logger.DebugS("res.tex", $"Loading texture {path}.");
var data = new LoadStepData {Path = path};
var loadParameters = _tryLoadTextureParameters(cache, path) ?? TextureLoadParameters.Default;
var manager = IoCManager.Resolve<IClyde>();
using var image = Image.Load<Rgba32>(stream);
Texture = manager.LoadTextureFromImage(image, path.ToString(), loadParameters);
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this));
}
}
LoadPreTexture(cache, data);
LoadTexture(clyde, data);
LoadFinish(cache, data);
}
private static TextureLoadParameters? _tryLoadTextureParameters(IResourceCache cache, ResourcePath path)
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
{
using (var stream = cache.ContentFileRead(data.Path))
{
data.Image = Image.Load<Rgba32>(stream);
}
data.LoadParameters = TryLoadTextureParameters(cache, data.Path) ?? TextureLoadParameters.Default;
}
internal static void LoadTexture(IClyde clyde, LoadStepData data)
{
data.Texture = clyde.LoadTextureFromImage(data.Image, data.Path.ToString(), data.LoadParameters);
}
internal void LoadFinish(IResourceCache cache, LoadStepData data)
{
_texture = data.Texture;
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(data.Path, data.Image, this));
}
data.Image.Dispose();
}
private static TextureLoadParameters? TryLoadTextureParameters(IResourceCache cache, ResourcePath path)
{
var metaPath = path.WithName(path.Filename + ".yml");
if (cache.TryContentFileRead(metaPath, out var stream))
@@ -70,6 +82,38 @@ namespace Robust.Client.ResourceManagement
return null;
}
public override void Reload(IResourceCache cache, ResourcePath path, CancellationToken ct = default)
{
var clyde = IoCManager.Resolve<IClyde>();
var data = new LoadStepData {Path = path};
LoadPreTexture(cache, data);
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
{
// Dimensions match, rewrite texture in place.
_texture.SetSubImage(Vector2i.Zero, data.Image);
}
else
{
// Dimensions do not match, make new texture.
_texture.Dispose();
LoadTexture(clyde, data);
_texture = data.Texture;
}
data.Image.Dispose();
}
internal sealed class LoadStepData
{
public ResourcePath Path = default!;
public Image<Rgba32> Image = default!;
public TextureLoadParameters LoadParameters;
public OwnedTexture Texture = default!;
public bool Bad;
}
// TODO: Due to a bug in Roslyn, NotNullIfNotNullAttribute doesn't work.
// So this can't work with both nullables and non-nullables at the same time.
// I decided to only have it work with non-nullables as such.

View File

@@ -14,7 +14,6 @@
<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="SixLabors.ImageSharp" Version="1.0.3" />
@@ -39,9 +38,6 @@
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Graphics\RSI\RSISchema.json" Condition="'$(Configuration)' == 'Debug'">
<LogicalName>Robust.Client.Graphics.RSI.RSISchema.json</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
</ItemGroup>

View File

@@ -9,9 +9,7 @@ namespace Robust.Client.State
State CurrentState { get; }
void RequestStateChange<T>() where T : State, new();
void Update(FrameEventArgs e);
void FrameUpdate(FrameEventArgs e);
void FormResize();
void RequestStateChange(Type type);
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Timing;
using Robust.Shared.Timing;
namespace Robust.Client.State
{
@@ -14,16 +14,6 @@ namespace Robust.Client.State
/// </summary>
public abstract void Shutdown();
/// <summary>
/// Update the contents of this screen.
/// </summary>
public virtual void Update(FrameEventArgs e) { }
public virtual void FrameUpdate(FrameEventArgs e) { }
/// <summary>
/// The screen has changed size, usually from resizing window. This is called automatically right after Startup.
/// </summary>
public virtual void FormResize() { }
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Log;
using Robust.Shared.Log;
using System;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
@@ -17,21 +17,11 @@ namespace Robust.Client.State
CurrentState = new DefaultState();
}
public void Update(FrameEventArgs e)
{
CurrentState?.Update(e);
}
public void FrameUpdate(FrameEventArgs e)
{
CurrentState?.FrameUpdate(e);
}
public void FormResize()
{
CurrentState?.FormResize();
}
public void RequestStateChange<T>() where T : State, new()
{
RequestStateChange(typeof(T));

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -817,25 +817,7 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Called when the size of the control changes.
/// </summary>
protected virtual void Resized()
{
}
internal void DoUpdate(FrameEventArgs args)
{
Update(args);
foreach (var child in Children)
{
child.DoUpdate(args);
}
}
/// <summary>
/// This is called every process frame.
/// </summary>
protected virtual void Update(FrameEventArgs args)
{
}
protected virtual void Resized() { }
internal void DoFrameUpdate(FrameEventArgs args)
{

View File

@@ -385,16 +385,16 @@ namespace Robust.Client.UserInterface.Controls
var offsetY = (int) (box.Height - font.GetHeight(UIScale)) / 2;
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
foreach (var chr in text)
foreach (var rune in text.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
if (!(baseLine.X < box.Left || baseLine.X + metrics.Advance > box.Right))
{
font.DrawChar(handle, chr, baseLine, UIScale, color);
font.DrawChar(handle, rune, baseLine, UIScale, color);
}
baseLine += (metrics.Advance, 0);

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using Robust.Client.Graphics;
using Robust.Shared.Animations;
using Robust.Shared.Maths;
@@ -180,15 +181,15 @@ namespace Robust.Client.UserInterface.Controls
var baseLine = CalcBaseline();
foreach (var chr in _text)
foreach (var rune in _text.EnumerateRunes())
{
if (chr == '\n')
if (rune == new Rune('\n'))
{
newlines += 1;
baseLine = CalcBaseline();
}
var advance = font.DrawChar(handle, chr, baseLine, UIScale, actualFontColor);
var advance = font.DrawChar(handle, rune, baseLine, UIScale, actualFontColor);
baseLine += (advance, 0);
}
}
@@ -252,16 +253,16 @@ namespace Robust.Client.UserInterface.Controls
var font = ActualFont;
var height = font.GetHeight(UIScale);
foreach (var chr in _text)
foreach (var rune in _text.EnumerateRunes())
{
if (chr == '\n')
if (rune == new Rune('\n'))
{
_cachedTextWidths.Add(0);
height += font.GetLineHeight(UIScale);
}
else
{
var metrics = font.GetCharMetrics(chr, UIScale);
var metrics = font.GetCharMetrics(rune, UIScale);
if (metrics == null)
{
continue;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Input;
@@ -23,6 +24,7 @@ namespace Robust.Client.UserInterface.Controls
public const string StyleClassLineEditNotEditable = "notEditable";
public const string StylePseudoClassPlaceholder = "placeholder";
// It is assumed that these two positions are NEVER inside a surrogate pair in the text buffer.
private int _cursorPosition;
private int _selectionStart;
private string _text = "";
@@ -126,7 +128,11 @@ namespace Robust.Client.UserInterface.Controls
get => _cursorPosition;
set
{
_cursorPosition = MathHelper.Clamp(value, 0, _text.Length);
var clamped = MathHelper.Clamp(value, 0, _text.Length);
if (_text.Length != 0 && _text.Length != clamped && !Rune.TryGetRuneAt(_text, clamped, out _))
throw new ArgumentException("Cannot set cursor inside surrogate pair.");
_cursorPosition = clamped;
_selectionStart = _cursorPosition;
}
}
@@ -134,7 +140,14 @@ namespace Robust.Client.UserInterface.Controls
public int SelectionStart
{
get => _selectionStart;
set => _selectionStart = MathHelper.Clamp(value, 0, _text.Length);
set
{
var clamped = MathHelper.Clamp(value, 0, _text.Length);
if (_text.Length != 0 && _text.Length != clamped && !Rune.TryGetRuneAt(_text, clamped, out _))
throw new ArgumentException("Cannot set cursor inside surrogate pair.");
_selectionStart = clamped;
}
}
public int SelectionLower => Math.Min(_selectionStart, _cursorPosition);
@@ -275,7 +288,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
InsertAtCursor(((char) args.CodePoint).ToString());
InsertAtCursor(args.AsRune.ToString());
}
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
@@ -302,8 +315,16 @@ namespace Robust.Client.UserInterface.Controls
}
else if (_cursorPosition != 0)
{
_text = _text.Remove(_cursorPosition - 1, 1);
_cursorPosition -= 1;
var remPos = _cursorPosition - 1;
var remAmt = 1;
// If this is a low surrogate remove two chars to remove the whole pair.
if (char.IsLowSurrogate(_text[remPos]))
{
remPos -= 1;
remAmt = 2;
}
_text = _text.Remove(remPos, remAmt);
_cursorPosition -= remAmt;
changed = true;
}
@@ -330,7 +351,10 @@ namespace Robust.Client.UserInterface.Controls
}
else if (_cursorPosition < _text.Length)
{
_text = _text.Remove(_cursorPosition, 1);
var remAmt = 1;
if (char.IsHighSurrogate(_text[_cursorPosition]))
remAmt = 2;
_text = _text.Remove(_cursorPosition, remAmt);
changed = true;
}
@@ -352,10 +376,7 @@ namespace Robust.Client.UserInterface.Controls
}
else
{
if (_cursorPosition != 0)
{
_cursorPosition -= 1;
}
ShiftCursorLeft();
_selectionStart = _cursorPosition;
}
@@ -370,10 +391,7 @@ namespace Robust.Client.UserInterface.Controls
}
else
{
if (_cursorPosition != _text.Length)
{
_cursorPosition += 1;
}
ShiftCursorRight();
_selectionStart = _cursorPosition;
}
@@ -404,19 +422,13 @@ namespace Robust.Client.UserInterface.Controls
}
else if (args.Function == EngineKeyFunctions.TextCursorSelectLeft)
{
if (_cursorPosition != 0)
{
_cursorPosition -= 1;
}
ShiftCursorLeft();
args.Handle();
}
else if (args.Function == EngineKeyFunctions.TextCursorSelectRight)
{
if (_cursorPosition != _text.Length)
{
_cursorPosition += 1;
}
ShiftCursorRight();
args.Handle();
}
@@ -525,6 +537,32 @@ namespace Robust.Client.UserInterface.Controls
// Reset this so the cursor is always visible immediately after a keybind is pressed.
_resetCursorBlink();
void ShiftCursorLeft()
{
if (_cursorPosition == 0)
return;
_cursorPosition -= 1;
if (char.IsLowSurrogate(_text[_cursorPosition]))
_cursorPosition -= 1;
}
void ShiftCursorRight()
{
if (_cursorPosition == _text.Length)
return;
_cursorPosition += 1;
// Before you confuse yourself on "shouldn't this be high surrogate since shifting left checks low"
// (Because yes, I did myself too a week after writing it)
// char.IsLowSurrogate(_text[_cursorPosition]) means "is the cursor between a surrogate pair"
// because we ALREADY moved.
if (_cursorPosition != _text.Length && char.IsLowSurrogate(_text[_cursorPosition]))
_cursorPosition += 1;
}
}
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
@@ -555,11 +593,11 @@ namespace Robust.Client.UserInterface.Controls
var index = 0;
var chrPosX = contentBox.Left - _drawOffset;
var lastChrPostX = contentBox.Left - _drawOffset;
foreach (var chr in _text)
foreach (var rune in _text.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
index += 1;
index += rune.Utf16SequenceLength;
continue;
}
@@ -570,7 +608,7 @@ namespace Robust.Client.UserInterface.Controls
lastChrPostX = chrPosX;
chrPosX += metrics.Advance;
index += 1;
index += rune.Utf16SequenceLength;
if (chrPosX > contentBox.Right)
{
@@ -586,6 +624,9 @@ namespace Robust.Client.UserInterface.Controls
if (index > 0 && distanceRight > distanceLeft)
{
index -= 1;
if (char.IsLowSurrogate(_text[index]))
index -= 1;
}
return index;
@@ -652,60 +693,87 @@ namespace Robust.Client.UserInterface.Controls
}
// Approach for NextWordPosition and PrevWordPosition taken from Avalonia.
private int NextWordPosition(string str, int cursor)
internal static int NextWordPosition(string str, int cursor)
{
if (cursor >= str.Length)
{
return str.Length;
}
var charClass = GetCharClass(str[cursor]);
var charClass = GetCharClass(Rune.GetRuneAt(str, cursor));
var i = cursor;
for (; i < str.Length && GetCharClass(str[i]) == charClass; i++)
{
}
for (; i < str.Length && GetCharClass(str[i]) == CharClass.Whitespace; i++)
{
}
IterForward(charClass);
IterForward(CharClass.Whitespace);
return i;
void IterForward(CharClass cClass)
{
while (i < str.Length)
{
var rune = Rune.GetRuneAt(str, i);
if (GetCharClass(rune) != cClass)
break;
i += rune.Utf16SequenceLength;
}
}
}
private int PrevWordPosition(string str, int cursor)
internal static int PrevWordPosition(string str, int cursor)
{
if (cursor == 0)
{
return 0;
}
var charClass = GetCharClass(str[cursor - 1]);
var startRune = GetRuneBackwards(str, cursor - 1);
var charClass = GetCharClass(startRune);
var i = cursor;
for (; i > 0 && GetCharClass(str[i - 1]) == charClass; i--)
{
}
IterBackward();
if (charClass == CharClass.Whitespace)
{
charClass = GetCharClass(str[i - 1]);
for (; i > 0 && GetCharClass(str[i - 1]) == charClass; i--)
{
}
if (!Rune.TryGetRuneAt(str, i - 1, out var midRune))
midRune = Rune.GetRuneAt(str, i - 2);
charClass = GetCharClass(midRune);
IterBackward();
}
return i;
void IterBackward()
{
while (i > 0)
{
var rune = GetRuneBackwards(str, i - 1);
if (GetCharClass(rune) != charClass)
break;
i -= rune.Utf16SequenceLength;
}
}
static Rune GetRuneBackwards(string str, int i)
{
return Rune.TryGetRuneAt(str, i, out var rune) ? rune : Rune.GetRuneAt(str, i - 1);
}
}
private CharClass GetCharClass(char chr)
internal static CharClass GetCharClass(Rune rune)
{
if (char.IsWhiteSpace(chr))
if (Rune.IsWhiteSpace(rune))
{
return CharClass.Whitespace;
}
if (char.IsLetterOrDigit(chr))
if (Rune.IsLetterOrDigit(rune))
{
return CharClass.AlphaNumeric;
}
@@ -713,7 +781,7 @@ namespace Robust.Client.UserInterface.Controls
return CharClass.Other;
}
private enum CharClass : byte
internal enum CharClass : byte
{
Other,
AlphaNumeric,
@@ -767,7 +835,7 @@ namespace Robust.Client.UserInterface.Controls
var posX = 0;
var actualCursorPosition = 0;
var actualSelectionStartPosition = 0;
foreach (var chr in renderedText)
foreach (var chr in renderedText.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
{
@@ -776,7 +844,7 @@ namespace Robust.Client.UserInterface.Controls
}
posX += metrics.Advance;
count += 1;
count += chr.Utf16SequenceLength;
if (count == _master._cursorPosition)
{
@@ -816,9 +884,9 @@ namespace Robust.Client.UserInterface.Controls
var baseLine = (-drawOffset, offsetY + font.GetAscent(UIScale)) +
contentBox.TopLeft;
foreach (var chr in renderedText)
foreach (var rune in renderedText.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
@@ -832,7 +900,7 @@ namespace Robust.Client.UserInterface.Controls
// Make sure we're not off the left edge of the box.
if (baseLine.X + metrics.BearingX + metrics.Width >= contentBox.Left)
{
font.DrawChar(handle, chr, baseLine, UIScale, renderedTextColor);
font.DrawChar(handle, rune, baseLine, UIScale, renderedTextColor);
}
baseLine += (metrics.Advance, 0);

View File

@@ -163,9 +163,9 @@ namespace Robust.Client.UserInterface.Controls
var titleLength = 0;
// Get string length.
foreach (var chr in title)
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
@@ -196,14 +196,14 @@ namespace Robust.Client.UserInterface.Controls
var baseLine = new Vector2(0, font.GetAscent(UIScale)) + contentBox.TopLeft;
foreach (var chr in title)
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
font.DrawChar(handle, chr, baseLine, UIScale, active ? fontColorActive : fontColorInactive);
font.DrawChar(handle, rune, baseLine, UIScale, active ? fontColorActive : fontColorInactive);
baseLine += new Vector2(metrics.Advance, 0);
}
@@ -295,9 +295,9 @@ namespace Robust.Client.UserInterface.Controls
var titleLength = 0;
// Get string length.
foreach (var chr in title)
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}

View File

@@ -219,9 +219,9 @@ namespace Robust.Client.UserInterface.Controls
{
var offset = itemSelected.GetContentOffset(Vector2.Zero);
var baseLine = offset + (hOffset, vOffset + font.GetAscent(UIScale));
foreach (var chr in item.Text)
foreach (var rune in item.Text.EnumerateRunes())
{
baseLine += (font.DrawChar(handle, chr, baseLine, UIScale, Color.White), 0);
baseLine += (font.DrawChar(handle, rune, baseLine, UIScale, Color.White), 0);
}
}

View File

@@ -2,12 +2,14 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Robust.Client.Console;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.ContentPack;
using Robust.Shared.Input;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -301,43 +303,95 @@ namespace Robust.Client.UserInterface.CustomControls
private async void _loadHistoryFromDisk()
{
CommandBar.ClearHistory();
Stream stream;
try
var sawmill = Logger.GetSawmill("dbgconsole");
var data = await Task.Run(async () =>
{
stream = _resourceManager.UserData.OpenRead(HistoryPath);
}
catch (FileNotFoundException)
{
// Nada, nothing to load in that case.
return;
}
try
{
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
Stream? stream = null;
for (var i = 0; i < 3; i++)
{
var data = JsonConvert.DeserializeObject<List<string>>(await reader.ReadToEndAsync());
CommandBar.ClearHistory();
CommandBar.History.AddRange(data);
CommandBar.HistoryIndex = CommandBar.History.Count;
try
{
stream = _resourceManager.UserData.OpenRead(HistoryPath);
break;
}
catch (FileNotFoundException)
{
// Nada, nothing to load in that case.
return null;
}
catch (IOException)
{
// File locked probably??
await Task.Delay(10);
}
}
}
finally
{
stream?.Dispose();
}
if (stream == null)
{
sawmill.Warning("Failed to load debug console history!");
return null;
}
try
{
return await JsonSerializer.DeserializeAsync<string[]>(stream);
}
catch (Exception e)
{
sawmill.Warning("Failed to load debug console history due to exception!\n{e}");
return null;
}
finally
{
// ReSharper disable once MethodHasAsyncOverload
stream.Dispose();
}
});
if (data == null)
return;
CommandBar.ClearHistory();
CommandBar.History.AddRange(data);
CommandBar.HistoryIndex = CommandBar.History.Count;
}
private void _flushHistoryToDisk()
private async void _flushHistoryToDisk()
{
using (var stream = _resourceManager.UserData.Create(HistoryPath))
using (var writer = new StreamWriter(stream, EncodingHelpers.UTF8))
CommandBar.HistoryIndex = CommandBar.History.Count;
var sawmill = Logger.GetSawmill("dbgconsole");
var newHistory = JsonSerializer.Serialize(CommandBar.History);
await Task.Run(async () =>
{
var data = JsonConvert.SerializeObject(CommandBar.History);
CommandBar.HistoryIndex = CommandBar.History.Count;
writer.Write(data);
}
Stream? stream = null;
for (var i = 0; i < 3; i++)
{
try
{
stream = _resourceManager.UserData.Create(HistoryPath);
break;
}
catch (IOException)
{
// Probably locking.
await Task.Delay(10);
}
}
if (stream == null)
{
sawmill.Warning("Failed to save debug console history!");
return;
}
// ReSharper disable once UseAwaitUsing
using var writer = new StreamWriter(stream, EncodingHelpers.UTF8);
// ReSharper disable once MethodHasAsyncOverload
writer.Write(newHistory);
});
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -45,9 +45,9 @@ namespace Robust.Client.UserInterface.CustomControls
Visible = false;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if ((_gameTiming.RealTime - _lastUpdate).Seconds < 1 || !VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
@@ -51,9 +51,9 @@ namespace Robust.Client.UserInterface.CustomControls
MouseFilter = contents.MouseFilter = MouseFilterMode.Ignore;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if ((GameTiming.RealTime - LastUpdate).Seconds < 1 || !VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using Robust.Client.GameStates;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
@@ -36,9 +36,9 @@ namespace Robust.Client.UserInterface.CustomControls
HorizontalAlignment = HAlignment.Left;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if (!VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -67,6 +67,7 @@ namespace Robust.Client.UserInterface.CustomControls
Title = Loc.GetString("Entity Spawn Panel");
SetSize = (250, 300);
MinSize = (250, 200);

View File

@@ -1,4 +1,4 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -17,7 +17,7 @@ namespace Robust.Client.UserInterface.CustomControls
ShadowOffsetYOverride = 1;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
if (!VisibleInTree)
{

View File

@@ -1,4 +1,6 @@
using Robust.Client.Graphics;
using System;
using System.Collections;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -31,9 +33,11 @@ namespace Robust.Client.UserInterface.CustomControls
// We keep track of frame times in a ring buffer.
private readonly float[] _frameTimes = new float[TrackedFrames];
private readonly BitArray _gcMarkers = new(TrackedFrames);
// Position of the last frame in the ring buffer.
private int _frameIndex;
private int _lastGCCount;
public FrameGraph(IGameTiming gameTiming)
{
@@ -49,14 +53,21 @@ namespace Robust.Client.UserInterface.CustomControls
protected override void FrameUpdate(FrameEventArgs args)
{
var gcCount = GC.CollectionCount(0);
_frameTimes[_frameIndex] = (float)_gameTiming.RealFrameTime.TotalSeconds;
_gcMarkers[_frameIndex] = gcCount > _lastGCCount;
_frameIndex = (_frameIndex + 1) % TrackedFrames;
_lastGCCount = gcCount;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
Span<Vector2> triangle = stackalloc Vector2[3];
float maxHeight = 0;
for (var i = 0; i < _frameTimes.Length; i++)
{
@@ -88,6 +99,16 @@ namespace Robust.Client.UserInterface.CustomControls
color = Color.Lime;
}
handle.DrawRect(rect, color);
var gc = _gcMarkers[currentFrameIndex];
if (gc)
{
triangle[0] = (rect.Left, 0);
triangle[1] = (rect.Right, 0);
triangle[2] = (rect.Center.X, 5);
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, triangle, Color.LightBlue);
}
}
}
}

View File

@@ -91,7 +91,7 @@ namespace Robust.Client.UserInterface.CustomControls
// Prevent window headers from getting off screen due to game window resizes.
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
var (spaceX, spaceY) = Parent!.Size;
if (Position.Y > spaceY)

View File

@@ -17,8 +17,6 @@ namespace Robust.Client.UserInterface
void Initialize();
void InitializeTesting();
void Update(FrameEventArgs args);
void FrameUpdate(FrameEventArgs args);
/// <returns>True if a UI control was hit and the key event should not pass through past UI.</returns>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -65,7 +66,7 @@ namespace Robust.Client.UserInterface
var wordSizePixels = 0;
// The horizontal position of the text cursor.
var posX = 0;
var lastChar = 'A';
var lastRune = new Rune('A');
// If a word is larger than maxSizeX, we split it.
// We need to keep track of some data to split it into two words.
(int breakIndex, int wordSizePixels)? forceSplitData = null;
@@ -83,14 +84,14 @@ namespace Robust.Client.UserInterface
var text = tagText.Text;
// And go over every character.
for (var i = 0; i < text.Length; i++, breakIndexCounter++)
foreach (var rune in text.EnumerateRunes())
{
var chr = text[i];
breakIndexCounter += 1;
if (IsWordBoundary(lastChar, chr) || chr == '\n')
if (IsWordBoundary(lastRune, rune) || rune == new Rune('\n'))
{
// Word boundary means we know where the word ends.
if (posX > maxSizeX && lastChar != ' ')
if (posX > maxSizeX && lastRune != new Rune(' '))
{
DebugTools.Assert(wordStartBreakIndex.HasValue,
"wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line.");
@@ -109,22 +110,22 @@ namespace Robust.Client.UserInterface
forceSplitData = null;
// Just manually handle newlines.
if (chr == '\n')
if (rune == new Rune('\n'))
{
LineBreaks.Add(breakIndexCounter);
Height += font.GetLineHeight(uiScale);
maxUsedWidth = Math.Max(maxUsedWidth, posX);
posX = 0;
lastChar = chr;
lastRune = rune;
wordStartBreakIndex = null;
continue;
}
}
// Uh just skip unknown characters I guess.
if (!font.TryGetCharMetrics(chr, uiScale, out var metrics))
if (!font.TryGetCharMetrics(rune, uiScale, out var metrics))
{
lastChar = chr;
lastRune = rune;
continue;
}
@@ -165,7 +166,7 @@ namespace Robust.Client.UserInterface
}
}
lastChar = chr;
lastRune = rune;
}
}
@@ -185,7 +186,7 @@ namespace Robust.Client.UserInterface
Logger.Error("wordStartBreakIndex: null (duh)");
Logger.Error($"wordSizePixels: {wordSizePixels}");
Logger.Error($"posX: {posX}");
Logger.Error($"lastChar: {lastChar}");
Logger.Error($"lastChar: {lastRune}");
Logger.Error($"forceSplitData: {forceSplitData}");
Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
@@ -247,9 +248,10 @@ namespace Robust.Client.UserInterface
case FormattedMessage.TagText tagText:
{
var text = tagText.Text;
for (var i = 0; i < text.Length; i++, globalBreakCounter++)
foreach (var rune in text.EnumerateRunes())
{
var chr = text[i];
globalBreakCounter += 1;
if (lineBreakIndex < LineBreaks.Count &&
LineBreaks[lineBreakIndex] == globalBreakCounter)
{
@@ -257,7 +259,7 @@ namespace Robust.Client.UserInterface
lineBreakIndex += 1;
}
var advance = font.DrawChar(handle, chr, baseLine, uiScale, currentColorTag.Color);
var advance = font.DrawChar(handle, rune, baseLine, uiScale, currentColorTag.Color);
baseLine += new Vector2(advance, 0);
}
@@ -268,9 +270,9 @@ namespace Robust.Client.UserInterface
}
[Pure]
private static bool IsWordBoundary(char a, char b)
private static bool IsWordBoundary(Rune a, Rune b)
{
return a == ' ' || b == ' ' || a == '-' || b == '-';
return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-');
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Console;
using Robust.Client.Graphics;
@@ -133,6 +133,7 @@ namespace Robust.Client.UserInterface
QueueMeasureUpdate(RootControl);
_displayManager.OnWindowResized += args => _updateRootSize();
_displayManager.OnWindowScaleChanged += UpdateUIScale;
StateRoot = new LayoutContainer
{
@@ -174,15 +175,9 @@ namespace Robust.Client.UserInterface
_initializeCommon();
}
public void Update(FrameEventArgs args)
{
RootControl.DoUpdate(args);
}
/// <inheritdoc />
public void FrameUpdate(FrameEventArgs args)
{
RootControl.DoFrameUpdate(args);
// Process queued style & layout updates.
while (_styleUpdateQueue.Count != 0)
{
@@ -220,6 +215,8 @@ namespace Robust.Client.UserInterface
RunArrange(control);
}
RootControl.DoFrameUpdate(args);
// count down tooltip delay if we're not showing one yet and
// are hovering the mouse over a control without moving it
if (_tooltipDelay != null && !showingTooltip)
@@ -833,7 +830,13 @@ namespace Robust.Client.UserInterface
private void _uiScaleChanged(float newValue)
{
UIScale = newValue == 0f ? DefaultUIScale : newValue;
UpdateUIScale();
}
private void UpdateUIScale()
{
var newVal = _configurationManager.GetCVar(CVars.DisplayUIScale);
UIScale = newVal == 0f ? DefaultUIScale : newVal;
if (RootControl == null)
{

View File

@@ -42,19 +42,20 @@ namespace Robust.Client.Utility
{
var dstSpan = destination.GetPixelSpan();
var dstWidth = destination.Width;
var srcHeight = sourceRect.Height;
var srcWidth = sourceRect.Width;
var (ox, oy) = destinationOffset;
for (var y = 0; y < sourceRect.Height; y++)
for (var y = 0; y < srcHeight; y++)
{
var sourceRowOffset = sourceWidth * (y + sourceRect.Top) + sourceRect.Left;
var destRowOffset = dstWidth * (y + oy) + ox;
for (var x = 0; x < sourceRect.Width; x++)
{
var pixel = source[x + sourceRowOffset];
dstSpan[x + destRowOffset] = pixel;
}
var srcRow = source[sourceRowOffset..(sourceRowOffset + srcWidth)];
var dstRow = dstSpan[destRowOffset..(destRowOffset + srcWidth)];
srcRow.CopyTo(dstRow);
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -41,6 +41,40 @@ namespace Robust.Client.Utility
return specifier.RsiStateLike().Default;
}
public static float[] FrameDelays(this SpriteSpecifier specifier) {
var resc = IoCManager.Resolve<IResourceCache>();
switch (specifier) {
case SpriteSpecifier.Rsi rsi:
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi)) {
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state)) {
return state.Delays;
}
}
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
return new float[0];
default:
throw new NotImplementedException();
}
}
public static Texture[] FrameArr(this SpriteSpecifier specifier) {
var resc = IoCManager.Resolve<IResourceCache>();
switch (specifier) {
case SpriteSpecifier.Rsi rsi:
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi)) {
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state)) {
return state.Icons[0];
}
}
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
return new Texture[0];
default:
throw new NotImplementedException();
}
}
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike();

View File

@@ -0,0 +1,149 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.ViewVariables.Editors
{
public class VVPropEditorIPrototype<T> : VVPropEditor
{
private object? _localValue;
private ViewVariablesObjectSelector? _selector;
private ViewVariablesAddWindow? _addWindow;
private LineEdit _lineEdit = new();
protected override Control MakeUI(object? value)
{
_localValue = value;
var hbox = new HBoxContainer() { HorizontalExpand = true };
_lineEdit = new LineEdit()
{
HorizontalExpand = true,
HorizontalAlignment = Control.HAlignment.Stretch,
PlaceHolder = "Prototype ID",
Text = value switch
{
IPrototype prototype => prototype.ID,
ViewVariablesBlobMembers.PrototypeReferenceToken token => token.ID,
_ => string.Empty
},
Editable = !ReadOnly
};
_lineEdit.OnTextEntered += ev =>
{
SetNewValue(ev.Text);
};
var list = new Button() { Text = "List", Disabled = ReadOnly };
var inspect = new Button() { Text = "Inspect" };
list.OnPressed += OnListButtonPressed;
inspect.OnPressed += OnInspectButtonPressed;
hbox.AddChild(_lineEdit);
hbox.AddChild(list);
hbox.AddChild(inspect);
return hbox;
}
private async void OnListButtonPressed(BaseButton.ButtonEventArgs obj)
{
_addWindow?.Dispose();
if (_selector == null)
{
ClientSideWindowList();
}
else
{
await ServerSideWindowList();
}
}
private void ClientSideWindowList()
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
if (!protoMan.TryGetVariantFrom(typeof(T), out var variant)) return;
var list = new List<string>();
foreach (var prototype in protoMan.EnumeratePrototypes(variant))
{
list.Add(prototype.ID);
}
_addWindow = new ViewVariablesAddWindow(list, "Set Prototype [C]");
_addWindow.AddButtonPressed += OnAddButtonPressed;
_addWindow.OpenCentered();
}
private async Task ServerSideWindowList()
{
if (_selector is not ViewVariablesSessionRelativeSelector selector
|| _localValue is not ViewVariablesBlobMembers.PrototypeReferenceToken protoToken) return;
var vvm = IoCManager.Resolve<IViewVariablesManagerInternal>();
if (!vvm.TryGetSession(selector.SessionId, out var session)) return;
var prototypeBlob = await vvm.RequestData<ViewVariablesBlobAllPrototypes>(session, new ViewVariablesRequestAllPrototypes(protoToken.Variant));
_addWindow = new ViewVariablesAddWindow(prototypeBlob.Prototypes, "Set Prototype [S]");
_addWindow.AddButtonPressed += OnAddButtonPressed;
_addWindow.OpenCentered();
}
private void OnAddButtonPressed(ViewVariablesAddWindow.AddButtonPressedEventArgs obj)
{
_lineEdit.Text = obj.Entry;
_addWindow?.Dispose();
SetNewValue(obj.Entry);
}
private void OnInspectButtonPressed(BaseButton.ButtonEventArgs obj)
{
var vvm = IoCManager.Resolve<IViewVariablesManager>();
if(_selector != null)
vvm.OpenVV(_selector);
else if (_localValue != null)
vvm.OpenVV(_localValue);
}
private void SetNewValue(string text)
{
// Remote variable, therefore we send a new PrototypeReferenceToken.
if (_selector != null)
{
if(_localValue is ViewVariablesBlobMembers.PrototypeReferenceToken token)
ValueChanged(new ViewVariablesBlobMembers.PrototypeReferenceToken()
{
Stringified = token.Variant, Variant = token.Variant, ID = text,
}, true);
return;
}
// Local variable, therefore the type T should be valid.
var protoMan = IoCManager.Resolve<IPrototypeManager>();
if(protoMan.TryIndex(typeof(T), text, out var prototype))
ValueChanged(prototype, false);
return;
}
public override void WireNetworkSelector(uint sessionId, object[] selectorChain)
{
_selector = new ViewVariablesSessionRelativeSelector(sessionId, selectorChain);
}
}
}

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Serialization;
namespace Robust.Client.ViewVariables.Editors
{
internal sealed class VVPropEditorISelfSerialzable<T> : VVPropEditor where T : ISelfSerialize
internal sealed class VVPropEditorISelfSerializable<T> : VVPropEditor where T : ISelfSerialize
{
protected override Control MakeUI(object? value)
{

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