mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5efd5ed0 | ||
|
|
e1110eadb4 | ||
|
|
06ace83a73 | ||
|
|
cd01ca924b | ||
|
|
38ad8ce132 | ||
|
|
ee440c2df9 | ||
|
|
32f3c863fb | ||
|
|
b00e0bef5a | ||
|
|
0a09b27918 | ||
|
|
c9f6a4e32a | ||
|
|
b205a14f69 | ||
|
|
d5f3292e0a | ||
|
|
561e4b330e | ||
|
|
36a5d102ff | ||
|
|
b9c39e0953 | ||
|
|
ad4c8be132 | ||
|
|
988cbf9a87 | ||
|
|
e26512001a | ||
|
|
8e97982f1e | ||
|
|
3ca686298e | ||
|
|
5e914cb13a | ||
|
|
a1bdfca8ba | ||
|
|
79deaca409 | ||
|
|
2eeb21431b | ||
|
|
c4062bcae9 | ||
|
|
cd3a85ea04 | ||
|
|
d15b5c7f22 | ||
|
|
18bbe2271d | ||
|
|
ee2b7a3a66 | ||
|
|
ca36671131 | ||
|
|
604a1a6960 | ||
|
|
2898f5396f | ||
|
|
39541639c5 | ||
|
|
50981ad1a1 | ||
|
|
0cbfbeffae | ||
|
|
e603153016 | ||
|
|
e7c417ca0c | ||
|
|
a3989f28eb | ||
|
|
38ace3c348 | ||
|
|
0e00170f45 | ||
|
|
261ee96cad | ||
|
|
2c851885db | ||
|
|
849be86455 | ||
|
|
ffd5c120be | ||
|
|
76b15dda70 | ||
|
|
0bf7f519ad | ||
|
|
f97f325a36 | ||
|
|
24315fa787 | ||
|
|
792179657b | ||
|
|
9b92bcf911 | ||
|
|
86e34ea27b | ||
|
|
62cf778958 | ||
|
|
5d667e44c3 | ||
|
|
6d84b8741c | ||
|
|
d08ca59b75 | ||
|
|
f06b046c1c | ||
|
|
bb412a6906 | ||
|
|
1baee3004c | ||
|
|
f18068c13a | ||
|
|
0ba00a1845 | ||
|
|
28caf0d74c | ||
|
|
ecbb32b70b | ||
|
|
8e2a9cc597 | ||
|
|
a7eb8201c9 | ||
|
|
1f95fe6782 | ||
|
|
07c1f9e1af | ||
|
|
826dce6659 | ||
|
|
cdf714f3ba | ||
|
|
671ca7959c | ||
|
|
b7a1345d3a | ||
|
|
835b6ebdba | ||
|
|
0ecabd6553 |
20
.github/workflows/codeql-analysis.yml
vendored
20
.github/workflows/codeql-analysis.yml
vendored
@@ -11,14 +11,14 @@
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '30 18 * * 6'
|
||||
#on:
|
||||
# push:
|
||||
# branches: [ master ]
|
||||
# pull_request:
|
||||
# # The branches below must be a subset of the branches above
|
||||
# branches: [ master ]
|
||||
# schedule:
|
||||
# - cron: '30 18 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -38,12 +38,12 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 5.0.100
|
||||
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
|
||||
@@ -1,24 +1,63 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
|
||||
<PropertyGroup>
|
||||
<RobustUseExternalMSBuild>true</RobustUseExternalMSBuild>
|
||||
<_RobustUseExternalMSBuild>$(RobustUseExternalMSBuild)</_RobustUseExternalMSBuild>
|
||||
<_RobustUseExternalMSBuild Condition="'$(_RobustForceInternalMSBuild)' == 'true'">false</_RobustUseExternalMSBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="**\*.xaml.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
</Compile>
|
||||
<EmbeddedResource Include="**\*.xaml" />
|
||||
<AdditionalFiles Include="**\*.xaml" />
|
||||
<EmbeddedResource Include="**\*.xaml"/>
|
||||
<AdditionalFiles Include="**\*.xaml"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false"/>
|
||||
</ItemGroup>
|
||||
|
||||
<UsingTask TaskName="CompileRobustXamlTask" AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(Configuration)\netstandard2.0\Robust.Client.Injectors.dll" />
|
||||
<Target Name="CompileRobustXaml" AfterTargets="AfterCompile">
|
||||
<UsingTask
|
||||
Condition="'$(_RobustUseExternalMSBuild)' != 'true' And $(DesignTimeBuild) != true"
|
||||
TaskName="CompileRobustXamlTask"
|
||||
AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(Configuration)\netstandard2.0\Robust.Client.Injectors.dll"/>
|
||||
<Target
|
||||
Name="CompileRobustXaml"
|
||||
Condition="Exists('@(IntermediateAssembly)')"
|
||||
AfterTargets="AfterCompile"
|
||||
Inputs="@(IntermediateAssembly);@(ReferencePathWithRefAssemblies)"
|
||||
Outputs="$(IntermediateOutputPath)XAML/doot">
|
||||
<PropertyGroup>
|
||||
<RobustXamlReferencesTemporaryFilePath Condition="'$(RobustXamlReferencesTemporaryFilePath)' == ''">$(IntermediateOutputPath)XAML/references</RobustXamlReferencesTemporaryFilePath>
|
||||
<RobustXamlOriginalCopyFilePath Condition="'$(RobustXamlOriginalCopyFilePath)' == ''">$(IntermediateOutputPath)XAML/original.dll</RobustXamlOriginalCopyFilePath>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile File="$(RobustXamlReferencesTemporaryFilePath)" Lines="@(ReferencePathWithRefAssemblies)" Overwrite="true" />
|
||||
<CompileRobustXamlTask AssemblyFile="@(IntermediateAssembly)" ReferencesFilePath="$(RobustXamlReferencesTemporaryFilePath)" OriginalCopyPath="$(RobustXamlOriginalCopyFilePath)" ProjectDirectory="$(MSBuildProjectDirectory)" AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)" SignAssembly="$(SignAssembly)" DelaySign="$(DelaySign)" />
|
||||
<WriteLinesToFile
|
||||
Condition="'$(_RobustForceInternalMSBuild)' != 'true'"
|
||||
File="$(RobustXamlReferencesTemporaryFilePath)"
|
||||
Lines="@(ReferencePathWithRefAssemblies)"
|
||||
Overwrite="true"/>
|
||||
|
||||
<!--
|
||||
UpdateBuildIndicator is done so that we can use MSBuild Inputs and Outputs on the target
|
||||
to avoid unecessary execution of this target
|
||||
Saves compile time if e.g. ONLY Robust.Client changes (Content.Client doesn't have to re-xaml).
|
||||
-->
|
||||
<CompileRobustXamlTask
|
||||
Condition="'$(_RobustUseExternalMSBuild)' != 'true'"
|
||||
AssemblyFile="@(IntermediateAssembly)"
|
||||
ReferencesFilePath="$(RobustXamlReferencesTemporaryFilePath)"
|
||||
OriginalCopyPath="$(RobustXamlOriginalCopyFilePath)"
|
||||
ProjectDirectory="$(MSBuildProjectDirectory)"
|
||||
AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)"
|
||||
SignAssembly="$(SignAssembly)"
|
||||
DelaySign="$(DelaySign)"
|
||||
UpdateBuildIndicator="$(IntermediateOutputPath)XAML/doot"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<DOTNET_HOST_PATH Condition="'$(DOTNET_HOST_PATH)' == ''">dotnet</DOTNET_HOST_PATH>
|
||||
</PropertyGroup>
|
||||
<Exec
|
||||
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
Submodule NetSerializer updated: 60d23c01f9...1e103e8e29
@@ -17,6 +17,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
|
||||
// On net472, we rely on Mono's DllMap for this. See the .dll.config file.
|
||||
NativeLibrary.SetDllImportResolver(typeof(GLFWNative).Assembly, (name, assembly, path) =>
|
||||
{
|
||||
// Please keep in sync with what Robust.Shared/DllMapHelper.cs does.
|
||||
if (name != "glfw3.dll")
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
@@ -48,6 +49,19 @@ namespace Robust.Build.Tasks
|
||||
if(File.Exists(inputPdb))
|
||||
File.Copy(inputPdb, outputPdb, true);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(UpdateBuildIndicator))
|
||||
{
|
||||
if (!File.Exists(UpdateBuildIndicator))
|
||||
{
|
||||
File.Create(UpdateBuildIndicator).Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
File.SetLastWriteTime(UpdateBuildIndicator, DateTime.Now);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,6 +78,7 @@ namespace Robust.Build.Tasks
|
||||
public string OriginalCopyPath { get; set; }
|
||||
|
||||
public string OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; }
|
||||
|
||||
public string AssemblyOriginatorKeyFile { get; set; }
|
||||
public bool SignAssembly { get; set; }
|
||||
|
||||
@@ -31,6 +31,16 @@ namespace Robust.Build.Tasks
|
||||
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
|
||||
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
|
||||
{
|
||||
// If this type exists, the assembly has already been processed by us.
|
||||
// Do not run again, it would corrupt the file.
|
||||
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
|
||||
// but better safe than sorry eh?
|
||||
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var compileRes = CompileCore(engine, typeSystem);
|
||||
if (compileRes == null)
|
||||
return (true, false);
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace {nameSpace}
|
||||
DiagnosticSeverity.Error,
|
||||
true),
|
||||
typeSymbol.Locations[0]));
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var txt = relevantXamlFile.GetText()?.ToString();
|
||||
@@ -169,7 +169,7 @@ namespace {nameSpace}
|
||||
DiagnosticSeverity.Error,
|
||||
true),
|
||||
Location.Create(xamlFileName, new TextSpan(0,0), new LinePositionSpan(new LinePosition(0,0),new LinePosition(0,0)))));
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -189,7 +189,7 @@ namespace {nameSpace}
|
||||
DiagnosticSeverity.Error,
|
||||
true),
|
||||
typeSymbol.Locations[0]));
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Robust.Client.Audio.Midi
|
||||
private const string OsxSoundfont =
|
||||
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
|
||||
|
||||
private const string FallbackSoundfont = "/Resources/Midi/fallback.sf2";
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
|
||||
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
|
||||
|
||||
@@ -207,13 +207,10 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader);
|
||||
|
||||
foreach (var file in _resourceManager.ContentFindFiles(new ResourcePath("/Audio/MidiCustom/")))
|
||||
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls") continue;
|
||||
if (_resourceManager.TryGetDiskFilePath(file, out var path))
|
||||
{
|
||||
renderer.LoadSoundfont(path);
|
||||
}
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
@@ -382,10 +379,18 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
Stream? stream;
|
||||
if (filename.StartsWith("/Resources/"))
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
if (!IoCManager.Resolve<IResourceCache>().TryContentFileRead(filename.Substring(10), out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
var resourcePath = new ResourcePath(filename);
|
||||
|
||||
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Net;
|
||||
using Robust.Client.Interfaces;
|
||||
using Robust.Client.Interfaces.Debugging;
|
||||
@@ -7,15 +7,14 @@ using Robust.Client.Interfaces.GameStates;
|
||||
using Robust.Client.Interfaces.Utility;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client
|
||||
@@ -25,7 +24,7 @@ namespace Robust.Client
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _playMan = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IDiscordRichPresence _discord = default!;
|
||||
@@ -50,18 +49,28 @@ namespace Robust.Client
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
_net.RegisterNetMessage<MsgServerInfo>(MsgServerInfo.NAME, HandleServerInfo);
|
||||
_net.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME, HandleSetTickRate);
|
||||
_net.RegisterNetMessage<MsgServerInfoReq>(MsgServerInfoReq.NAME);
|
||||
_net.Connected += OnConnected;
|
||||
_net.ConnectFailed += OnConnectFailed;
|
||||
_net.Disconnect += OnNetDisconnect;
|
||||
|
||||
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged);
|
||||
|
||||
_playMan.Initialize();
|
||||
_debugDrawMan.Initialize();
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void TickRateChanged(int tickrate)
|
||||
{
|
||||
if (GameInfo != null)
|
||||
{
|
||||
GameInfo.TickRate = (byte) tickrate;
|
||||
}
|
||||
|
||||
_timing.TickRate = (byte) tickrate;
|
||||
Logger.InfoS("client", $"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ConnectToServer(DnsEndPoint endPoint)
|
||||
{
|
||||
@@ -98,9 +107,39 @@ namespace Robust.Client
|
||||
|
||||
private void OnConnected(object? sender, NetChannelArgs args)
|
||||
{
|
||||
// request base info about the server
|
||||
var msgInfo = _net.CreateNetMessage<MsgServerInfoReq>();
|
||||
_net.ClientSendMessage(msgInfo);
|
||||
_configManager.SyncWithServer();
|
||||
_configManager.ReceivedInitialNwVars += OnReceivedClientData;
|
||||
}
|
||||
|
||||
private void OnReceivedClientData(object? sender, EventArgs e)
|
||||
{
|
||||
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
|
||||
|
||||
var info = GameInfo;
|
||||
|
||||
var serverName = _configManager.GetCVar<string>("game.hostname");
|
||||
if (info == null)
|
||||
{
|
||||
GameInfo = info = new ServerInfo(serverName);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ServerName = serverName;
|
||||
}
|
||||
|
||||
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
|
||||
info.ServerMaxPlayers = maxPlayers;
|
||||
|
||||
var userName = _net.ServerChannel!.UserName;
|
||||
var userId = _net.ServerChannel.UserId;
|
||||
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
|
||||
// start up player management
|
||||
_playMan.Startup(_net.ServerChannel!);
|
||||
|
||||
_playMan.LocalPlayer!.UserId = userId;
|
||||
_playMan.LocalPlayer.Name = userName;
|
||||
|
||||
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,6 +174,7 @@ namespace Robust.Client
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_configManager.ClearReceivedInitialNwVars();
|
||||
OnRunLevelChanged(ClientRunLevel.Initialize);
|
||||
}
|
||||
|
||||
@@ -152,6 +192,7 @@ namespace Robust.Client
|
||||
|
||||
LastDisconnectReason = args.Reason;
|
||||
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
_gameStates.Reset();
|
||||
_playMan.Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
@@ -160,42 +201,6 @@ namespace Robust.Client
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void HandleServerInfo(MsgServerInfo msg)
|
||||
{
|
||||
var info = GameInfo;
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
GameInfo = info = new ServerInfo(msg.ServerName);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ServerName = msg.ServerName;
|
||||
}
|
||||
|
||||
info.ServerMaxPlayers = msg.ServerMaxPlayers;
|
||||
info.TickRate = msg.TickRate;
|
||||
_timing.TickRate = msg.TickRate;
|
||||
Logger.InfoS("client", $"Tickrate changed to: {msg.TickRate}");
|
||||
|
||||
var userName = msg.MsgChannel.UserName;
|
||||
var userId = msg.MsgChannel.UserId;
|
||||
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
|
||||
// start up player management
|
||||
_playMan.Startup(_net.ServerChannel!);
|
||||
|
||||
_playMan.LocalPlayer!.UserId = userId;
|
||||
_playMan.LocalPlayer.Name = userName;
|
||||
|
||||
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
|
||||
}
|
||||
|
||||
private void HandleSetTickRate(MsgSetTickRate message)
|
||||
{
|
||||
_timing.TickRate = message.NewTickRate;
|
||||
Logger.InfoS("client", $"Tickrate changed to: {message.NewTickRate} on tick {_timing.CurTick}");
|
||||
}
|
||||
|
||||
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
|
||||
{
|
||||
// player finished fully connecting to the server.
|
||||
|
||||
72
Robust.Client/Console/Commands/AddCompCommand.cs
Normal file
72
Robust.Client/Console/Commands/AddCompCommand.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Interfaces.Console;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class AddCompCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "addcompc";
|
||||
public string Description => "Adds a component to an entity on the client";
|
||||
public string Help => "addcompc <uid> <componentName>";
|
||||
|
||||
public bool Execute(IDebugConsole shell, params string[] args)
|
||||
{
|
||||
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.AddLine("Wrong number of arguments");
|
||||
return false;
|
||||
}
|
||||
|
||||
var entityUid = EntityUid.Parse(args[0]);
|
||||
var componentName = args[1];
|
||||
|
||||
var compManager = IoCManager.Resolve<IComponentManager>();
|
||||
var compFactory = IoCManager.Resolve<IComponentFactory>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var entity = entityManager.GetEntity(entityUid);
|
||||
var component = (Component) compFactory.GetComponent(componentName);
|
||||
|
||||
component.Owner = entity;
|
||||
|
||||
compManager.AddComponent(entity, component);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal sealed class RemoveCompCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "rmcompc";
|
||||
public string Description => "Removes a component from an entity.";
|
||||
public string Help => "rmcompc <uid> <componentName>";
|
||||
|
||||
public bool Execute(IDebugConsole shell, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.AddLine("Wrong number of arguments");
|
||||
return false;
|
||||
}
|
||||
|
||||
var entityUid = EntityUid.Parse(args[0]);
|
||||
var componentName = args[1];
|
||||
|
||||
var compManager = IoCManager.Resolve<IComponentManager>();
|
||||
var compFactory = IoCManager.Resolve<IComponentFactory>();
|
||||
|
||||
var registration = compFactory.GetRegistration(componentName);
|
||||
|
||||
compManager.RemoveComponent(entityUid, registration.Type);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -709,7 +709,8 @@ namespace Robust.Client.Console.Commands
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<ILightManager>();
|
||||
mgr.Enabled = !mgr.Enabled;
|
||||
if (!mgr.LockConsoleAccess)
|
||||
mgr.Enabled = !mgr.Enabled;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -722,10 +723,12 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<IEyeManager>();
|
||||
if (mgr.CurrentEye != null)
|
||||
mgr.CurrentEye.DrawFov = !mgr.CurrentEye.DrawFov;
|
||||
return false;
|
||||
var lmgr = IoCManager.Resolve<ILightManager>();
|
||||
var mgr = IoCManager.Resolve<IEyeManager>();
|
||||
if (!lmgr.LockConsoleAccess)
|
||||
if (mgr.CurrentEye != null)
|
||||
mgr.CurrentEye.DrawFov = !mgr.CurrentEye.DrawFov;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,7 +741,8 @@ namespace Robust.Client.Console.Commands
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<ILightManager>();
|
||||
mgr.DrawHardFov = !mgr.DrawHardFov;
|
||||
if (!mgr.LockConsoleAccess)
|
||||
mgr.DrawHardFov = !mgr.DrawHardFov;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -752,7 +756,22 @@ namespace Robust.Client.Console.Commands
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<ILightManager>();
|
||||
mgr.DrawShadows = !mgr.DrawShadows;
|
||||
if (!mgr.LockConsoleAccess)
|
||||
mgr.DrawShadows = !mgr.DrawShadows;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
internal class ToggleLightBuf : IConsoleCommand
|
||||
{
|
||||
public string Command => "togglelightbuf";
|
||||
public string Description => "Toggles lighting rendering. This includes shadows but not FOV.";
|
||||
public string Help => "togglelightbuf";
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<ILightManager>();
|
||||
if (!mgr.LockConsoleAccess)
|
||||
mgr.DrawLighting = !mgr.DrawLighting;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -781,6 +800,21 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal class GcFullCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gcf";
|
||||
public string Description => "Run the GC, fully, compacting LOH and everything.";
|
||||
public string Help => "gcf";
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect(2, GCCollectionMode.Forced, true, true);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal class GcModeCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
|
||||
69
Robust.Client/Console/Commands/LauncherAuthCommand.cs
Normal file
69
Robust.Client/Console/Commands/LauncherAuthCommand.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
#if !FULL_RELEASE
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Robust.Client.Interfaces.Console;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
internal sealed class LauncherAuthCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "launchauth";
|
||||
public string Description => "Load authentication tokens from launcher data to aid in testing of live servers";
|
||||
public string Help => "launchauth [account name]";
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var wantName = args.Length > 0 ? args[0] : null;
|
||||
|
||||
var basePath = Path.GetDirectoryName(UserDataDir.GetUserDataDir())!;
|
||||
var cfgPath = Path.Combine(basePath, "launcher", "launcher_config.json");
|
||||
|
||||
var data = JsonSerializer.Deserialize<LauncherConfig>(File.ReadAllText(cfgPath))!;
|
||||
|
||||
var login = wantName != null
|
||||
? data.Logins.FirstOrDefault(p => p.Username == wantName)
|
||||
: data.Logins.FirstOrDefault();
|
||||
|
||||
if (login == null)
|
||||
{
|
||||
console.AddLine("Unable to find a matching login");
|
||||
return false;
|
||||
}
|
||||
|
||||
var token = login.Token.Token;
|
||||
var userId = login.UserId;
|
||||
|
||||
var cfg = IoCManager.Resolve<IConfigurationManagerInternal>();
|
||||
cfg.SetSecureCVar(CVars.AuthUserId, userId);
|
||||
cfg.SetSecureCVar(CVars.AuthToken, token);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class LauncherConfig
|
||||
{
|
||||
[JsonInclude] [JsonPropertyName("logins")]
|
||||
public LauncherLogin[] Logins = default!;
|
||||
}
|
||||
|
||||
private sealed class LauncherLogin
|
||||
{
|
||||
[JsonInclude] public string Username = default!;
|
||||
[JsonInclude] public string UserId = default!;
|
||||
[JsonInclude] public LauncherToken Token = default!;
|
||||
}
|
||||
|
||||
private sealed class LauncherToken
|
||||
{
|
||||
[JsonInclude] public string Token = default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
35
Robust.Client/Console/Commands/SetInputContextCommand.cs
Normal file
35
Robust.Client/Console/Commands/SetInputContextCommand.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Interfaces.Console;
|
||||
using Robust.Client.Interfaces.Input;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class SetInputContextCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "setinputcontext";
|
||||
public string Description => "Sets the active input context.";
|
||||
public string Help => "setinputcontext <context>";
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
console.AddLine("Invalid number of arguments!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var inputMan = IoCManager.Resolve<IInputManager>();
|
||||
|
||||
if (!inputMan.Contexts.Exists(args[0]))
|
||||
{
|
||||
console.AddLine("Context not found!");
|
||||
return false;
|
||||
}
|
||||
|
||||
inputMan.Contexts.SetActiveContext(args[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
@@ -164,6 +164,7 @@ namespace Robust.Client
|
||||
|
||||
_userInterfaceManager.Initialize();
|
||||
_networkManager.Initialize(false);
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
|
||||
@@ -48,11 +48,21 @@ namespace Robust.Client.GameObjects.Components.Animations
|
||||
return;
|
||||
}
|
||||
|
||||
List<string>? toRemove = null;
|
||||
// TODO: Get rid of this ToArray() allocation.
|
||||
foreach (var (key, playback) in _playingAnimations.ToArray())
|
||||
{
|
||||
var keep = UpdatePlayback(Owner, playback, frameTime);
|
||||
if (!keep)
|
||||
{
|
||||
toRemove ??= new List<string>();
|
||||
toRemove.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove != null)
|
||||
{
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
_playingAnimations.Remove(key);
|
||||
AnimationCompleted?.Invoke(key);
|
||||
|
||||
@@ -54,17 +54,17 @@ namespace Robust.Client.GameObjects
|
||||
return (T) data[key];
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data)
|
||||
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data)
|
||||
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
internal bool TryGetData<T>(object key, [MaybeNullWhen(false)] out T data)
|
||||
internal bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
if (this.data.TryGetValue(key, out var dat))
|
||||
{
|
||||
@@ -72,7 +72,7 @@ namespace Robust.Client.GameObjects
|
||||
return true;
|
||||
}
|
||||
|
||||
data = default;
|
||||
data = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private static IDirectionalTextureProvider TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache)
|
||||
private static IRsiStateLike TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache)
|
||||
{
|
||||
DebugTools.Assert(serializer.Reading);
|
||||
|
||||
if (serializer.TryGetCacheData<IDirectionalTextureProvider>(SerializationCache, out var dirTex))
|
||||
if (serializer.TryGetCacheData<IRsiStateLike>(SerializationCache, out var dirTex))
|
||||
{
|
||||
return dirTex;
|
||||
}
|
||||
@@ -93,7 +93,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public static IDirectionalTextureProvider? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
if (!prototype.Components.TryGetValue("Icon", out var mapping))
|
||||
{
|
||||
|
||||
@@ -991,6 +991,9 @@ namespace Robust.Client.GameObjects
|
||||
var mRotation = Matrix3.CreateRotation(angle);
|
||||
Matrix3.Multiply(ref mRotation, ref mOffset, out var transform);
|
||||
|
||||
// Only apply scale if needed.
|
||||
if(!Scale.EqualsApprox(Vector2.One)) transform.Multiply(Matrix3.CreateScale(Scale));
|
||||
|
||||
transform.Multiply(worldTransform);
|
||||
|
||||
RenderInternal(drawingHandle, worldRotation, overrideDirection, transform);
|
||||
@@ -1043,7 +1046,7 @@ namespace Robust.Client.GameObjects
|
||||
var rsi = layer.RSI ?? BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
|
||||
{
|
||||
state = GetFallbackState();
|
||||
state = GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
var layerSpecificDir = layer.EffectiveDirection(state, worldRotation, overrideDirection);
|
||||
@@ -1276,7 +1279,7 @@ namespace Robust.Client.GameObjects
|
||||
var rsi = layer.RSI ?? BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
|
||||
{
|
||||
state = GetFallbackState();
|
||||
state = GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
if (!state.IsAnimated)
|
||||
@@ -1404,7 +1407,7 @@ namespace Robust.Client.GameObjects
|
||||
var rsi = layer.RSI ?? BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
|
||||
{
|
||||
state = GetFallbackState();
|
||||
state = GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
if (state.IsAnimated)
|
||||
@@ -1415,9 +1418,9 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private RSI.State GetFallbackState()
|
||||
internal static RSI.State GetFallbackState(IResourceCache cache)
|
||||
{
|
||||
var rsi = resourceCache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
|
||||
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
|
||||
return rsi["error"];
|
||||
}
|
||||
|
||||
@@ -1735,14 +1738,14 @@ namespace Robust.Client.GameObjects
|
||||
var rsi = ActualRsi;
|
||||
if (rsi == null)
|
||||
{
|
||||
state = _parent.GetFallbackState();
|
||||
state = GetFallbackState(_parent.resourceCache);
|
||||
Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!rsi.TryGetState(stateId, out state))
|
||||
{
|
||||
state = _parent.GetFallbackState();
|
||||
state = GetFallbackState(_parent.resourceCache);
|
||||
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId,
|
||||
Environment.StackTrace);
|
||||
}
|
||||
@@ -1793,7 +1796,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public IDirectionalTextureProvider? Icon
|
||||
public IRsiStateLike? Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -1803,13 +1806,13 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var texture = layer.Texture;
|
||||
|
||||
if (!layer.State.IsValid) return null;
|
||||
if (!layer.State.IsValid) return texture;
|
||||
|
||||
// Pull texture from RSI state instead.
|
||||
var rsi = layer.RSI ?? BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
|
||||
{
|
||||
state = GetFallbackState();
|
||||
state = GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
return state;
|
||||
@@ -1867,21 +1870,21 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
}
|
||||
|
||||
public static IDirectionalTextureProvider? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
var icon = IconComponent.GetPrototypeIcon(prototype, resourceCache);
|
||||
if (icon != null) return icon;
|
||||
|
||||
if (!prototype.Components.TryGetValue("Sprite", out var spriteNode))
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
return resourceCache.GetFallback<TextureResource>().Texture;
|
||||
return GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
var dummy = new DummyIconEntity() {Prototype = prototype};
|
||||
var dummy = new DummyIconEntity {Prototype = prototype};
|
||||
var spriteComponent = dummy.AddComponent<SpriteComponent>();
|
||||
dummy.Delete();
|
||||
|
||||
return spriteComponent?.Icon ?? resourceCache.GetFallback<TextureResource>().Texture;
|
||||
return spriteComponent.Icon ?? GetFallbackState(resourceCache);
|
||||
}
|
||||
|
||||
#region DummyIconEntity
|
||||
|
||||
@@ -56,12 +56,12 @@ namespace Robust.Client.GameObjects.EntitySystems
|
||||
|
||||
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
|
||||
{
|
||||
var gridId = ev.Coordinates.GetGridId(_entityManager);
|
||||
|
||||
if (!_mapManager.GridExists(gridId))
|
||||
var mapId = ev.Coordinates.GetMapId(_entityManager);
|
||||
|
||||
if (!_mapManager.MapExists(mapId))
|
||||
{
|
||||
Logger.Error(
|
||||
$"Server tried to play sound on grid {gridId}, which does not exist. Ignoring.");
|
||||
$"Server tried to play sound on map {mapId}, which does not exist. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,13 +111,13 @@ namespace Robust.Client.GameObjects.EntitySystems
|
||||
if (stream.TrackingCoordinates != null)
|
||||
{
|
||||
var coords = stream.TrackingCoordinates.Value;
|
||||
if (_mapManager.GridExists(coords.GetGridId(_entityManager)))
|
||||
if (_mapManager.MapExists(coords.GetMapId(_entityManager)))
|
||||
{
|
||||
mapPos = stream.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Grid no longer exists, delete stream.
|
||||
// Map no longer exists, delete stream.
|
||||
StreamDone(stream);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,8 @@ namespace Robust.Client.GameObjects.EntitySystems
|
||||
|
||||
if (!entity.TryGetComponent(out InputComponent? inputComp))
|
||||
{
|
||||
Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity.Uid}, entProto={entity.Prototype}");
|
||||
Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity.Uid}, entProto={entity.Prototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +139,8 @@ namespace Robust.Client.GameObjects.EntitySystems
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("input.context", $"Unknown context: entId={entity.Uid}, entProto={entity.Prototype}, context={inputComp.ContextName}");
|
||||
Logger.ErrorS("input.context", $"Unknown context: entId={entity.Uid}, entProto={entity.Prototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Client.Interfaces;
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
@@ -41,7 +42,7 @@ namespace Robust.Client.GameStates
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
@@ -57,7 +58,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public bool Predicting { get; private set; }
|
||||
|
||||
public int PredictSize { get; private set; }
|
||||
public int PredictTickBias { get; private set; }
|
||||
public float PredictLagBias { get; private set; }
|
||||
|
||||
public int StateBufferMergeThreshold { get; private set; }
|
||||
|
||||
@@ -82,14 +84,16 @@ namespace Robust.Client.GameStates
|
||||
_config.OnValueChanged(CVars.NetInterpRatio, i => _processor.InterpRatio = i, true);
|
||||
_config.OnValueChanged(CVars.NetLogging, b => _processor.Logging = b, true);
|
||||
_config.OnValueChanged(CVars.NetPredict, b => Predicting = b, true);
|
||||
_config.OnValueChanged(CVars.NetPredictSize, i => PredictSize = i, true);
|
||||
_config.OnValueChanged(CVars.NetPredictTickBias, i => PredictTickBias = i, true);
|
||||
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
|
||||
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
|
||||
|
||||
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
|
||||
_processor.InterpRatio = _config.GetCVar(CVars.NetInterpRatio);
|
||||
_processor.Logging = _config.GetCVar(CVars.NetLogging);
|
||||
Predicting = _config.GetCVar(CVars.NetPredict);
|
||||
PredictSize = _config.GetCVar(CVars.NetPredictSize);
|
||||
PredictTickBias = _config.GetCVar(CVars.NetPredictTickBias);
|
||||
PredictLagBias = _config.GetCVar(CVars.NetPredictLagBias);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -174,7 +178,7 @@ namespace Robust.Client.GameStates
|
||||
var i = 0;
|
||||
for (; i < applyCount; i++)
|
||||
{
|
||||
_timing.CurTick = _lastProcessedTick + 1;
|
||||
_timing.LastRealTick = _timing.CurTick = _lastProcessedTick + 1;
|
||||
|
||||
// TODO: We could theoretically communicate with the GameStateProcessor better here.
|
||||
// Since game states are sliding windows, it is possible that we need less than applyCount applies here.
|
||||
@@ -256,9 +260,9 @@ namespace Robust.Client.GameStates
|
||||
var hasPendingInput = pendingInputEnumerator.MoveNext();
|
||||
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
|
||||
var ping = _network.ServerChannel!.Ping / 1000f; // seconds.
|
||||
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
|
||||
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
|
||||
(int) Math.Ceiling(_timing.TickRate * ping) + PredictSize;
|
||||
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
|
||||
|
||||
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
|
||||
|
||||
@@ -377,6 +381,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private List<EntityUid> ApplyGameState(GameState curState, GameState? nextState)
|
||||
{
|
||||
_config.TickProcessMessages();
|
||||
_mapManager.ApplyGameStatePre(curState.MapData);
|
||||
var createdEntities = _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
|
||||
nextState?.EntityStates);
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
while (readSamples < totalSamples)
|
||||
{
|
||||
var read = vorbis.ReadSamples(buffer, readSamples * channels, (int)totalSamples - readSamples);
|
||||
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private const string UniProjViewMatrices = "projectionViewMatrices";
|
||||
private const string UniUniformConstants = "uniformConstants";
|
||||
|
||||
private static readonly Color AmbientLightColor = Color.Black;
|
||||
|
||||
private const int BindingIndexProjView = 0;
|
||||
private const int BindingIndexUniformConstants = 1;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Buffers;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -30,7 +31,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Horizontal width, in pixels, of the shadow maps used to render FOV.
|
||||
// I figured this was more accuracy sensitive than lights so resolution is significantly higher.
|
||||
private const int FovMapSize = 2048;
|
||||
private const int MaxLightsPerScene = 128;
|
||||
|
||||
// The maximum possible amount of lights in the light list.
|
||||
// In the average case, the only cost of increasing this value is memory.
|
||||
// If you are ever in a situation where this value needs to be increased, however, it will also implicitly cost some CPU time to sort the additional lights.
|
||||
private const int LightsToRenderListSize = 2048;
|
||||
|
||||
private ClydeShaderInstance _fovDebugShaderInstance = default!;
|
||||
|
||||
@@ -80,16 +85,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// For depth calculation of lighting shadows.
|
||||
private RenderTexture _shadowRenderTarget = default!;
|
||||
// Used because otherwise a MaxLightsPerScene change callback getting hit on startup causes interesting issues (read: bugs)
|
||||
private bool _shadowRenderTargetCanInitializeSafely = false;
|
||||
|
||||
// Proxies to textures of the above render targets.
|
||||
private ClydeTexture FovTexture => _fovRenderTarget.Texture;
|
||||
private ClydeTexture ShadowTexture => _shadowRenderTarget.Texture;
|
||||
|
||||
private readonly (PointLightComponent light, Vector2 pos)[] _lightsToRenderList
|
||||
= new (PointLightComponent light, Vector2 pos)[MaxLightsPerScene];
|
||||
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList = new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
|
||||
|
||||
private unsafe void InitLighting()
|
||||
{
|
||||
// Other...
|
||||
LoadLightingShaders();
|
||||
|
||||
{
|
||||
@@ -160,10 +167,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
// Shadow FBO.
|
||||
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, MaxLightsPerScene),
|
||||
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new TextureSampleParameters {WrapMode = TextureWrapMode.Repeat, Filter = true},
|
||||
nameof(_shadowRenderTarget));
|
||||
_shadowRenderTargetCanInitializeSafely = true;
|
||||
MaxLightsPerSceneChanged(_maxLightsPerScene);
|
||||
}
|
||||
|
||||
private void LoadLightingShaders()
|
||||
@@ -334,6 +339,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
DrawFov(viewport, eye);
|
||||
|
||||
if (!_lightManager.DrawLighting)
|
||||
{
|
||||
BindRenderTargetFull(viewport.RenderTarget);
|
||||
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
|
||||
CheckGlError();
|
||||
return;
|
||||
}
|
||||
|
||||
using (DebugGroup("Draw shadow depth"))
|
||||
{
|
||||
PrepareDepthDraw(RtToLoaded(_shadowRenderTarget));
|
||||
@@ -344,7 +357,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var (light, lightPos) = lights[i];
|
||||
var (light, lightPos, _) = lights[i];
|
||||
|
||||
DrawOcclusionDepth(lightPos, ShadowMapSize, light.Radius, i);
|
||||
}
|
||||
@@ -355,7 +368,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
|
||||
CheckGlError();
|
||||
GLClearColor(Color.FromSrgb(AmbientLightColor));
|
||||
GLClearColor(_lightManager.AmbientLightColor);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit);
|
||||
CheckGlError();
|
||||
|
||||
@@ -382,7 +395,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var (component, lightPos) = lights[i];
|
||||
var (component, lightPos, _) = lights[i];
|
||||
|
||||
var transform = component.Owner.Transform;
|
||||
|
||||
@@ -473,25 +486,24 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_lightingReady = true;
|
||||
}
|
||||
|
||||
private ((PointLightComponent light, Vector2 pos)[] lights, int count, Box2 expandedBounds)
|
||||
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2 expandedBounds)
|
||||
GetLightsToRender(MapId map, in Box2 worldBounds)
|
||||
{
|
||||
// When culling occluders later, we can't just remove any occluders outside the worldBounds.
|
||||
// As they could still affect the shadows of (large) light sources.
|
||||
// We expand the world bounds so that it encompasses the center of every light source.
|
||||
// This should make it so no culled occluder can make a difference.
|
||||
// (if the occluder is in the current lights at all, it's still not between the light and the world bounds).
|
||||
var expandedBounds = worldBounds;
|
||||
|
||||
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
|
||||
var lightTree = renderingTreeSystem.GetLightTreeForMap(map);
|
||||
|
||||
var state = (this, expandedBounds, count: 0);
|
||||
var state = (this, worldBounds, count: 0);
|
||||
|
||||
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 expandedBounds, int count) state, in PointLightComponent light) =>
|
||||
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;
|
||||
}
|
||||
|
||||
if (!light.Enabled || light.ContainerOccluded)
|
||||
{
|
||||
return true;
|
||||
@@ -501,26 +513,45 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var circle = new Circle(lightPos, light.Radius);
|
||||
|
||||
if (!circle.Intersects(state.expandedBounds))
|
||||
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
|
||||
if (!circle.Intersects(state.worldBounds))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
state.clyde._lightsToRenderList[state.count] = (light, lightPos);
|
||||
state.count += 1;
|
||||
|
||||
state.expandedBounds = state.expandedBounds.ExtendToContain(lightPos);
|
||||
|
||||
if (state.count == MaxLightsPerScene)
|
||||
{
|
||||
// TODO: Allow more than MaxLightsPerScene lights.
|
||||
return false;
|
||||
}
|
||||
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
|
||||
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
|
||||
|
||||
return true;
|
||||
}, expandedBounds);
|
||||
}, worldBounds);
|
||||
|
||||
return (_lightsToRenderList, state.count, state.expandedBounds);
|
||||
if (state.count > _maxLightsPerScene)
|
||||
{
|
||||
// There are too many lights to fit in the scene.
|
||||
// This check must occur before occluder expansion, or else bad things happen.
|
||||
// Sort lights by distance.
|
||||
Array.Sort(_lightsToRenderList, 0, state.count, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
|
||||
{
|
||||
return x.distanceSquared.CompareTo(y.distanceSquared);
|
||||
}));
|
||||
// Then effectively delete the furthest lights.
|
||||
state.count = _maxLightsPerScene;
|
||||
}
|
||||
|
||||
// When culling occluders later, we can't just remove any occluders outside the worldBounds.
|
||||
// As they could still affect the shadows of (large) light sources.
|
||||
// We expand the world bounds so that it encompasses the center of every light source.
|
||||
// This should make it so no culled occluder can make a difference.
|
||||
// (if the occluder is in the current lights at all, it's still not between the light and the world bounds).
|
||||
var expandedBounds = worldBounds;
|
||||
|
||||
for (var i = 0; i < state.count; i++)
|
||||
{
|
||||
var (_, lightPos, _) = _lightsToRenderList[i];
|
||||
expandedBounds = expandedBounds.ExtendToContain(lightPos);
|
||||
}
|
||||
|
||||
return (_lightsToRenderList, state.count, expandedBounds);
|
||||
}
|
||||
|
||||
private void BlurOntoWalls(Viewport viewport, IEye eye)
|
||||
@@ -959,6 +990,25 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RegenAllLightRts();
|
||||
}
|
||||
|
||||
protected override void MaxLightsPerSceneChanged(int newValue)
|
||||
{
|
||||
_maxLightsPerScene = newValue;
|
||||
|
||||
// This guard is in place because otherwise the shadow FBO is initialized before GL is initialized.
|
||||
if (!_shadowRenderTargetCanInitializeSafely)
|
||||
return;
|
||||
|
||||
if (_shadowRenderTarget != null)
|
||||
{
|
||||
DeleteRenderTexture(_shadowRenderTarget.Handle);
|
||||
}
|
||||
// Shadow FBO.
|
||||
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, _maxLightsPerScene),
|
||||
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new TextureSampleParameters {WrapMode = TextureWrapMode.Repeat, Filter = true},
|
||||
nameof(_shadowRenderTarget));
|
||||
}
|
||||
|
||||
protected override void SoftShadowsChanged(bool newValue)
|
||||
{
|
||||
_enableSoftShadows = newValue;
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CheckGlError();
|
||||
|
||||
// Check on original format is NOT a bug, this is so srgb emulation works
|
||||
textureObject = GenTexture(texture, size, format.ColorFormat == RTCF.Rgba8Srgb, name == null ? null : $"{name}-color");
|
||||
textureObject = GenTexture(texture, size, format.ColorFormat == RTCF.Rgba8Srgb, name == null ? null : $"{name}-color", TexturePixelType.RenderTarget);
|
||||
}
|
||||
|
||||
// Depth/stencil buffers.
|
||||
|
||||
@@ -11,6 +11,9 @@ using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
|
||||
using PIF = OpenToolkit.Graphics.OpenGL4.PixelInternalFormat;
|
||||
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
|
||||
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -20,8 +23,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private ClydeTexture _stockTextureBlack = default!;
|
||||
private ClydeTexture _stockTextureTransparent = default!;
|
||||
|
||||
private readonly Dictionary<ClydeHandle, LoadedTexture> _loadedTextures =
|
||||
new();
|
||||
private readonly Dictionary<ClydeHandle, LoadedTexture> _loadedTextures = new();
|
||||
|
||||
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
|
||||
|
||||
@@ -69,85 +71,129 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Flip image because OpenGL reads images upside down.
|
||||
var copy = FlipClone(image);
|
||||
|
||||
var texture = new GLHandle((uint) GL.GenTexture());
|
||||
CheckGlError();
|
||||
GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
|
||||
CheckGlError();
|
||||
ApplySampleParameters(actualParams.SampleParameters);
|
||||
|
||||
PixelInternalFormat internalFormat;
|
||||
PixelFormat pixelDataFormat;
|
||||
PixelType pixelDataType;
|
||||
bool isActuallySrgb = false;
|
||||
|
||||
if (pixelType == typeof(Rgba32))
|
||||
{
|
||||
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
|
||||
// Shaders are expected to compensate for this
|
||||
internalFormat = (actualParams.Srgb && _hasGLSrgb) ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8;
|
||||
isActuallySrgb = actualParams.Srgb;
|
||||
pixelDataFormat = PixelFormat.Rgba;
|
||||
pixelDataType = PixelType.UnsignedByte;
|
||||
}
|
||||
else if (pixelType == typeof(A8))
|
||||
{
|
||||
if (image.Width % 4 != 0 || image.Height % 4 != 0)
|
||||
{
|
||||
throw new ArgumentException("Alpha8 images must have multiple of 4 sizes.");
|
||||
}
|
||||
|
||||
internalFormat = PixelInternalFormat.R8;
|
||||
pixelDataFormat = PixelFormat.Red;
|
||||
pixelDataType = PixelType.UnsignedByte;
|
||||
|
||||
unsafe
|
||||
{
|
||||
// TODO: Does it make sense to default to 1 for RGB parameters?
|
||||
// It might make more sense to pass some options to change swizzling.
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.One);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.One);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.One);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.Red);
|
||||
}
|
||||
}
|
||||
else if (pixelType == typeof(L8) && !actualParams.Srgb)
|
||||
{
|
||||
// Can only use R8 for L8 if sRGB is OFF.
|
||||
// Because OpenGL doesn't provide sRGB single/dual channel image formats.
|
||||
// Vulkan when?
|
||||
if (copy.Width % 4 != 0 || copy.Height % 4 != 0)
|
||||
{
|
||||
throw new ArgumentException("L8 non-sRGB images must have multiple of 4 sizes.");
|
||||
}
|
||||
|
||||
internalFormat = PixelInternalFormat.R8;
|
||||
pixelDataFormat = PixelFormat.Red;
|
||||
pixelDataType = PixelType.UnsignedByte;
|
||||
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.Red);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.Red);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.Red);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.One);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"Unable to handle pixel type '{pixelType.Name}'");
|
||||
}
|
||||
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
|
||||
|
||||
unsafe
|
||||
{
|
||||
var span = copy.GetPixelSpan();
|
||||
fixed (T* ptr = span)
|
||||
{
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, internalFormat, copy.Width, copy.Height, 0,
|
||||
pixelDataFormat, pixelDataType, (IntPtr) ptr);
|
||||
CheckGlError();
|
||||
// Still bound.
|
||||
DoTexUpload(copy.Width, copy.Height, actualParams.Srgb, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
var pressureEst = EstPixelSize(internalFormat) * copy.Width * copy.Height;
|
||||
return texture;
|
||||
}
|
||||
|
||||
return GenTexture(texture, (copy.Width, copy.Height), isActuallySrgb, name, pressureEst);
|
||||
public unsafe OwnedTexture CreateBlankTexture<T>(
|
||||
Vector2i size,
|
||||
string? name = null,
|
||||
in TextureLoadParameters? loadParams = null)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var actualParams = loadParams ?? TextureLoadParameters.Default;
|
||||
if (!_hasGLTextureSwizzle)
|
||||
{
|
||||
// Actually create RGBA32 texture if missing texture swizzle.
|
||||
// This is fine (TexturePixelType that's stored) because all other APIs do the same.
|
||||
if (typeof(T) == typeof(A8) || typeof(T) == typeof(L8))
|
||||
{
|
||||
return CreateBlankTexture<Rgba32>(size, name, loadParams);
|
||||
}
|
||||
}
|
||||
|
||||
var texture = CreateBaseTextureInternal<T>(
|
||||
size.X, size.Y,
|
||||
actualParams,
|
||||
name);
|
||||
|
||||
// Texture still bound, run glTexImage2D with null data param to specify bounds.
|
||||
DoTexUpload<T>(size.X, size.Y, actualParams.Srgb, null);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
private unsafe void DoTexUpload<T>(int width, int height, bool srgb, T* ptr) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
if (sizeof(T) < 4)
|
||||
{
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
var (pif, pf, pt) = PixelEnums<T>(srgb);
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, pif, width, height, 0, pf, pt, (IntPtr) ptr);
|
||||
CheckGlError();
|
||||
|
||||
if (sizeof(T) < 4)
|
||||
{
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
private ClydeTexture CreateBaseTextureInternal<T>(
|
||||
int width, int height,
|
||||
in TextureLoadParameters loadParams,
|
||||
string? name = null)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var texture = new GLHandle((uint) GL.GenTexture());
|
||||
CheckGlError();
|
||||
GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
|
||||
CheckGlError();
|
||||
ApplySampleParameters(loadParams.SampleParameters);
|
||||
|
||||
var (pif, _, _) = PixelEnums<T>(loadParams.Srgb);
|
||||
var pixelType = typeof(T);
|
||||
var texPixType = GetTexturePixelType<T>();
|
||||
var isActuallySrgb = false;
|
||||
|
||||
if (pixelType == typeof(Rgba32))
|
||||
{
|
||||
isActuallySrgb = loadParams.Srgb;
|
||||
}
|
||||
else if (pixelType == typeof(A8))
|
||||
{
|
||||
DebugTools.Assert(_hasGLTextureSwizzle);
|
||||
|
||||
// TODO: Does it make sense to default to 1 for RGB parameters?
|
||||
// It might make more sense to pass some options to change swizzling.
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.One);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.One);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.One);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.Red);
|
||||
CheckGlError();
|
||||
}
|
||||
else if (pixelType == typeof(L8) && !loadParams.Srgb)
|
||||
{
|
||||
DebugTools.Assert(_hasGLTextureSwizzle);
|
||||
|
||||
// Can only use R8 for L8 if sRGB is OFF.
|
||||
// Because OpenGL doesn't provide sRGB single/dual channel image formats.
|
||||
// Vulkan when?
|
||||
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.Red);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.Red);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.Red);
|
||||
CheckGlError();
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.One);
|
||||
CheckGlError();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Unable to handle pixel type '{pixelType.Name}'");
|
||||
}
|
||||
|
||||
var pressureEst = EstPixelSize(pif) * width * height;
|
||||
|
||||
return GenTexture(texture, (width, height), isActuallySrgb, name, texPixType, pressureEst);
|
||||
}
|
||||
|
||||
private void ApplySampleParameters(TextureSampleParameters? sampleParameters)
|
||||
@@ -201,10 +247,30 @@ namespace Robust.Client.Graphics.Clyde
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
private ClydeTexture GenTexture(GLHandle glHandle, Vector2i size, bool srgb, string? name, long memoryPressure=0)
|
||||
private (PIF pif, PF pf, PT pt) PixelEnums<T>(bool srgb)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
return default(T) switch
|
||||
{
|
||||
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
|
||||
// Shaders are expected to compensate for this
|
||||
Rgba32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Rgba, PT.UnsignedByte),
|
||||
A8 or L8 => (PIF.R8, PF.Red, PT.UnsignedByte),
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
};
|
||||
}
|
||||
|
||||
private ClydeTexture GenTexture(
|
||||
GLHandle glHandle,
|
||||
Vector2i size,
|
||||
bool srgb,
|
||||
string? name,
|
||||
TexturePixelType pixType,
|
||||
long memoryPressure = 0)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
@@ -222,7 +288,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Height = height,
|
||||
IsSrgb = srgb,
|
||||
Name = name,
|
||||
MemoryPressure = memoryPressure
|
||||
MemoryPressure = memoryPressure,
|
||||
TexturePixelType = pixType
|
||||
// TextureInstance = new WeakReference<ClydeTexture>(instance)
|
||||
};
|
||||
|
||||
@@ -246,6 +313,97 @@ namespace Robust.Client.Graphics.Clyde
|
||||
//GC.RemoveMemoryPressure(loadedTexture.MemoryPressure);
|
||||
}
|
||||
|
||||
private unsafe void SetSubImage<T>(
|
||||
ClydeTexture texture,
|
||||
Vector2i dstTl,
|
||||
Image<T> srcImage,
|
||||
in UIBox2i srcBox)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
if (!_hasGLTextureSwizzle)
|
||||
{
|
||||
if (typeof(T) == typeof(A8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(L8))
|
||||
{
|
||||
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
|
||||
}
|
||||
}
|
||||
|
||||
var loaded = _loadedTextures[texture.TextureId];
|
||||
|
||||
var pixType = GetTexturePixelType<T>();
|
||||
|
||||
if (pixType != loaded.TexturePixelType)
|
||||
{
|
||||
if (loaded.TexturePixelType == TexturePixelType.RenderTarget)
|
||||
throw new InvalidOperationException("Cannot modify texture for render target directly.");
|
||||
|
||||
throw new InvalidOperationException("Mismatching pixel type for texture.");
|
||||
}
|
||||
|
||||
if (loaded.Width < dstTl.X + srcBox.Width || loaded.Height < dstTl.Y + srcBox.Height)
|
||||
throw new ArgumentOutOfRangeException(nameof(srcBox), "Destination rectangle out of bounds.");
|
||||
|
||||
if (srcBox.Left < 0 || srcBox.Top < 0 || srcBox.Right > srcImage.Width || srcBox.Bottom > srcImage.Height)
|
||||
throw new ArgumentOutOfRangeException(nameof(srcBox), "Source rectangle out of bounds.");
|
||||
|
||||
if (sizeof(T) != 4)
|
||||
{
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
// sRGB doesn't matter since that only changes the internalFormat, which we don't need here.
|
||||
var (_, pf, pt) = PixelEnums<T>(srgb: false);
|
||||
|
||||
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
|
||||
CheckGlError();
|
||||
|
||||
var size = srcBox.Width * srcBox.Height;
|
||||
|
||||
var copyBuffer = size < 16 * 16 ? stackalloc T[size] : new T[size];
|
||||
|
||||
for (var y = 0; y < srcBox.Height; y++)
|
||||
for (var x = 0; x < srcBox.Width; x++)
|
||||
{
|
||||
copyBuffer[(srcBox.Height - y - 1) * srcBox.Width + x] = srcImage[x + srcBox.Left, srcBox.Top + y];
|
||||
}
|
||||
|
||||
fixed (T* aPtr = copyBuffer)
|
||||
{
|
||||
var dstY = loaded.Height - dstTl.Y - srcBox.Height;
|
||||
GL.TexSubImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
dstTl.X, dstY,
|
||||
srcBox.Width, srcBox.Height,
|
||||
pf, pt,
|
||||
(IntPtr) aPtr);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
if (sizeof(T) != 4)
|
||||
{
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
private static TexturePixelType GetTexturePixelType<T>() where T : unmanaged, IPixel<T>
|
||||
{
|
||||
return default(T) switch
|
||||
{
|
||||
Rgba32 => TexturePixelType.Rgba32,
|
||||
L8 => TexturePixelType.L8,
|
||||
A8 => TexturePixelType.A8,
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
};
|
||||
}
|
||||
|
||||
private void LoadStockTextures()
|
||||
{
|
||||
var white = new Image<Rgba32>(1, 1);
|
||||
@@ -350,10 +508,20 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public bool IsSrgb;
|
||||
public string? Name;
|
||||
public long MemoryPressure;
|
||||
public TexturePixelType TexturePixelType;
|
||||
|
||||
public Vector2i Size => (Width, Height);
|
||||
// public WeakReference<ClydeTexture> TextureInstance;
|
||||
}
|
||||
|
||||
private enum TexturePixelType : byte
|
||||
{
|
||||
RenderTarget = 0,
|
||||
Rgba32,
|
||||
A8,
|
||||
L8,
|
||||
}
|
||||
|
||||
private void FlushTextureDispose()
|
||||
{
|
||||
while (_textureDisposeQueue.TryDequeue(out var handle))
|
||||
@@ -369,6 +537,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
internal ClydeHandle TextureId { get; }
|
||||
|
||||
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
|
||||
{
|
||||
_clyde.SetSubImage(this, topLeft, sourceImage, sourceRegion);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private GLShaderProgram? _currentProgram;
|
||||
|
||||
private int _lightmapDivider = 2;
|
||||
private int _maxLightsPerScene = 128;
|
||||
private bool _enableSoftShadows = true;
|
||||
|
||||
private bool _checkGLErrors;
|
||||
@@ -133,6 +134,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
base.ReadConfig();
|
||||
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
|
||||
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
|
||||
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public string GetKeyName(Keyboard.Key key) => string.Empty;
|
||||
public string GetKeyNameScanCode(int scanCode) => string.Empty;
|
||||
public int GetKeyScanCode(Keyboard.Key key) => default;
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nada.
|
||||
@@ -77,8 +78,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public override event Action<WindowResizedEventArgs> OnWindowResized
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public void Render()
|
||||
@@ -111,6 +112,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new DummyTexture((image.Width, image.Height));
|
||||
}
|
||||
|
||||
public OwnedTexture CreateBlankTexture<T>(
|
||||
Vector2i size,
|
||||
string? name = null,
|
||||
in TextureLoadParameters? loadParams = null)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
return new DummyTexture(size);
|
||||
}
|
||||
|
||||
public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
|
||||
TextureSampleParameters? sampleParameters = null, string? name = null)
|
||||
{
|
||||
@@ -174,7 +184,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return DummyAudioSource.Instance;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
@@ -296,6 +306,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public DummyTexture(Vector2i size) : base(size)
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
|
||||
{
|
||||
// Just do nothing on mutate.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyShaderInstance : ShaderInstance
|
||||
@@ -434,7 +449,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
}
|
||||
|
||||
public IRenderTexture RenderTarget { get; } = new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
|
||||
public IRenderTexture RenderTarget { get; } =
|
||||
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
|
||||
|
||||
public IEye? Eye { get; set; }
|
||||
public Vector2i Size { get; }
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
|
||||
// xy: A, zw: B
|
||||
varying highp vec4 fragPos;
|
||||
// x: actual angle, y: horizontal (1) / vertical (-1)
|
||||
varying highp vec2 fragAngle;
|
||||
// x: Angle being queried, y: Angle of closest point of line (is of 90-degree angle to line angle), z: Distance at y
|
||||
varying highp vec3 fragControl;
|
||||
|
||||
void main()
|
||||
{
|
||||
// Stuff that needs to be inferred to avoid interpolation issues.
|
||||
highp vec2 rayNormal = vec2(cos(fragAngle.x), -sin(fragAngle.x));
|
||||
|
||||
// Depth calculation accounting for interpolation.
|
||||
highp float dist;
|
||||
|
||||
if (fragAngle.y > 0.0) {
|
||||
// Line is horizontal
|
||||
dist = abs(fragPos.y / rayNormal.y);
|
||||
} else {
|
||||
// Line is vertical
|
||||
dist = abs(fragPos.x / rayNormal.x);
|
||||
}
|
||||
// Thanks to Radrark for finding this for me. There's also a useful diagram, but this is text, so:
|
||||
// r = p / cos(theta - phi)
|
||||
// r: Distance to line *given angle theta*
|
||||
// p: Distance to closest point of line
|
||||
// theta: Angle being queried
|
||||
// phi: Angle of closest point of line - inherently on 90-degree angle to line angle
|
||||
highp float dist = abs(fragControl.z / cos(fragControl.x - fragControl.y));
|
||||
|
||||
// Main body.
|
||||
#ifdef HAS_DFDX
|
||||
|
||||
@@ -11,10 +11,8 @@ attribute vec4 aPos;
|
||||
// x: deflection(0=A/1=B) y: height
|
||||
attribute vec2 subVertex;
|
||||
|
||||
// xy: A, zw: B
|
||||
varying vec4 fragPos;
|
||||
// x: actual angle, y: horizontal (1) / vertical (-1)
|
||||
varying vec2 fragAngle;
|
||||
// x: actual angle, y: line angle + 90 degrees, z: Distance at y
|
||||
varying vec3 fragControl;
|
||||
|
||||
// Note: This is *not* the standard projectionMatrix!
|
||||
uniform vec2 shadowLightCentre;
|
||||
@@ -70,8 +68,19 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
fragPos = vec4(pA, pB);
|
||||
fragAngle = vec2(mix(xA, xB, subVertex.x), abs(pA.x - pB.x) - abs(pA.y - pB.y));
|
||||
float targetAngle = mix(xA, xB, subVertex.x);
|
||||
|
||||
// Calculate the necessary control data for the fragment shader.
|
||||
vec2 lineNormal = pB - pA; // hypothetical: <- would have negative X, zero Y
|
||||
lineNormal /= length(lineNormal);
|
||||
fragControl = vec3(
|
||||
// Angle
|
||||
targetAngle,
|
||||
// Angle Out
|
||||
atan(lineNormal.x, lineNormal.y),
|
||||
// Distance @ Angle Out
|
||||
dot(vec2(lineNormal.y, -lineNormal.x), pA)
|
||||
);
|
||||
|
||||
// Depth divide MUST be implemented here no matter what,
|
||||
// because GLES SL 1.00 doesn't have gl_FragDepth.
|
||||
@@ -79,5 +88,5 @@ void main()
|
||||
// and we don't really need to have correction
|
||||
float zbufferDepth = 1.0 - (1.0 / (length(mix(pA, pB, subVertex.x)) + DEPTH_ZBUFFER_PREDIV_BIAS));
|
||||
|
||||
gl_Position = vec4(mix(xA, xB, subVertex.x) / PI, mix(1.0, -1.0, subVertex.y), zbufferDepth, 1.0);
|
||||
gl_Position = vec4(targetAngle / PI, mix(1.0, -1.0, subVertex.y), zbufferDepth, 1.0);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Robust.Client.Graphics
|
||||
_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;
|
||||
@@ -76,6 +77,10 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void MaxLightsPerSceneChanged(int newValue)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void SoftShadowsChanged(bool newValue)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
@@ -17,7 +18,11 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
internal sealed class FontManager : IFontManagerInternal
|
||||
{
|
||||
private const int SheetWidth = 256;
|
||||
private const int SheetHeight = 256;
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
private uint BaseFontDPI;
|
||||
|
||||
@@ -51,8 +56,7 @@ namespace Robust.Client.Graphics
|
||||
return instance;
|
||||
}
|
||||
|
||||
var glyphMap = _generateGlyphMap(fontFaceHandle.Face);
|
||||
instance = new FontInstanceHandle(this, size, glyphMap, fontFaceHandle);
|
||||
instance = new FontInstanceHandle(this, size, fontFaceHandle);
|
||||
|
||||
_loadedInstances.Add((fontFaceHandle, size), instance);
|
||||
return instance;
|
||||
@@ -67,135 +71,128 @@ namespace Robust.Client.Graphics
|
||||
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
|
||||
var lineHeight = ftFace.Size.Metrics.Height.ToInt32();
|
||||
|
||||
var (atlas, metricsMap) = _generateAtlas(instance, scale);
|
||||
var data = new ScaledFontData(ascent, descent, ascent + descent, lineHeight);
|
||||
|
||||
return new ScaledFontData(metricsMap, ascent, descent, ascent + descent, lineHeight, atlas);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private (FontTextureAtlas, Dictionary<uint, CharMetrics> metricsMap)
|
||||
_generateAtlas(FontInstanceHandle instance, float scale)
|
||||
private void CacheGlyph(FontInstanceHandle instance, ScaledFontData scaled, float scale, uint glyph)
|
||||
{
|
||||
// TODO: This could use a better box packing algorithm.
|
||||
// Right now we treat each glyph bitmap as having the max size among all glyphs.
|
||||
// So we can divide the atlas into equal-size rectangles.
|
||||
// This wastes a lot of space though because there's a lot of tiny glyphs.
|
||||
// Check if already cached.
|
||||
if (scaled.AtlasData.ContainsKey(glyph))
|
||||
return;
|
||||
|
||||
var face = instance.FaceHandle.Face;
|
||||
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
|
||||
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
|
||||
face.Glyph.RenderGlyph(RenderMode.Normal);
|
||||
|
||||
var maxGlyphSize = Vector2i.Zero;
|
||||
var count = 0;
|
||||
var glyphMetrics = face.Glyph.Metrics;
|
||||
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
|
||||
glyphMetrics.HorizontalBearingY.ToInt32(),
|
||||
glyphMetrics.HorizontalAdvance.ToInt32(),
|
||||
glyphMetrics.Width.ToInt32(),
|
||||
glyphMetrics.Height.ToInt32());
|
||||
|
||||
var metricsMap = new Dictionary<uint, CharMetrics>();
|
||||
|
||||
foreach (var glyph in instance.GlyphMap.Values)
|
||||
using var bitmap = face.Glyph.Bitmap;
|
||||
if (bitmap.Pitch < 0)
|
||||
{
|
||||
if (metricsMap.ContainsKey(glyph))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
|
||||
face.Glyph.RenderGlyph(RenderMode.Normal);
|
||||
|
||||
var glyphMetrics = face.Glyph.Metrics;
|
||||
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
|
||||
glyphMetrics.HorizontalBearingY.ToInt32(),
|
||||
glyphMetrics.HorizontalAdvance.ToInt32(),
|
||||
glyphMetrics.Width.ToInt32(),
|
||||
glyphMetrics.Height.ToInt32());
|
||||
metricsMap.Add(glyph, metrics);
|
||||
|
||||
maxGlyphSize = Vector2i.ComponentMax(maxGlyphSize,
|
||||
new Vector2i(face.Glyph.Bitmap.Width, face.Glyph.Bitmap.Rows));
|
||||
|
||||
count += 1;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Make atlas.
|
||||
// This is the same algorithm used for RSIs. Tries to keep width and height as close as possible,
|
||||
// but preferring to increase width if necessary.
|
||||
var atlasEntriesHorizontal = (int) Math.Ceiling(Math.Sqrt(count));
|
||||
var atlasEntriesVertical =
|
||||
(int) Math.Ceiling(count / (float) atlasEntriesHorizontal);
|
||||
var atlasDimX =
|
||||
(int) Math.Ceiling(atlasEntriesHorizontal * maxGlyphSize.X / 4f) * 4;
|
||||
var atlasDimY =
|
||||
(int) Math.Ceiling(atlasEntriesVertical * maxGlyphSize.Y / 4f) * 4;
|
||||
|
||||
using (var atlas = new Image<A8>(atlasDimX, atlasDimY))
|
||||
if (bitmap.Pitch != 0)
|
||||
{
|
||||
var atlasRegions = new Dictionary<uint, UIBox2>();
|
||||
count = 0;
|
||||
foreach (var glyph in metricsMap.Keys)
|
||||
Image<A8> img;
|
||||
switch (bitmap.PixelMode)
|
||||
{
|
||||
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
|
||||
face.Glyph.RenderGlyph(RenderMode.Normal);
|
||||
|
||||
var bitmap = face.Glyph.Bitmap;
|
||||
if (bitmap.Pitch == 0)
|
||||
case PixelMode.Mono:
|
||||
{
|
||||
count += 1;
|
||||
continue;
|
||||
img = MonoBitMapToImage(bitmap);
|
||||
break;
|
||||
}
|
||||
|
||||
if (bitmap.Pitch < 0)
|
||||
case PixelMode.Gray:
|
||||
{
|
||||
ReadOnlySpan<A8> span;
|
||||
unsafe
|
||||
{
|
||||
span = new ReadOnlySpan<A8>((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows);
|
||||
}
|
||||
|
||||
img = new Image<A8>(bitmap.Width, bitmap.Rows);
|
||||
|
||||
span.Blit(
|
||||
bitmap.Pitch,
|
||||
UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows),
|
||||
img,
|
||||
(0, 0));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PixelMode.Gray2:
|
||||
case PixelMode.Gray4:
|
||||
case PixelMode.Lcd:
|
||||
case PixelMode.VerticalLcd:
|
||||
case PixelMode.Bgra:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
var column = count % atlasEntriesHorizontal;
|
||||
var row = count / atlasEntriesVertical;
|
||||
var offsetX = column * maxGlyphSize.X;
|
||||
var offsetY = row * maxGlyphSize.Y;
|
||||
count += 1;
|
||||
atlasRegions.Add(glyph, UIBox2i.FromDimensions(offsetX, offsetY, bitmap.Width, bitmap.Rows));
|
||||
|
||||
switch (bitmap.PixelMode)
|
||||
{
|
||||
case PixelMode.Mono:
|
||||
{
|
||||
using (var bitmapImage = MonoBitMapToImage(bitmap))
|
||||
{
|
||||
bitmapImage.Blit(new UIBox2i(0, 0, bitmapImage.Width, bitmapImage.Height), atlas,
|
||||
(offsetX, offsetY));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PixelMode.Gray:
|
||||
{
|
||||
ReadOnlySpan<A8> span;
|
||||
unsafe
|
||||
{
|
||||
span = new ReadOnlySpan<A8>((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows);
|
||||
}
|
||||
|
||||
span.Blit(bitmap.Pitch, UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows), atlas,
|
||||
(offsetX, offsetY));
|
||||
break;
|
||||
}
|
||||
|
||||
case PixelMode.Gray2:
|
||||
case PixelMode.Gray4:
|
||||
case PixelMode.Lcd:
|
||||
case PixelMode.VerticalLcd:
|
||||
case PixelMode.Bgra:
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var atlasDictionary = new Dictionary<uint, AtlasTexture>();
|
||||
var texture = Texture.LoadFromImage(atlas, $"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}");
|
||||
OwnedTexture sheet;
|
||||
if (scaled.AtlasTextures.Count == 0)
|
||||
sheet = GenSheet();
|
||||
else
|
||||
sheet = scaled.AtlasTextures[^1];
|
||||
|
||||
foreach (var (glyph, region) in atlasRegions)
|
||||
var (sheetW, sheetH) = sheet.Size;
|
||||
|
||||
if (sheetW - scaled.CurSheetX < img.Width)
|
||||
{
|
||||
atlasDictionary.Add(glyph, new AtlasTexture(texture, region));
|
||||
scaled.CurSheetX = 0;
|
||||
scaled.CurSheetY = scaled.CurSheetMaxY;
|
||||
}
|
||||
|
||||
return (new FontTextureAtlas(texture, atlasDictionary), metricsMap);
|
||||
if (sheetH - scaled.CurSheetY < img.Height)
|
||||
{
|
||||
// Make new sheet.
|
||||
scaled.CurSheetY = 0;
|
||||
scaled.CurSheetX = 0;
|
||||
scaled.CurSheetMaxY = 0;
|
||||
|
||||
sheet = GenSheet();
|
||||
}
|
||||
|
||||
sheet.SetSubImage((scaled.CurSheetX, scaled.CurSheetY), img);
|
||||
|
||||
var atlasTexture = new AtlasTexture(
|
||||
sheet,
|
||||
UIBox2.FromDimensions(
|
||||
scaled.CurSheetX,
|
||||
scaled.CurSheetY,
|
||||
bitmap.Width,
|
||||
bitmap.Rows));
|
||||
|
||||
scaled.AtlasData.Add(glyph, atlasTexture);
|
||||
|
||||
scaled.CurSheetMaxY = Math.Max(scaled.CurSheetMaxY, scaled.CurSheetY + bitmap.Rows);
|
||||
scaled.CurSheetX += bitmap.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
scaled.AtlasData.Add(glyph, null);
|
||||
}
|
||||
|
||||
scaled.MetricsMap.Add(glyph, metrics);
|
||||
|
||||
OwnedTexture GenSheet()
|
||||
{
|
||||
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
|
||||
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
|
||||
scaled.AtlasTextures.Add(sheet);
|
||||
return sheet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,46 +223,6 @@ namespace Robust.Client.Graphics
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
private Dictionary<char, uint> _generateGlyphMap(Face face)
|
||||
{
|
||||
var map = new Dictionary<char, uint>();
|
||||
|
||||
// TODO: Render more than extended ASCII, Cyrillic and Greek. somehow.
|
||||
// Does it make sense to just render every glyph in the font?
|
||||
|
||||
// Render all the extended ASCII characters.
|
||||
// Yeah I know "extended ASCII" isn't a real thing get off my back.
|
||||
for (var i = 32u; i <= 255; i++)
|
||||
{
|
||||
_addGlyph(i, face, map);
|
||||
}
|
||||
|
||||
// Render basic cyrillic.
|
||||
for (var i = 0x0410u; i <= 0x044F; i++)
|
||||
{
|
||||
_addGlyph(i, face, map);
|
||||
}
|
||||
|
||||
// Render greek.
|
||||
for (var i = 0x03B1u; i <= 0x03C9; i++)
|
||||
{
|
||||
_addGlyph(i, face, map);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void _addGlyph(uint codePoint, Face face, Dictionary<char, uint> map)
|
||||
{
|
||||
var glyphIndex = face.GetCharIndex(codePoint);
|
||||
if (glyphIndex == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
map.Add((char) codePoint, glyphIndex);
|
||||
}
|
||||
|
||||
private class FontFaceHandle : IFontFaceHandle
|
||||
{
|
||||
public Face Face { get; }
|
||||
@@ -282,78 +239,84 @@ namespace Robust.Client.Graphics
|
||||
public FontFaceHandle FaceHandle { get; }
|
||||
public int Size { get; }
|
||||
private readonly Dictionary<float, ScaledFontData> _scaledData = new();
|
||||
public readonly IReadOnlyDictionary<char, uint> GlyphMap;
|
||||
private readonly FontManager _fontManager;
|
||||
public readonly Dictionary<Rune, uint> GlyphMap;
|
||||
|
||||
public FontInstanceHandle(FontManager fontManager, int size, IReadOnlyDictionary<char, uint> glyphMap,
|
||||
FontFaceHandle faceHandle)
|
||||
public FontInstanceHandle(FontManager fontManager, int size, FontFaceHandle faceHandle)
|
||||
{
|
||||
GlyphMap = new Dictionary<Rune, uint>();
|
||||
_fontManager = fontManager;
|
||||
Size = size;
|
||||
GlyphMap = glyphMap;
|
||||
FaceHandle = faceHandle;
|
||||
}
|
||||
|
||||
public Texture? GetCharTexture(char chr, float scale)
|
||||
public Texture? GetCharTexture(Rune codePoint, float scale)
|
||||
{
|
||||
var glyph = _getGlyph(chr);
|
||||
var glyph = GetGlyph(codePoint);
|
||||
if (glyph == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var scaled = _getScaleDatum(scale);
|
||||
scaled.Atlas.AtlasData.TryGetValue(glyph, out var texture);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
_fontManager.CacheGlyph(this, scaled, scale, glyph);
|
||||
|
||||
scaled.AtlasData.TryGetValue(glyph, out var texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public CharMetrics? GetCharMetrics(char chr, float scale)
|
||||
public CharMetrics? GetCharMetrics(Rune codePoint, float scale)
|
||||
{
|
||||
var glyph = _getGlyph(chr);
|
||||
var glyph = GetGlyph(codePoint);
|
||||
if (glyph == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var scaled = _getScaleDatum(scale);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
_fontManager.CacheGlyph(this, scaled, scale, glyph);
|
||||
|
||||
return scaled.MetricsMap[glyph];
|
||||
}
|
||||
|
||||
public int GetAscent(float scale)
|
||||
{
|
||||
var scaled = _getScaleDatum(scale);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
return scaled.Ascent;
|
||||
}
|
||||
|
||||
public int GetDescent(float scale)
|
||||
{
|
||||
var scaled = _getScaleDatum(scale);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
return scaled.Descent;
|
||||
}
|
||||
|
||||
public int GetHeight(float scale)
|
||||
{
|
||||
var scaled = _getScaleDatum(scale);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
return scaled.Height;
|
||||
}
|
||||
|
||||
public int GetLineHeight(float scale)
|
||||
{
|
||||
var scaled = _getScaleDatum(scale);
|
||||
var scaled = GetScaleDatum(scale);
|
||||
return scaled.LineHeight;
|
||||
}
|
||||
|
||||
private uint _getGlyph(char chr)
|
||||
private uint GetGlyph(Rune chr)
|
||||
{
|
||||
if (GlyphMap.TryGetValue(chr, out var glyph))
|
||||
{
|
||||
return glyph;
|
||||
}
|
||||
|
||||
return 0;
|
||||
// Check FreeType to see if it exists.
|
||||
var index = FaceHandle.Face.GetCharIndex((uint) chr.Value);
|
||||
|
||||
GlyphMap.Add(chr, index);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private ScaledFontData _getScaleDatum(float scale)
|
||||
private ScaledFontData GetScaleDatum(float scale)
|
||||
{
|
||||
if (_scaledData.TryGetValue(scale, out var datum))
|
||||
{
|
||||
@@ -368,37 +331,25 @@ namespace Robust.Client.Graphics
|
||||
|
||||
private class ScaledFontData
|
||||
{
|
||||
public ScaledFontData(IReadOnlyDictionary<uint, CharMetrics> metricsMap, int ascent, int descent,
|
||||
int height, int lineHeight, FontTextureAtlas atlas)
|
||||
public ScaledFontData(int ascent, int descent, int height, int lineHeight)
|
||||
{
|
||||
MetricsMap = metricsMap;
|
||||
Ascent = ascent;
|
||||
Descent = descent;
|
||||
Height = height;
|
||||
LineHeight = lineHeight;
|
||||
Atlas = atlas;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<uint, CharMetrics> MetricsMap { get; }
|
||||
public int Ascent { get; }
|
||||
public int Descent { get; }
|
||||
public int Height { get; }
|
||||
public int LineHeight { get; }
|
||||
public FontTextureAtlas Atlas { get; }
|
||||
}
|
||||
public readonly List<OwnedTexture> AtlasTextures = new();
|
||||
public readonly Dictionary<uint, AtlasTexture?> AtlasData = new();
|
||||
public readonly Dictionary<uint, CharMetrics> MetricsMap = new();
|
||||
public readonly int Ascent;
|
||||
public readonly int Descent;
|
||||
public readonly int Height;
|
||||
public readonly int LineHeight;
|
||||
|
||||
private class FontTextureAtlas
|
||||
{
|
||||
public FontTextureAtlas(Texture mainTexture, Dictionary<uint, AtlasTexture> atlasData)
|
||||
{
|
||||
MainTexture = mainTexture;
|
||||
AtlasData = atlasData;
|
||||
}
|
||||
|
||||
public Texture MainTexture { get; }
|
||||
|
||||
// Maps glyph index to atlas.
|
||||
public Dictionary<uint, AtlasTexture> AtlasData { get; }
|
||||
public int CurSheetX;
|
||||
public int CurSheetY;
|
||||
public int CurSheetMaxY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
Robust.Client/Graphics/IRsiStateLike.cs
Normal file
12
Robust.Client/Graphics/IRsiStateLike.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
public interface IRsiStateLike : IDirectionalTextureProvider
|
||||
{
|
||||
RSI.State.DirectionType Directions { get; }
|
||||
bool IsAnimated { get; }
|
||||
int AnimationFrameCount { get; }
|
||||
|
||||
float GetDelay(int frame);
|
||||
Texture GetFrame(RSI.State.Direction dir, int frame);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.Interfaces.Graphics.Lighting;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics.Lighting
|
||||
{
|
||||
@@ -7,5 +8,8 @@ namespace Robust.Client.Graphics.Lighting
|
||||
public bool Enabled { get; set; } = true;
|
||||
public bool DrawShadows { get; set; } = true;
|
||||
public bool DrawHardFov { get; set; } = true;
|
||||
public bool DrawLighting { get; set; } = true;
|
||||
public bool LockConsoleAccess { get; set; } = false;
|
||||
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -12,21 +14,42 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a sub area of the texture with new data.
|
||||
/// </summary>
|
||||
/// <param name="topLeft">The top left corner of the area to modify.</param>
|
||||
/// <param name="sourceImage">The image from which to copy pixel data.</param>
|
||||
/// <param name="sourceRegion">The rectangle inside <paramref name="sourceImage"/> from which to copy.</param>
|
||||
/// <typeparam name="T">
|
||||
/// The type of pixels being used.
|
||||
/// This must match the type used when creating the texture.
|
||||
/// </typeparam>
|
||||
public abstract void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
|
||||
where T : unmanaged, IPixel<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a sub area of the texture with new data.
|
||||
/// </summary>
|
||||
/// <param name="topLeft">The top left corner of the area to modify.</param>
|
||||
/// <param name="sourceImage">The image to paste onto the texture.</param>
|
||||
/// <typeparam name="T">
|
||||
/// The type of pixels being used.
|
||||
/// This must match the type used when creating the texture.
|
||||
/// </typeparam>
|
||||
public void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
SetSubImage(topLeft, sourceImage, UIBox2i.FromDimensions(0, 0, sourceImage.Width, sourceImage.Height));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
[Obsolete("Use Dispose() instead")]
|
||||
public void Delete()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
~OwnedTexture()
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Robust.Client.Graphics
|
||||
/// RSIs are folded into a single set of animation timings when loaded.
|
||||
/// This is to simplify animation playback code in-engine.
|
||||
/// </remarks>
|
||||
public sealed class State : IDirectionalTextureProvider
|
||||
public sealed class State : IRsiStateLike
|
||||
{
|
||||
// List of delays for the frame to reach the next frame.
|
||||
private readonly float[] Delays;
|
||||
@@ -80,6 +80,8 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public bool IsAnimated => DelayCount > 1;
|
||||
|
||||
int IRsiStateLike.AnimationFrameCount => DelayCount;
|
||||
|
||||
public Texture GetFrame(Direction direction, int frame)
|
||||
{
|
||||
return Icons[(int) direction][frame];
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name","directions"] //'delays' is marked as optional in the spec
|
||||
"required": ["name"] //'delays' is marked as optional in the spec
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Client.Graphics
|
||||
/// Contains a texture used for drawing things.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public abstract class Texture : IDirectionalTextureProvider
|
||||
public abstract class Texture : IRsiStateLike
|
||||
{
|
||||
/// <summary>
|
||||
/// The width of the texture, in pixels.
|
||||
@@ -85,6 +85,26 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
RSI.State.DirectionType IRsiStateLike.Directions => RSI.State.DirectionType.Dir1;
|
||||
bool IRsiStateLike.IsAnimated => false;
|
||||
int IRsiStateLike.AnimationFrameCount => 0;
|
||||
|
||||
float IRsiStateLike.GetDelay(int frame)
|
||||
{
|
||||
if (frame != 0)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Texture IRsiStateLike.GetFrame(RSI.State.Direction dir, int frame)
|
||||
{
|
||||
if (frame != 0)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -134,7 +134,8 @@ namespace Robust.Client.Input
|
||||
Priority = p.Priority,
|
||||
Type = p.BindingType,
|
||||
CanFocus = p.CanFocus,
|
||||
CanRepeat = p.CanRepeat
|
||||
CanRepeat = p.CanRepeat,
|
||||
AllowSubCombs = p.AllowSubCombs
|
||||
}).ToArray();
|
||||
|
||||
var leaveEmpty = _modifiedKeyFunctions
|
||||
@@ -203,6 +204,7 @@ namespace Robust.Client.Input
|
||||
|
||||
var bindsDown = new List<KeyBinding>();
|
||||
var hasCanFocus = false;
|
||||
var hasAllowSubCombs = false;
|
||||
|
||||
// bindings are ordered with larger combos before single key bindings so combos have priority.
|
||||
foreach (var binding in _bindings)
|
||||
@@ -221,12 +223,22 @@ namespace Robust.Client.Input
|
||||
matchedCombo = binding.PackedKeyCombo;
|
||||
|
||||
bindsDown.Add(binding);
|
||||
|
||||
hasCanFocus |= binding.CanFocus;
|
||||
hasAllowSubCombs |= binding.AllowSubCombs;
|
||||
|
||||
}
|
||||
else if (PackedIsSubPattern(matchedCombo, binding.PackedKeyCombo))
|
||||
{
|
||||
// kill any lower level matches
|
||||
UpBind(binding);
|
||||
if (hasAllowSubCombs)
|
||||
{
|
||||
bindsDown.Add(binding);
|
||||
}
|
||||
else
|
||||
{
|
||||
// kill any lower level matches
|
||||
UpBind(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,8 +390,8 @@ namespace Robust.Client.Input
|
||||
{
|
||||
for (var i = 0; i < 32; i += 8)
|
||||
{
|
||||
var key = (Key) (subPackedCombo.Packed >> i);
|
||||
if (!PackedContainsKey(packedCombo, key))
|
||||
var key = (Key) ((subPackedCombo.Packed >> i) & 0b_1111_1111);
|
||||
if (key != Key.Unknown && !PackedContainsKey(packedCombo, key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -453,7 +465,7 @@ namespace Robust.Client.Input
|
||||
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
|
||||
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
|
||||
{
|
||||
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false,
|
||||
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false,
|
||||
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
|
||||
|
||||
RegisterBinding(binding);
|
||||
@@ -464,7 +476,7 @@ namespace Robust.Client.Input
|
||||
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
|
||||
{
|
||||
var binding = new KeyBinding(this, reg.Function, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
|
||||
reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
|
||||
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
|
||||
|
||||
RegisterBinding(binding, markModified);
|
||||
|
||||
@@ -619,12 +631,18 @@ namespace Robust.Client.Input
|
||||
[ViewVariables]
|
||||
public bool CanRepeat { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Bound Key Combination allows Sub Combinations of it to trigger.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool AllowSubCombs { get; internal set; }
|
||||
|
||||
[ViewVariables] public int Priority { get; internal set; }
|
||||
|
||||
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
|
||||
KeyBindingType bindingType,
|
||||
Key baseKey,
|
||||
bool canFocus, bool canRepeat, int priority, Key mod1 = Key.Unknown,
|
||||
bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown,
|
||||
Key mod2 = Key.Unknown,
|
||||
Key mod3 = Key.Unknown)
|
||||
{
|
||||
@@ -632,6 +650,7 @@ namespace Robust.Client.Input
|
||||
BindingType = bindingType;
|
||||
CanFocus = canFocus;
|
||||
CanRepeat = canRepeat;
|
||||
AllowSubCombs = allowSubCombs;
|
||||
Priority = priority;
|
||||
_inputManager = inputManager;
|
||||
|
||||
|
||||
@@ -34,6 +34,29 @@ namespace Robust.Client.Interfaces.Graphics
|
||||
Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a blank texture of the specified parameters.
|
||||
/// This texture can later be modified using <see cref="OwnedTexture.SetSubImage{T}"/>
|
||||
/// </summary>
|
||||
/// <param name="size">The size of the new texture, in pixels.</param>
|
||||
/// <param name="name">A name for the texture that can show up in debugging tools like renderdoc.</param>
|
||||
/// <param name="loadParams">
|
||||
/// Load parameters for the texture describing stuff such as sample mode.
|
||||
/// </param>
|
||||
/// <typeparam name="T">
|
||||
/// The type of pixels to "store" in the texture.
|
||||
/// This is the same type you should pass to <see cref="OwnedTexture.SetSubImage{T}"/>,
|
||||
/// and also determines how the texture is stored internally.
|
||||
/// </typeparam>
|
||||
/// <returns>
|
||||
/// An owned, mutable texture object.
|
||||
/// </returns>
|
||||
OwnedTexture CreateBlankTexture<T>(
|
||||
Vector2i size,
|
||||
string? name = null,
|
||||
in TextureLoadParameters? loadParams = null)
|
||||
where T : unmanaged, IPixel<T>;
|
||||
|
||||
IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
|
||||
TextureSampleParameters? sampleParameters = null, string? name = null);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Robust.Client.Interfaces.Graphics
|
||||
@@ -22,8 +23,13 @@ namespace Robust.Client.Interfaces.Graphics
|
||||
|
||||
internal interface IFontInstanceHandle
|
||||
{
|
||||
Texture? GetCharTexture(char chr, float scale);
|
||||
CharMetrics? GetCharMetrics(char chr, float scale);
|
||||
|
||||
|
||||
Texture? GetCharTexture(Rune codePoint, float scale);
|
||||
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
|
||||
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
|
||||
CharMetrics? GetCharMetrics(char chr, float scale) => GetCharMetrics((Rune) chr, scale);
|
||||
|
||||
int GetAscent(float scale);
|
||||
int GetDescent(float scale);
|
||||
int GetHeight(float scale);
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Interfaces.Graphics.Lighting
|
||||
{
|
||||
public interface ILightManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables/disables the entire light manager.
|
||||
/// </summary>
|
||||
bool Enabled { get; set; }
|
||||
/// <summary>
|
||||
/// Enables/disables shadows, but lights are still functional.
|
||||
/// </summary>
|
||||
bool DrawShadows { get; set; }
|
||||
/// <summary>
|
||||
/// Enables/disables hard FOV.
|
||||
/// </summary>
|
||||
bool DrawHardFov { get; set; }
|
||||
/// <summary>
|
||||
/// Enables/disables everything to do with the lighting buffer, without interfering with hard FOV.
|
||||
/// </summary>
|
||||
bool DrawLighting { get; set; }
|
||||
/// <summary>
|
||||
/// This is useful to prevent players messing with lighting setup when they shouldn't.
|
||||
/// </summary>
|
||||
bool LockConsoleAccess { get; set; }
|
||||
/// <summary>
|
||||
/// Ambient light. This is in linear-light, i.e. when providing a fixed colour, you must use Color.FromSrgb(Color.Black)!
|
||||
/// </summary>
|
||||
Color AmbientLightColor { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Robust.Client.Interfaces.Input
|
||||
|
||||
bool CanFocus { get; }
|
||||
bool CanRepeat { get; }
|
||||
bool AllowSubCombs { get; }
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -16,6 +16,7 @@ namespace Robust.Client.Interfaces.Input
|
||||
public int Priority;
|
||||
public bool CanFocus;
|
||||
public bool CanRepeat;
|
||||
public bool AllowSubCombs;
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
@@ -28,6 +29,7 @@ namespace Robust.Client.Interfaces.Input
|
||||
serializer.DataField(ref Priority, "priority", 0);
|
||||
serializer.DataField(ref CanFocus, "canFocus", false);
|
||||
serializer.DataField(ref CanRepeat, "canRepeat", false);
|
||||
serializer.DataField(ref AllowSubCombs, "allowSubCombs", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,22 @@ namespace Robust.Client.Interfaces.UserInterface
|
||||
/// </summary>
|
||||
Stylesheet? Stylesheet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A control can have "keyboard focus" separate from ControlFocused, obtained when calling
|
||||
/// Control.GrabKeyboardFocus. Corresponding events in Control are KeyboardFocusEntered/Exited
|
||||
/// </summary>
|
||||
Control? KeyboardFocused { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A control gets "ControlFocused" when a mouse button (or any KeyBinding which has CanFocus = true) is
|
||||
/// pressed down on the control. While it is focused, it will receive mouse hover events and the corresponding
|
||||
/// keyup event if it still has focus when that occurs (it will NOT receive the keyup if focus has
|
||||
/// been taken by another control). Focus is removed when a different control takes focus
|
||||
/// (such as by pressing a different mouse button down over a different control) or when the keyup event
|
||||
/// happens. When focus is lost on a control, it always fires Control.ControlFocusExited.
|
||||
/// </summary>
|
||||
Control? ControlFocused { get; }
|
||||
|
||||
LayoutContainer StateRoot { get; }
|
||||
|
||||
LayoutContainer WindowRoot { get; }
|
||||
|
||||
@@ -98,9 +98,9 @@ namespace Robust.Client.Placement
|
||||
public bool Eraser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The texture we use to show from our placement manager to represent the entity to place
|
||||
/// The texture we use to show from our placement manager to represent the entity to place
|
||||
/// </summary>
|
||||
public IDirectionalTextureProvider? CurrentBaseSprite { get; set; }
|
||||
public List<IDirectionalTextureProvider>? CurrentTextures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which of the placement orientations we are trying to place with
|
||||
@@ -311,7 +311,7 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
PlacementChanged?.Invoke(this, EventArgs.Empty);
|
||||
Hijack = null;
|
||||
CurrentBaseSprite = null;
|
||||
CurrentTextures = null;
|
||||
CurrentPrototype = null;
|
||||
CurrentPermission = null;
|
||||
CurrentMode = null;
|
||||
@@ -555,7 +555,7 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
var prototype = _prototypeManager.Index<EntityPrototype>(templateName);
|
||||
|
||||
CurrentBaseSprite = SpriteComponent.GetPrototypeIcon(prototype, ResourceCache);
|
||||
CurrentTextures = SpriteComponent.GetPrototypeTextures(prototype, ResourceCache).ToList();
|
||||
CurrentPrototype = prototype;
|
||||
|
||||
IsActive = true;
|
||||
@@ -563,8 +563,9 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void PreparePlacementTile()
|
||||
{
|
||||
CurrentBaseSprite = ResourceCache
|
||||
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture;
|
||||
CurrentTextures = new List<IDirectionalTextureProvider>
|
||||
{ResourceCache
|
||||
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture};
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.ClientEye;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
@@ -28,9 +29,9 @@ namespace Robust.Client.Placement
|
||||
public EntityCoordinates MouseCoords { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture resource to draw to represent the entity we are tryign to spawn
|
||||
/// Texture resources to draw to represent the entity we are trying to spawn
|
||||
/// </summary>
|
||||
public Texture? SpriteToDraw { get; set; }
|
||||
public List<Texture>? TexturesToDraw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Color set to the ghost entity when it has a valid spawn position
|
||||
@@ -85,12 +86,15 @@ namespace Robust.Client.Placement
|
||||
|
||||
public virtual void Render(DrawingHandleWorld handle)
|
||||
{
|
||||
if (SpriteToDraw == null)
|
||||
if (TexturesToDraw == null)
|
||||
{
|
||||
SetSprite();
|
||||
DebugTools.AssertNotNull(SpriteToDraw);
|
||||
DebugTools.AssertNotNull(TexturesToDraw);
|
||||
}
|
||||
|
||||
if (TexturesToDraw == null || TexturesToDraw.Count == 0)
|
||||
return;
|
||||
|
||||
IEnumerable<EntityCoordinates> locationcollection;
|
||||
switch (pManager.PlacementType)
|
||||
{
|
||||
@@ -108,13 +112,17 @@ namespace Robust.Client.Placement
|
||||
break;
|
||||
}
|
||||
|
||||
var size = SpriteToDraw!.Size;
|
||||
var size = TexturesToDraw[0].Size;
|
||||
foreach (var coordinate in locationcollection)
|
||||
{
|
||||
var worldPos = coordinate.ToMapPos(pManager.EntityManager);
|
||||
var pos = worldPos - (size/(float)EyeManager.PixelsPerMeter) / 2f;
|
||||
var color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
handle.DrawTexture(SpriteToDraw, pos, color);
|
||||
|
||||
foreach (var texture in TexturesToDraw)
|
||||
{
|
||||
handle.DrawTexture(texture, pos, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +196,10 @@ namespace Robust.Client.Placement
|
||||
|
||||
public void SetSprite()
|
||||
{
|
||||
SpriteToDraw = pManager.CurrentBaseSprite!.TextureFor(pManager.Direction);
|
||||
if (pManager.CurrentTextures == null)
|
||||
return;
|
||||
|
||||
TexturesToDraw = pManager.CurrentTextures.Select(o => o.TextureFor(pManager.Direction)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,12 +13,15 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public override void Load(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
if (!cache.ContentFileExists(path))
|
||||
if (!cache.TryContentFileRead(path, out var stream))
|
||||
{
|
||||
throw new FileNotFoundException("Content file does not exist for font");
|
||||
}
|
||||
|
||||
FontFaceHandle = IoCManager.Resolve<IFontManagerInternal>().Load(cache.ContentFileRead(path));
|
||||
using (stream)
|
||||
{
|
||||
FontFaceHandle = IoCManager.Resolve<IFontManagerInternal>().Load(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public VectorFont MakeDefault()
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
@@ -83,7 +82,12 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
// Load image from disk.
|
||||
var texPath = path / (stateObject.StateId + ".png");
|
||||
var image = Image.Load<Rgba32>(cache.ContentFileRead(texPath));
|
||||
var stream = cache.ContentFileRead(texPath);
|
||||
Image<Rgba32> image;
|
||||
using (stream)
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
var sheetSize = new Vector2i(image.Width, image.Height);
|
||||
|
||||
if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0)
|
||||
@@ -341,15 +345,25 @@ namespace Robust.Client.ResourceManagement
|
||||
foreach (var stateObject in manifestJson["states"]!.Cast<JObject>())
|
||||
{
|
||||
var stateName = stateObject["name"]!.ToObject<string>()!;
|
||||
var dirValue = stateObject["directions"]!.ToObject<int>();
|
||||
RSI.State.DirectionType directions;
|
||||
int dirValue;
|
||||
|
||||
var directions = dirValue switch
|
||||
if (stateObject.TryGetValue("directions", out var dirJToken))
|
||||
{
|
||||
1 => RSI.State.DirectionType.Dir1,
|
||||
4 => RSI.State.DirectionType.Dir4,
|
||||
8 => RSI.State.DirectionType.Dir8,
|
||||
_ => throw new RSILoadException($"Invalid direction: {dirValue}")
|
||||
};
|
||||
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!
|
||||
|
||||
@@ -26,20 +26,23 @@ namespace Robust.Client.ResourceManagement
|
||||
throw new FileNotFoundException("Content file does not exist for texture");
|
||||
}
|
||||
|
||||
// Primarily for tracking down iCCP sRGB errors in the image files.
|
||||
Logger.DebugS("res.tex", $"Loading texture {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)
|
||||
using (stream)
|
||||
{
|
||||
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this));
|
||||
// Primarily for tracking down iCCP sRGB errors in the image files.
|
||||
Logger.DebugS("res.tex", $"Loading texture {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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,21 +51,25 @@ namespace Robust.Client.ResourceManagement
|
||||
var metaPath = path.WithName(path.Filename + ".yml");
|
||||
if (cache.TryContentFileRead(metaPath, out var stream))
|
||||
{
|
||||
YamlDocument yamlData;
|
||||
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
|
||||
using (stream)
|
||||
{
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
if (yamlStream.Documents.Count == 0)
|
||||
YamlDocument yamlData;
|
||||
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
|
||||
{
|
||||
return null;
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
if (yamlStream.Documents.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
yamlData = yamlStream.Documents[0];
|
||||
}
|
||||
|
||||
yamlData = yamlStream.Documents[0];
|
||||
return TextureLoadParameters.FromYaml((YamlMappingNode) yamlData.RootNode);
|
||||
}
|
||||
|
||||
return TextureLoadParameters.FromYaml((YamlMappingNode)yamlData.RootNode);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
DebugTools.Assert(!Disposed, "Control has been disposed.");
|
||||
|
||||
foreach (var child in Children.ToList())
|
||||
foreach (var child in Children.ToArray())
|
||||
{
|
||||
RemoveChild(child);
|
||||
}
|
||||
@@ -757,14 +757,32 @@ namespace Robust.Client.UserInterface
|
||||
/// <summary>
|
||||
/// Called when this control receives keyboard focus.
|
||||
/// </summary>
|
||||
protected internal virtual void FocusEntered()
|
||||
protected internal virtual void KeyboardFocusEntered()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this control loses keyboard focus.
|
||||
/// Called when this control loses keyboard focus (corresponds to UserInterfaceManager.KeyboardFocused).
|
||||
/// </summary>
|
||||
protected internal virtual void FocusExited()
|
||||
protected internal virtual void KeyboardFocusExited()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a control loses control focus for any reason. See <see cref="IUserInterfaceManager.ControlFocused"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Controls which have some sort of drag / drop behavior should usually implement this method (typically by cancelling the drag drop).
|
||||
/// Otherwise, if a user clicks down LMB over one control to initiate a drag, then clicks RMB down
|
||||
/// over a different control while still holding down LMB, the control being dragged will now lose focus
|
||||
/// and will no longer receive the keyup for the LMB, thus won't cancel the drag.
|
||||
/// This should also be considered for controls which have any special KeyBindUp behavior - consider
|
||||
/// what would happen if the control lost focus and never received the KeyBindUp.
|
||||
///
|
||||
/// There is no corresponding ControlFocusEntered - if a control wants to handle that situation they should simply
|
||||
/// handle KeyBindDown as that's the only way a control would gain focus.
|
||||
/// </remarks>
|
||||
protected internal virtual void ControlFocusExited()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
62
Robust.Client/UserInterface/Controls/AnimatedTextureRect.cs
Normal file
62
Robust.Client/UserInterface/Controls/AnimatedTextureRect.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// A more complex control wrapping <see cref="TextureRect"/> that can do RSI directions and animations.
|
||||
/// </summary>
|
||||
public sealed class AnimatedTextureRect : Control
|
||||
{
|
||||
private IRsiStateLike? _state;
|
||||
private int _curFrame;
|
||||
private float _curFrameTime;
|
||||
|
||||
/// <summary>
|
||||
/// Internal TextureRect used to do actual drawing of the texture.
|
||||
/// You can use this property to change shaders or styling or such.
|
||||
/// </summary>
|
||||
public TextureRect DisplayRect { get; }
|
||||
|
||||
public RSI.State.Direction RsiDirection { get; } = RSI.State.Direction.South;
|
||||
|
||||
public AnimatedTextureRect()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
DisplayRect = new TextureRect();
|
||||
AddChild(DisplayRect);
|
||||
}
|
||||
|
||||
public void SetFromSpriteSpecifier(SpriteSpecifier specifier)
|
||||
{
|
||||
_curFrame = 0;
|
||||
_state = specifier.RsiStateLike();
|
||||
_curFrameTime = _state.GetDelay(0);
|
||||
DisplayRect.Texture = _state.GetFrame(RsiDirection, 0);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (!VisibleInTree || _state == null || !_state.IsAnimated)
|
||||
return;
|
||||
|
||||
var oldFrame = _curFrame;
|
||||
|
||||
_curFrameTime -= args.DeltaSeconds;
|
||||
while (_curFrameTime < _state.GetDelay(_curFrame))
|
||||
{
|
||||
_curFrame = (_curFrame + 1) % _state.AnimationFrameCount;
|
||||
_curFrameTime += _state.GetDelay(_curFrame);
|
||||
}
|
||||
|
||||
if (_curFrame != oldFrame)
|
||||
{
|
||||
DisplayRect.Texture = _state.GetFrame(RsiDirection, _curFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,8 +446,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var (vSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
|
||||
var hSep = _limitDimension == Dimension.Column ? hSepActual : vSepActual;
|
||||
var vSep = _limitDimension == Dimension.Column ? vSepActual : hSepActual;
|
||||
var width = _limitDimension == Dimension.Column ? Width : Height;
|
||||
var height = _limitDimension == Dimension.Column ? Height : Width;
|
||||
var width = _limitDimension == Dimension.Column ? PixelWidth : PixelHeight;
|
||||
var height = _limitDimension == Dimension.Column ? PixelHeight : PixelWidth;
|
||||
|
||||
var stretchMaxX = width - hSep * (cols - 1);
|
||||
var stretchMaxY = height - vSep * (rows - 1);
|
||||
|
||||
@@ -433,7 +433,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (item.Region == null)
|
||||
continue;
|
||||
|
||||
if (!item.Region.Value.Contains(args.RelativePosition))
|
||||
if (!item.Region.Value.Contains(args.RelativePixelPosition))
|
||||
continue;
|
||||
|
||||
if (item.Selectable && !item.Disabled)
|
||||
|
||||
@@ -589,18 +589,18 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return index;
|
||||
}
|
||||
|
||||
protected internal override void FocusEntered()
|
||||
protected internal override void KeyboardFocusEntered()
|
||||
{
|
||||
base.FocusEntered();
|
||||
base.KeyboardFocusEntered();
|
||||
|
||||
// Reset this so the cursor is always visible immediately after gaining focus..
|
||||
_resetCursorBlink();
|
||||
OnFocusEnter?.Invoke(new LineEditEventArgs(this, _text));
|
||||
}
|
||||
|
||||
protected internal override void FocusExited()
|
||||
protected internal override void KeyboardFocusExited()
|
||||
{
|
||||
base.FocusExited();
|
||||
base.KeyboardFocusExited();
|
||||
OnFocusExit?.Invoke(new LineEditEventArgs(this, _text));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected override void LayoutUpdateOverride()
|
||||
{
|
||||
var contentBox = _getStyleBox()?.GetContentBox(PixelSizeBox) ?? SizeBox;
|
||||
var contentBox = _getStyleBox()?.GetContentBox(PixelSizeBox) ?? PixelSizeBox;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
base.Draw(handle);
|
||||
|
||||
var bg = _getBackground();
|
||||
bg?.Draw(handle, SizeBox);
|
||||
bg?.Draw(handle, PixelSizeBox);
|
||||
|
||||
var fg = _getForeground();
|
||||
if (fg == null)
|
||||
@@ -69,10 +69,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
var minSize = fg.MinimumSize;
|
||||
var size = Width * GetAsRatio() - minSize.X;
|
||||
var size = PixelWidth * GetAsRatio() - minSize.X;
|
||||
if (size > 0)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, Height));
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
264
Robust.Client/UserInterface/Controls/RadioOptions.cs
Normal file
264
Robust.Client/UserInterface/Controls/RadioOptions.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
public enum RadioOptionsLayout { Horizontal, Vertical }
|
||||
|
||||
public class RadioOptions<T> : Control
|
||||
{
|
||||
private int internalIdCount = 0;
|
||||
|
||||
private readonly List<RadioOptionButtonData<T>> _buttonDataList = new();
|
||||
//private readonly Dictionary<int, int> _idMap = new();
|
||||
private ButtonGroup _buttonGroup = new();
|
||||
private Container _container;
|
||||
|
||||
public string ButtonStyle = string.Empty;
|
||||
public string FirstButtonStyle = string.Empty;
|
||||
public string LastButtonStyle = string.Empty;
|
||||
|
||||
public int ItemCount => _buttonDataList.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever you select a button.
|
||||
///
|
||||
/// Note: You should add optionButtons.Select(args.Id); if you want to actually select the button.
|
||||
/// </summary>
|
||||
public event Action<RadioOptionItemSelectedEventArgs<T>>? OnItemSelected;
|
||||
|
||||
public RadioOptions(RadioOptionsLayout layout)
|
||||
{
|
||||
switch (layout)
|
||||
{
|
||||
case RadioOptionsLayout.Vertical:
|
||||
_container = new VBoxContainer();
|
||||
break;
|
||||
case RadioOptionsLayout.Horizontal:
|
||||
default:
|
||||
_container = new HBoxContainer();
|
||||
break;
|
||||
}
|
||||
|
||||
this.AddChild(_container);
|
||||
}
|
||||
|
||||
public int AddItem(string label, T value, Action<RadioOptionItemSelectedEventArgs<T>>? itemSelectedAction = null)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Text = label,
|
||||
Group = _buttonGroup
|
||||
};
|
||||
|
||||
button.OnPressed += ButtonOnPressed;
|
||||
|
||||
var data = new RadioOptionButtonData<T>(label, value, button)
|
||||
{
|
||||
Id = internalIdCount++
|
||||
};
|
||||
|
||||
if (itemSelectedAction != null)
|
||||
{
|
||||
data.OnItemSelected += itemSelectedAction;
|
||||
}
|
||||
|
||||
_buttonDataList.Add(data);
|
||||
_container.AddChild(button);
|
||||
UpdateFirstAndLastButtonStyle();
|
||||
|
||||
if (_buttonDataList.Count == 1)
|
||||
{
|
||||
Select(data.Id);
|
||||
}
|
||||
return data.Id;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This is triggered when the button is pressed via the UI
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
private void ButtonOnPressed(ButtonEventArgs obj)
|
||||
{
|
||||
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Button == obj.Button);
|
||||
if (buttonData != null)
|
||||
{
|
||||
InvokeItemSelected(new RadioOptionItemSelectedEventArgs<T>(buttonData.Id, this));
|
||||
return;
|
||||
}
|
||||
// Not reachable.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var buttonDatum in _buttonDataList)
|
||||
{
|
||||
buttonDatum.Button.OnPressed -= ButtonOnPressed;
|
||||
}
|
||||
_buttonDataList.Clear();
|
||||
_container.Children.Clear();
|
||||
SelectedId = 0;
|
||||
}
|
||||
|
||||
public object? GetItemMetadata(int idx)
|
||||
{
|
||||
return _buttonDataList.FirstOrDefault(bd => bd.Id == idx)?.Metadata;
|
||||
}
|
||||
|
||||
public int SelectedId { get; private set; }
|
||||
public RadioOptionButtonData<T> SelectedButtonData => _buttonDataList.First(bd => bd.Id == SelectedId);
|
||||
public Button SelectedButton => SelectedButtonData.Button;
|
||||
public string SelectedText => SelectedButtonData.Text;
|
||||
public T SelectedValue => SelectedButtonData.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Always will return true if itemId is not found.
|
||||
/// </summary>
|
||||
/// <param name="idx"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsItemDisabled(int idx)
|
||||
{
|
||||
return _buttonDataList.FirstOrDefault(bd => bd.Id == idx)?.Disabled ?? true;
|
||||
}
|
||||
|
||||
public void RemoveItem(int idx)
|
||||
{
|
||||
|
||||
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (data!= null)
|
||||
{
|
||||
data.Button.OnPressed -= ButtonOnPressed;
|
||||
_container.RemoveChild(data.Button);
|
||||
|
||||
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (buttonData != null)
|
||||
_buttonDataList.Remove(buttonData);
|
||||
|
||||
UpdateFirstAndLastButtonStyle();
|
||||
}
|
||||
}
|
||||
|
||||
public void Select(int idx)
|
||||
{
|
||||
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (data != null)
|
||||
{
|
||||
SelectedId = data.Id;
|
||||
data.Button.Pressed = true;
|
||||
return;
|
||||
}
|
||||
// Not found.
|
||||
}
|
||||
|
||||
public void SelectByValue(T value)
|
||||
{
|
||||
var data = _buttonDataList.FirstOrDefault(bd => EqualityComparer<T>.Default.Equals(bd.Value, value));
|
||||
if (data != null)
|
||||
{
|
||||
Select(data.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void InvokeItemSelected(RadioOptionItemSelectedEventArgs<T> args)
|
||||
{
|
||||
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == args.Id);
|
||||
if (buttonData == null) return;
|
||||
|
||||
if (buttonData.HasOnItemSelectedEvent)
|
||||
buttonData.InvokeItemSelected(args);
|
||||
else
|
||||
OnItemSelected?.Invoke(args);
|
||||
}
|
||||
|
||||
public void UpdateFirstAndLastButtonStyle()
|
||||
{
|
||||
for (int i = 0; i < _buttonDataList.Count; i++)
|
||||
{
|
||||
var buttonData = _buttonDataList[i];
|
||||
if (buttonData.Button == null) continue;
|
||||
|
||||
buttonData.Button.StyleClasses.Remove(ButtonStyle);
|
||||
buttonData.Button.StyleClasses.Remove(LastButtonStyle);
|
||||
buttonData.Button.StyleClasses.Remove(FirstButtonStyle);
|
||||
|
||||
if (i == 0)
|
||||
buttonData.Button.StyleClasses.Add(FirstButtonStyle);
|
||||
else if (i == _buttonDataList.Count - 1)
|
||||
buttonData.Button.StyleClasses.Add(LastButtonStyle);
|
||||
else
|
||||
buttonData.Button.StyleClasses.Add(ButtonStyle);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetItemDisabled(int idx, bool disabled)
|
||||
{
|
||||
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (data != null)
|
||||
{
|
||||
data.Disabled = disabled;
|
||||
data.Button.Disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetItemMetadata(int idx, object metadata)
|
||||
{
|
||||
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (buttonData != null)
|
||||
buttonData.Metadata = metadata;
|
||||
}
|
||||
|
||||
public void SetItemText(int idx, string text)
|
||||
{
|
||||
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
|
||||
if (data != null)
|
||||
{
|
||||
data.Text = text;
|
||||
data.Button.Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
public class RadioOptionItemSelectedEventArgs<T> : EventArgs
|
||||
{
|
||||
public RadioOptions<T> Button { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the item that has been selected.
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
public RadioOptionItemSelectedEventArgs(int id, RadioOptions<T> button)
|
||||
{
|
||||
Id = id;
|
||||
Button = button;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RadioOptionButtonData<T>
|
||||
{
|
||||
public int Id;
|
||||
public string Text;
|
||||
public T Value;
|
||||
public bool Disabled;
|
||||
public object? Metadata;
|
||||
|
||||
public Button Button;
|
||||
|
||||
public RadioOptionButtonData(string text, T value, Button button)
|
||||
{
|
||||
Text = text;
|
||||
this.Button = button;
|
||||
Value = value;
|
||||
}
|
||||
public event Action<RadioOptionItemSelectedEventArgs<T>>? OnItemSelected;
|
||||
public bool HasOnItemSelectedEvent => OnItemSelected != null;
|
||||
public void InvokeItemSelected(RadioOptionItemSelectedEventArgs<T> args)
|
||||
{
|
||||
OnItemSelected?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
var oldHeight = _entry.Height;
|
||||
var oldWidth = _entry.Width;
|
||||
_entry.Update(font, MaxWidth ?? Width, UIScale);
|
||||
_entry.Update(font, (MaxWidth ?? Width) * UIScale, UIScale);
|
||||
if (oldHeight != _entry.Height || MaxWidth != null && _entry.Width != oldWidth)
|
||||
{
|
||||
MinimumSizeChanged();
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// <summary>
|
||||
/// Simple control that draws a single texture using a variety of possible stretching modes.
|
||||
/// </summary>
|
||||
/// <seealso cref="AnimatedTextureRect"/>
|
||||
public class TextureRect : Control
|
||||
{
|
||||
public const string StylePropertyTexture = "texture";
|
||||
@@ -28,8 +29,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
get => _texture;
|
||||
set
|
||||
{
|
||||
var oldSize = _texture?.Size;
|
||||
_texture = value;
|
||||
MinimumSizeChanged();
|
||||
|
||||
if (value?.Size != oldSize)
|
||||
{
|
||||
MinimumSizeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_label.Text = string.Join("\n", _inputManager.DownKeyFunctions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
return;
|
||||
}
|
||||
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetManager.IsConnected)
|
||||
{
|
||||
contents.Text = "Not connected to server.";
|
||||
|
||||
@@ -179,7 +179,17 @@ namespace Robust.Client.UserInterface
|
||||
return tcs.Task;
|
||||
#else
|
||||
// Luckily, GTK Linux and COM Windows are both happily threaded. Yay!
|
||||
return Task.Run(action);
|
||||
// * Actual attempts to have multiple file dialogs up at the same time, and the resulting crashes,
|
||||
// have shown that at least for GTK+ (Linux), just because it can handle being on any thread doesn't mean it handle being on two at the same time.
|
||||
// Testing system was Ubuntu 20.04.
|
||||
// COM on Windows might handle this, but honestly, who exactly wants to risk it?
|
||||
// In particular this could very well be an swnfd issue.
|
||||
return Task.Run(() =>
|
||||
{
|
||||
lock (this) {
|
||||
return action();
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -172,8 +173,29 @@ namespace Robust.Client.UserInterface
|
||||
// This needs to happen because word wrapping doesn't get checked for the last word.
|
||||
if (posX > maxSizeX)
|
||||
{
|
||||
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.");
|
||||
if (!wordStartBreakIndex.HasValue)
|
||||
{
|
||||
Logger.Error(
|
||||
"Assert fail inside RichTextEntry.Update, " +
|
||||
"wordStartBreakIndex is null on method end w/ word wrap required. " +
|
||||
"Dumping relevant stuff. Send this to PJB.");
|
||||
Logger.Error($"Message: {Message}");
|
||||
Logger.Error($"maxSizeX: {maxSizeX}");
|
||||
Logger.Error($"maxUsedWidth: {maxUsedWidth}");
|
||||
Logger.Error($"breakIndexCounter: {breakIndexCounter}");
|
||||
Logger.Error("wordStartBreakIndex: null (duh)");
|
||||
Logger.Error($"wordSizePixels: {wordSizePixels}");
|
||||
Logger.Error($"posX: {posX}");
|
||||
Logger.Error($"lastChar: {lastChar}");
|
||||
Logger.Error($"forceSplitData: {forceSplitData}");
|
||||
Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
|
||||
|
||||
throw new Exception(
|
||||
"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.");
|
||||
}
|
||||
|
||||
LineBreaks.Add(wordStartBreakIndex!.Value.index);
|
||||
Height += font.GetLineHeight(uiScale);
|
||||
maxUsedWidth = Math.Max(maxUsedWidth, wordStartBreakIndex.Value.lineSize);
|
||||
|
||||
@@ -57,9 +57,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
// When a control receives a mouse down it must also receive a mouse up and mouse moves, always.
|
||||
// So we keep track of which control is "focused" by the mouse.
|
||||
private Control? _controlFocused;
|
||||
public Control? ControlFocused { get; private set; }
|
||||
|
||||
public LayoutContainer StateRoot { get; private set; } = default!;
|
||||
public PopupContainer ModalRoot { get; private set; } = default!;
|
||||
@@ -244,7 +242,8 @@ namespace Robust.Client.UserInterface
|
||||
RemoveModal(top);
|
||||
else
|
||||
{
|
||||
_controlFocused = top;
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = top;
|
||||
return false; // prevent anything besides the top modal control from receiving input
|
||||
}
|
||||
}
|
||||
@@ -260,12 +259,12 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = control;
|
||||
|
||||
_controlFocused = control;
|
||||
|
||||
if (_controlFocused.CanKeyboardFocus && _controlFocused.KeyboardFocusOnClick)
|
||||
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
|
||||
{
|
||||
_controlFocused.GrabKeyboardFocus();
|
||||
ControlFocused.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -273,7 +272,8 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public void HandleCanFocusUp()
|
||||
{
|
||||
_controlFocused = null;
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public void KeyBindDown(BoundKeyEventArgs args)
|
||||
@@ -290,7 +290,7 @@ namespace Robust.Client.UserInterface
|
||||
return;
|
||||
}
|
||||
|
||||
var control = _controlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
|
||||
|
||||
if (control == null)
|
||||
{
|
||||
@@ -311,7 +311,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
var control = _controlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
@@ -352,7 +352,7 @@ namespace Robust.Client.UserInterface
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
var target = _controlFocused ?? newHovered;
|
||||
var target = ControlFocused ?? newHovered;
|
||||
if (target != null)
|
||||
{
|
||||
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / UIScale,
|
||||
@@ -368,7 +368,7 @@ namespace Robust.Client.UserInterface
|
||||
private void UpdateActiveCursor()
|
||||
{
|
||||
// Consider mouse input focus first so that dragging windows don't act up etc.
|
||||
var cursorTarget = _controlFocused ?? CurrentlyHovered;
|
||||
var cursorTarget = ControlFocused ?? CurrentlyHovered;
|
||||
|
||||
if (cursorTarget == null)
|
||||
{
|
||||
@@ -478,13 +478,13 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
KeyboardFocused = control;
|
||||
|
||||
KeyboardFocused.FocusEntered();
|
||||
KeyboardFocused.KeyboardFocusEntered();
|
||||
}
|
||||
|
||||
public void ReleaseKeyboardFocus()
|
||||
{
|
||||
var oldFocused = KeyboardFocused;
|
||||
oldFocused?.FocusExited();
|
||||
oldFocused?.KeyboardFocusExited();
|
||||
KeyboardFocused = null;
|
||||
}
|
||||
|
||||
@@ -528,10 +528,9 @@ namespace Robust.Client.UserInterface
|
||||
_clearTooltip();
|
||||
}
|
||||
|
||||
if (control == _controlFocused)
|
||||
{
|
||||
_controlFocused = null;
|
||||
}
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
public void PushModal(Control modal)
|
||||
@@ -569,7 +568,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public void CursorChanged(Control control)
|
||||
{
|
||||
if (control == _controlFocused || control == CurrentlyHovered)
|
||||
if (control == ControlFocused || control == CurrentlyHovered)
|
||||
{
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.Renderable;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -11,66 +12,67 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper methods for resolving <see cref="SpriteSpecifier"/>s.
|
||||
/// </summary>
|
||||
public static class SpriteSpecifierExt
|
||||
{
|
||||
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
|
||||
{
|
||||
return cache
|
||||
.GetResource<TextureResource>(SharedSpriteComponent.TextureRoot / texSpecifier.TexturePath)
|
||||
.Texture;
|
||||
}
|
||||
|
||||
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
|
||||
{
|
||||
if (cache.TryGetResource<RSIResource>(
|
||||
SharedSpriteComponent.TextureRoot / rsiSpecifier.RsiPath,
|
||||
out var theRsi))
|
||||
{
|
||||
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return SpriteComponent.GetFallbackState(cache);
|
||||
}
|
||||
|
||||
public static Texture Frame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
var resc = IoCManager.Resolve<IResourceCache>();
|
||||
switch (specifier)
|
||||
{
|
||||
case SpriteSpecifier.Texture tex:
|
||||
return resc.GetResource<TextureResource>(SpriteComponent.TextureRoot / tex.TexturePath).Texture;
|
||||
|
||||
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.Frame0;
|
||||
}
|
||||
}
|
||||
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
|
||||
return resc.GetFallback<TextureResource>().Texture;
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
var protMgr = IoCManager.Resolve<IPrototypeManager>();
|
||||
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
|
||||
{
|
||||
Logger.Error("Failed to load EntityPrototype for EntityPrototypeId {0}", prototypeIcon.EntityPrototypeId);
|
||||
return resc.GetFallback<TextureResource>().Texture;
|
||||
}
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, resc)?.Default ?? resc.GetFallback<TextureResource>().Texture;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
return specifier.RsiStateLike().Default;
|
||||
}
|
||||
|
||||
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
var resc = IoCManager.Resolve<IResourceCache>();
|
||||
return specifier.RsiStateLike();
|
||||
}
|
||||
|
||||
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
|
||||
{
|
||||
var resC = IoCManager.Resolve<IResourceCache>();
|
||||
switch (specifier)
|
||||
{
|
||||
case SpriteSpecifier.Texture tex:
|
||||
return resc.GetResource<TextureResource>(SpriteComponent.TextureRoot / tex.TexturePath).Texture;
|
||||
return tex.GetTexture(resC);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return resc.GetFallback<TextureResource>().Texture;
|
||||
return rsi.GetState(resC);
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
var protMgr = IoCManager.Resolve<IPrototypeManager>();
|
||||
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
|
||||
{
|
||||
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
|
||||
return resc.GetFallback<TextureResource>().Texture;
|
||||
return SpriteComponent.GetFallbackState(resC);
|
||||
}
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, resc) ?? resc.GetFallback<TextureResource>().Texture;
|
||||
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, resC);
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Client.Utility
|
||||
{
|
||||
public static class UserDataDir
|
||||
internal static class UserDataDir
|
||||
{
|
||||
[Pure]
|
||||
public static string GetUserDataDir()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.ViewVariables.Editors
|
||||
{
|
||||
@@ -13,48 +15,32 @@ namespace Robust.Client.ViewVariables.Editors
|
||||
protected override Control MakeUI(object? value)
|
||||
{
|
||||
DebugTools.Assert(value!.GetType().IsEnum);
|
||||
var enumVal = (Enum)value;
|
||||
var enumType = value.GetType();
|
||||
var enumStorageType = enumType.GetEnumUnderlyingType();
|
||||
var enumList = Enum.GetValues(enumType);
|
||||
|
||||
var hBox = new HBoxContainer
|
||||
var optionButton = new OptionButton();
|
||||
foreach (var val in enumList)
|
||||
{
|
||||
CustomMinimumSize = new Vector2(200, 0)
|
||||
};
|
||||
var label = val?.ToString();
|
||||
if (label == null)
|
||||
continue;
|
||||
optionButton.AddItem(label, Convert.ToInt32(val));
|
||||
}
|
||||
|
||||
var lineEdit = new LineEdit
|
||||
{
|
||||
Text = enumVal.ToString(),
|
||||
Editable = !ReadOnly,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.FillExpand
|
||||
};
|
||||
optionButton.SelectId(Convert.ToInt32(value));
|
||||
optionButton.Disabled = ReadOnly;
|
||||
|
||||
if (!ReadOnly)
|
||||
{
|
||||
lineEdit.OnTextEntered += e =>
|
||||
var underlyingType = Enum.GetUnderlyingType(value.GetType());
|
||||
optionButton.OnItemSelected += e =>
|
||||
{
|
||||
var parseSig = new []{typeof(string), typeof(NumberStyles), typeof(CultureInfo), enumStorageType.MakeByRefType()};
|
||||
var parseMethod = enumStorageType.GetMethod("TryParse", parseSig);
|
||||
DebugTools.AssertNotNull(parseMethod);
|
||||
|
||||
var parameters = new object?[] {e.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, null};
|
||||
var parseWorked = (bool)parseMethod!.Invoke(null, parameters)!;
|
||||
|
||||
if (parseWorked) // textbox was the underlying type
|
||||
{
|
||||
DebugTools.AssertNotNull(parameters[3]);
|
||||
ValueChanged(parameters[3]);
|
||||
}
|
||||
else if(Enum.TryParse(enumType, e.Text, true, out var enumValue))
|
||||
{
|
||||
var underlyingVal = Convert.ChangeType(enumValue, enumStorageType);
|
||||
ValueChanged(underlyingVal);
|
||||
}
|
||||
optionButton.SelectId(e.Id);
|
||||
ValueChanged(Convert.ChangeType(e.Id, underlyingType));
|
||||
};
|
||||
}
|
||||
|
||||
hBox.AddChild(lineEdit);
|
||||
return hBox;
|
||||
return optionButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.ViewVariables.Traits;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -51,7 +52,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
|
||||
private bool _serverLoaded;
|
||||
|
||||
public ViewVariablesInstanceEntity(IViewVariablesManagerInternal vvm, IResourceCache resCache, IEntityManager entityManager) : base(vvm, resCache)
|
||||
public ViewVariablesInstanceEntity(IViewVariablesManagerInternal vvm, IEntityManager entityManager, IRobustSerializer robustSerializer) : base(vvm, robustSerializer)
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
@@ -117,7 +118,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
_tabs.SetTabTitle(TabClientVars, "Client Variables");
|
||||
|
||||
var first = true;
|
||||
foreach (var group in LocalPropertyList(obj, ViewVariablesManager, _resourceCache))
|
||||
foreach (var group in LocalPropertyList(obj, ViewVariablesManager, _robustSerializer))
|
||||
{
|
||||
ViewVariablesTraitMembers.CreateMemberGroupHeader(
|
||||
ref first,
|
||||
@@ -202,7 +203,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
button.Visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
button.Visible = true;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +232,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
button.Visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!button.Text.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
button.Visible = false;
|
||||
@@ -314,7 +315,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
|
||||
foreach (var propertyData in groupMembers)
|
||||
{
|
||||
var propertyEdit = new ViewVariablesPropertyControl(ViewVariablesManager, _resourceCache);
|
||||
var propertyEdit = new ViewVariablesPropertyControl(ViewVariablesManager, _robustSerializer);
|
||||
propertyEdit.SetStyle(otherStyle = !otherStyle);
|
||||
var editor = propertyEdit.SetProperty(propertyData);
|
||||
var selectorChain = new object[] {new ViewVariablesMemberSelector(propertyData.PropertyIndex)};
|
||||
@@ -328,7 +329,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
var componentsBlob = await ViewVariablesManager.RequestData<ViewVariablesBlobEntityComponents>(_entitySession, new ViewVariablesRequestEntityComponents());
|
||||
|
||||
_serverComponents.DisposeAllChildren();
|
||||
|
||||
|
||||
_serverComponents.AddChild(_serverComponentsSearchBar = new LineEdit
|
||||
{
|
||||
PlaceHolder = Loc.GetString("Search"),
|
||||
@@ -336,7 +337,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
});
|
||||
|
||||
_serverComponentsSearchBar.OnTextChanged += OnServerComponentsSearchBarChanged;
|
||||
|
||||
|
||||
componentsBlob.ComponentTypes.Sort();
|
||||
|
||||
var componentTypes = componentsBlob.ComponentTypes.AsEnumerable();
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.ViewVariables.Traits;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -19,8 +20,8 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
public ViewVariablesRemoteSession? Session { get; private set; }
|
||||
public object? Object { get; private set; }
|
||||
|
||||
public ViewVariablesInstanceObject(IViewVariablesManagerInternal vvm, IResourceCache resCache)
|
||||
: base(vvm, resCache) { }
|
||||
public ViewVariablesInstanceObject(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
|
||||
: base(vvm, robustSerializer) { }
|
||||
|
||||
public override void Initialize(SS14Window window, object obj)
|
||||
{
|
||||
@@ -113,7 +114,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
var list = new List<ViewVariablesTrait>(traitData.Count);
|
||||
if (traitData.Contains(ViewVariablesTraits.Members))
|
||||
{
|
||||
list.Add(new ViewVariablesTraitMembers(ViewVariablesManager, _resourceCache));
|
||||
list.Add(new ViewVariablesTraitMembers(ViewVariablesManager, _robustSerializer));
|
||||
}
|
||||
|
||||
if (traitData.Contains(ViewVariablesTraits.Enumerable))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.ViewVariables.Instances;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -11,7 +12,7 @@ namespace Robust.Client.ViewVariables.Traits
|
||||
internal class ViewVariablesTraitMembers : ViewVariablesTrait
|
||||
{
|
||||
private readonly IViewVariablesManagerInternal _vvm;
|
||||
private readonly IResourceCache _resourceCache;
|
||||
private readonly IRobustSerializer _robustSerializer;
|
||||
|
||||
private VBoxContainer _memberList = default!;
|
||||
|
||||
@@ -22,9 +23,9 @@ namespace Robust.Client.ViewVariables.Traits
|
||||
instance.AddTab("Members", _memberList);
|
||||
}
|
||||
|
||||
public ViewVariablesTraitMembers(IViewVariablesManagerInternal vvm, IResourceCache resourceCache)
|
||||
public ViewVariablesTraitMembers(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
|
||||
{
|
||||
_resourceCache = resourceCache;
|
||||
_robustSerializer = robustSerializer;
|
||||
_vvm = vvm;
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ namespace Robust.Client.ViewVariables.Traits
|
||||
{
|
||||
var first = true;
|
||||
foreach (var group in ViewVariablesInstance.LocalPropertyList(Instance.Object,
|
||||
Instance.ViewVariablesManager, _resourceCache))
|
||||
Instance.ViewVariablesManager, _robustSerializer))
|
||||
{
|
||||
CreateMemberGroupHeader(
|
||||
ref first,
|
||||
@@ -64,7 +65,7 @@ namespace Robust.Client.ViewVariables.Traits
|
||||
|
||||
foreach (var propertyData in groupMembers)
|
||||
{
|
||||
var propertyEdit = new ViewVariablesPropertyControl(_vvm, _resourceCache);
|
||||
var propertyEdit = new ViewVariablesPropertyControl(_vvm, _robustSerializer);
|
||||
propertyEdit.SetStyle(otherStyle = !otherStyle);
|
||||
var editor = propertyEdit.SetProperty(propertyData);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.ViewVariables.Traits;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -19,12 +20,12 @@ namespace Robust.Client.ViewVariables
|
||||
internal abstract class ViewVariablesInstance
|
||||
{
|
||||
public readonly IViewVariablesManagerInternal ViewVariablesManager;
|
||||
protected readonly IResourceCache _resourceCache;
|
||||
protected readonly IRobustSerializer _robustSerializer;
|
||||
|
||||
protected ViewVariablesInstance(IViewVariablesManagerInternal vvm, IResourceCache resCache)
|
||||
protected ViewVariablesInstance(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
|
||||
{
|
||||
ViewVariablesManager = vvm;
|
||||
_resourceCache = resCache;
|
||||
_robustSerializer = robustSerializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,7 +55,7 @@ namespace Robust.Client.ViewVariables
|
||||
}
|
||||
|
||||
protected internal static IEnumerable<IGrouping<Type, Control>> LocalPropertyList(object obj, IViewVariablesManagerInternal vvm,
|
||||
IResourceCache resCache)
|
||||
IRobustSerializer robustSerializer)
|
||||
{
|
||||
var styleOther = false;
|
||||
var type = obj.GetType();
|
||||
@@ -104,7 +105,7 @@ namespace Robust.Client.ViewVariables
|
||||
Value = value
|
||||
};
|
||||
|
||||
var propertyEdit = new ViewVariablesPropertyControl(vvm, resCache);
|
||||
var propertyEdit = new ViewVariablesPropertyControl(vvm, robustSerializer);
|
||||
propertyEdit.SetStyle(styleOther = !styleOther);
|
||||
var editor = propertyEdit.SetProperty(data);
|
||||
editor.OnValueChanged += onValueChanged;
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Robust.Client.ViewVariables
|
||||
internal class ViewVariablesManager : ViewVariablesManagerShared, IViewVariablesManagerInternal
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private uint _nextReqId = 1;
|
||||
@@ -214,11 +214,11 @@ namespace Robust.Client.ViewVariables
|
||||
ViewVariablesInstance instance;
|
||||
if (obj is IEntity entity && !entity.Deleted)
|
||||
{
|
||||
instance = new ViewVariablesInstanceEntity(this, _resourceCache, _entityManager);
|
||||
instance = new ViewVariablesInstanceEntity(this, _entityManager, _robustSerializer);
|
||||
}
|
||||
else
|
||||
{
|
||||
instance = new ViewVariablesInstanceObject(this, _resourceCache);
|
||||
instance = new ViewVariablesInstanceObject(this, _robustSerializer);
|
||||
}
|
||||
|
||||
var window = new SS14Window {Title = "View Variables"};
|
||||
@@ -255,11 +255,11 @@ namespace Robust.Client.ViewVariables
|
||||
ViewVariablesInstance instance;
|
||||
if (type != null && typeof(IEntity).IsAssignableFrom(type))
|
||||
{
|
||||
instance = new ViewVariablesInstanceEntity(this, _resourceCache, _entityManager);
|
||||
instance = new ViewVariablesInstanceEntity(this, _entityManager, _robustSerializer);
|
||||
}
|
||||
else
|
||||
{
|
||||
instance = new ViewVariablesInstanceObject(this, _resourceCache);
|
||||
instance = new ViewVariablesInstanceObject(this, _robustSerializer);
|
||||
}
|
||||
|
||||
loadingLabel.Dispose();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.ViewVariables.Editors;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -23,14 +24,14 @@ namespace Robust.Client.ViewVariables
|
||||
private readonly Label _bottomLabel;
|
||||
|
||||
private readonly IViewVariablesManagerInternal _viewVariablesManager;
|
||||
private readonly IResourceCache _resourceCache;
|
||||
private readonly IRobustSerializer _robustSerializer;
|
||||
|
||||
public ViewVariablesPropertyControl(IViewVariablesManagerInternal viewVars, IResourceCache resourceCache)
|
||||
public ViewVariablesPropertyControl(IViewVariablesManagerInternal viewVars, IRobustSerializer robustSerializer)
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
|
||||
_viewVariablesManager = viewVars;
|
||||
_resourceCache = resourceCache;
|
||||
_robustSerializer = robustSerializer;
|
||||
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
ToolTip = "Click to expand";
|
||||
@@ -68,11 +69,11 @@ namespace Robust.Client.ViewVariables
|
||||
|
||||
_bottomLabel.Text = $"Type: {member.TypePretty}";
|
||||
VVPropEditor editor;
|
||||
if (type == null)
|
||||
if (type == null || !_robustSerializer.CanSerialize(type))
|
||||
{
|
||||
// Type is server-side only.
|
||||
// Info whether it's reference or value type can be figured out from the sent value.
|
||||
if (member.Value is ViewVariablesBlobMembers.ServerValueTypeToken)
|
||||
if (type?.IsValueType == true || member.Value is ViewVariablesBlobMembers.ServerValueTypeToken)
|
||||
{
|
||||
// Value type, just display it stringified read-only.
|
||||
editor = new VVPropEditorDummy();
|
||||
@@ -80,7 +81,7 @@ namespace Robust.Client.ViewVariables
|
||||
else
|
||||
{
|
||||
// Has to be a reference type at this point.
|
||||
DebugTools.Assert(member.Value is ViewVariablesBlobMembers.ReferenceToken || member.Value == null);
|
||||
DebugTools.Assert(member.Value is ViewVariablesBlobMembers.ReferenceToken || member.Value == null || type?.IsClass == true || type?.IsInterface == true);
|
||||
editor = _viewVariablesManager.PropertyFor(typeof(object));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Prometheus;
|
||||
using Robust.Server.Console;
|
||||
@@ -37,6 +38,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Server.DataMetrics;
|
||||
using Robust.Server.Log;
|
||||
using Robust.Server.Utility;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Serilog.Debugging;
|
||||
@@ -236,7 +238,6 @@ namespace Robust.Server
|
||||
{
|
||||
netMan.Initialize(true);
|
||||
netMan.StartServer();
|
||||
netMan.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -286,6 +287,8 @@ namespace Robust.Server
|
||||
|
||||
// Initialize Tier 2 services
|
||||
IoCManager.Resolve<IGameTiming>().InSimulation = true;
|
||||
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
|
||||
_stateManager.Initialize();
|
||||
IoCManager.Resolve<IPlayerManager>().Initialize(MaxPlayers);
|
||||
@@ -320,6 +323,11 @@ namespace Robust.Server
|
||||
|
||||
_stringSerializer.LockStrings();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
|
||||
{
|
||||
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -459,7 +467,6 @@ namespace Robust.Server
|
||||
_time.TickRate = b;
|
||||
|
||||
Logger.InfoS("game", $"Tickrate changed to: {b} on tick {_time.CurTick}");
|
||||
SendTickRateUpdateToClients(b);
|
||||
});
|
||||
|
||||
_time.TickRate = (byte) _config.GetCVar(CVars.NetTickrate);
|
||||
@@ -469,17 +476,11 @@ namespace Robust.Server
|
||||
Logger.InfoS("srv", $"Max players: {MaxPlayers}");
|
||||
}
|
||||
|
||||
private void SendTickRateUpdateToClients(byte newTickRate)
|
||||
{
|
||||
var msg = _network.CreateNetMessage<MsgSetTickRate>();
|
||||
msg.NewTickRate = newTickRate;
|
||||
|
||||
_network.ServerSendToAll(msg);
|
||||
}
|
||||
|
||||
// called right before main loop returns, do all saving/cleanup in here
|
||||
private void Cleanup()
|
||||
{
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
|
||||
// shut down networking, kicking all players.
|
||||
_network.Shutdown($"Server shutting down: {_shutdownReason}");
|
||||
|
||||
@@ -500,6 +501,11 @@ namespace Robust.Server
|
||||
AppDomain.CurrentDomain.ProcessExit -= ProcessExiting;
|
||||
|
||||
//TODO: This should prob shutdown all managers in a loop.
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
|
||||
{
|
||||
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
|
||||
}
|
||||
}
|
||||
|
||||
private string UpdateBps()
|
||||
@@ -528,6 +534,9 @@ namespace Robust.Server
|
||||
ServerCurTick.Set(_time.CurTick.Value);
|
||||
ServerCurTime.Set(_time.CurTime.TotalSeconds);
|
||||
|
||||
// These are always the same on the server, there is no prediction.
|
||||
_time.LastRealTick = _time.CurTick;
|
||||
|
||||
UpdateTitle();
|
||||
|
||||
using (TickUsage.WithLabels("PreEngine").NewTimer())
|
||||
@@ -535,6 +544,11 @@ namespace Robust.Server
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
using (TickUsage.WithLabels("NetworkedCVar").NewTimer())
|
||||
{
|
||||
IoCManager.Resolve<INetConfigurationManager>().TickProcessMessages();
|
||||
}
|
||||
|
||||
using (TickUsage.WithLabels("Timers").NewTimer())
|
||||
{
|
||||
timerManager.UpdateTimers(frameEventArgs);
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal sealed class RemoveCompCommand : IClientCommand
|
||||
{
|
||||
public string Command => "rmcomp";
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Maps;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Server.Interfaces.Timing;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -111,7 +112,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
public string Command => "loadbp";
|
||||
public string Description => "Loads a blueprint from disk into the game.";
|
||||
public string Help => "loadbp <MapID> <Path>";
|
||||
public string Help => "loadbp <MapID> <Path> [storeUids]";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
@@ -141,8 +142,14 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var loadOptions = new MapLoadOptions();
|
||||
if (args.Length > 2)
|
||||
{
|
||||
loadOptions.StoreMapUids = bool.Parse(args[2]);
|
||||
}
|
||||
|
||||
var mapLoader = IoCManager.Resolve<IMapLoader>();
|
||||
mapLoader.LoadBlueprint(mapId, args[1]);
|
||||
mapLoader.LoadBlueprint(mapId, args[1], loadOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,17 +40,17 @@ namespace Robust.Server.GameObjects
|
||||
return (T)data[key];
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data)
|
||||
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data)
|
||||
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
bool TryGetData<T>(object key, [MaybeNullWhen(false)] out T data)
|
||||
private bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
if (this.data.TryGetValue(key, out var dat))
|
||||
{
|
||||
|
||||
@@ -32,8 +32,11 @@ namespace Robust.Server.GameObjects
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
_enabled = value;
|
||||
Dirty();
|
||||
if (_enabled != value)
|
||||
{
|
||||
_enabled = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace Robust.Server.GameObjects.Components.UserInterface
|
||||
_stateDirty = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Switches between closed and open for a specific client.
|
||||
/// </summary>
|
||||
@@ -183,8 +183,8 @@ namespace Robust.Server.GameObjects.Components.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Opens this interface for a specific client.
|
||||
/// </summary>
|
||||
@@ -263,6 +263,7 @@ namespace Robust.Server.GameObjects.Components.UserInterface
|
||||
OnClosed?.Invoke(session);
|
||||
_subscribedSessions.Remove(session);
|
||||
_playerStateOverrides.Remove(session);
|
||||
session.PlayerStatusChanged -= OnSessionOnPlayerStatusChanged;
|
||||
|
||||
if (_subscribedSessions.Count == 0)
|
||||
{
|
||||
|
||||
18
Robust.Server/GameObjects/MapSaveIdComponent.cs
Normal file
18
Robust.Server/GameObjects/MapSaveIdComponent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata component used to keep consistent UIDs inside map files cross saving.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This component stores the previous map UID of entities from map load.
|
||||
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
|
||||
/// </remarks>
|
||||
public sealed class MapSaveIdComponent : Component
|
||||
{
|
||||
public override string Name => "MapSaveId";
|
||||
|
||||
public int Uid { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,8 @@ namespace Robust.Server.GameObjects
|
||||
Register<DebugExceptionInitializeComponent>();
|
||||
Register<DebugExceptionStartupComponent>();
|
||||
#endif
|
||||
|
||||
Register<MapSaveIdComponent>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Map;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
@@ -7,9 +8,11 @@ namespace Robust.Server.Interfaces.Maps
|
||||
public interface IMapLoader
|
||||
{
|
||||
IMapGrid? LoadBlueprint(MapId mapId, string path);
|
||||
IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options);
|
||||
void SaveBlueprint(GridId gridId, string yamlPath);
|
||||
|
||||
void LoadMap(MapId mapId, string path);
|
||||
void LoadMap(MapId mapId, string path, MapLoadOptions options);
|
||||
void SaveMap(MapId mapId, string yamlPath);
|
||||
|
||||
event Action<YamlStream, string> LoadedMapData;
|
||||
|
||||
11
Robust.Server/Maps/MapLoadOptions.cs
Normal file
11
Robust.Server/Maps/MapLoadOptions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Robust.Server.Maps
|
||||
{
|
||||
public sealed class MapLoadOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, UID components will be created for loaded entities
|
||||
/// to maintain consistency upon subsequent savings.
|
||||
/// </summary>
|
||||
public bool StoreMapUids { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Robust.Shared.GameObjects;
|
||||
using System.Globalization;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using System.Linq;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Interfaces.Timing;
|
||||
using Robust.Shared.GameObjects.Components.Map;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
@@ -29,6 +30,8 @@ namespace Robust.Server.Maps
|
||||
/// </summary>
|
||||
public class MapLoader : IMapLoader
|
||||
{
|
||||
private static readonly MapLoadOptions DefaultLoadOptions = new();
|
||||
|
||||
private const int MapFormatVersion = 2;
|
||||
|
||||
[Dependency] private readonly IResourceManager _resMan = default!;
|
||||
@@ -46,7 +49,8 @@ namespace Robust.Server.Maps
|
||||
{
|
||||
var grid = _mapManager.GetGrid(gridId);
|
||||
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager);
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
||||
_componentManager, _prototypeManager);
|
||||
context.RegisterGrid(grid);
|
||||
var root = context.Serialize();
|
||||
var document = new YamlDocument(root);
|
||||
@@ -68,6 +72,11 @@ namespace Robust.Server.Maps
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMapGrid? LoadBlueprint(MapId mapId, string path)
|
||||
{
|
||||
return LoadBlueprint(mapId, path, DefaultLoadOptions);
|
||||
}
|
||||
|
||||
public IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
|
||||
{
|
||||
TextReader reader;
|
||||
var resPath = new ResourcePath(path).ToRootedPath();
|
||||
@@ -108,7 +117,8 @@ namespace Robust.Server.Maps
|
||||
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
|
||||
}
|
||||
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager, (YamlMappingNode)data.RootNode, mapId);
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
||||
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
|
||||
context.Deserialize();
|
||||
grid = context.Grids[0];
|
||||
|
||||
@@ -128,7 +138,8 @@ namespace Robust.Server.Maps
|
||||
public void SaveMap(MapId mapId, string yamlPath)
|
||||
{
|
||||
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager);
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
||||
_componentManager, _prototypeManager);
|
||||
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
|
||||
{
|
||||
context.RegisterGrid(grid);
|
||||
@@ -149,11 +160,16 @@ namespace Robust.Server.Maps
|
||||
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.InfoS("map", "Save completed!");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadMap(MapId mapId, string path)
|
||||
{
|
||||
LoadMap(mapId, path, DefaultLoadOptions);
|
||||
}
|
||||
|
||||
public void LoadMap(MapId mapId, string path, MapLoadOptions options)
|
||||
{
|
||||
TextReader reader;
|
||||
var resPath = new ResourcePath(path).ToRootedPath();
|
||||
@@ -188,7 +204,8 @@ namespace Robust.Server.Maps
|
||||
|
||||
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
|
||||
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager, (YamlMappingNode)data.RootNode, mapId);
|
||||
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
||||
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
|
||||
context.Deserialize();
|
||||
|
||||
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(mapId))
|
||||
@@ -213,6 +230,7 @@ namespace Robust.Server.Maps
|
||||
private readonly IComponentManager _componentManager;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
|
||||
private readonly MapLoadOptions? _loadOptions;
|
||||
private readonly Dictionary<GridId, int> GridIDMap = new();
|
||||
public readonly List<IMapGrid> Grids = new();
|
||||
|
||||
@@ -225,8 +243,6 @@ namespace Robust.Server.Maps
|
||||
|
||||
private bool IsBlueprintMode => GridIDMap.Count == 1;
|
||||
|
||||
private int uidCounter;
|
||||
|
||||
private readonly YamlMappingNode RootNode;
|
||||
private readonly MapId TargetMap;
|
||||
|
||||
@@ -239,7 +255,9 @@ namespace Robust.Server.Maps
|
||||
|
||||
public bool MapIsPostInit { get; private set; }
|
||||
|
||||
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities, IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager)
|
||||
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
|
||||
IServerEntityManagerInternal entities, IPauseManager pauseManager, IComponentManager componentManager,
|
||||
IPrototypeManager prototypeManager)
|
||||
{
|
||||
_mapManager = maps;
|
||||
_tileDefinitionManager = tileDefs;
|
||||
@@ -251,14 +269,17 @@ namespace Robust.Server.Maps
|
||||
RootNode = new YamlMappingNode();
|
||||
}
|
||||
|
||||
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities,
|
||||
IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager, YamlMappingNode node, MapId targetMapId)
|
||||
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
|
||||
IServerEntityManagerInternal entities,
|
||||
IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager,
|
||||
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
|
||||
{
|
||||
_mapManager = maps;
|
||||
_tileDefinitionManager = tileDefs;
|
||||
_serverEntityManager = entities;
|
||||
_pauseManager = pauseManager;
|
||||
_componentManager = componentManager;
|
||||
_loadOptions = options;
|
||||
|
||||
RootNode = node;
|
||||
TargetMap = targetMapId;
|
||||
@@ -438,8 +459,8 @@ namespace Robust.Server.Maps
|
||||
var newId = new GridId?();
|
||||
YamlGridSerializer.DeserializeGrid(
|
||||
_mapManager, TargetMap, ref newId,
|
||||
(YamlMappingNode)grid["settings"],
|
||||
(YamlSequenceNode)grid["chunks"],
|
||||
(YamlMappingNode) grid["settings"],
|
||||
(YamlSequenceNode) grid["chunks"],
|
||||
_tileMap!,
|
||||
_tileDefinitionManager
|
||||
);
|
||||
@@ -489,6 +510,12 @@ namespace Robust.Server.Maps
|
||||
Entities.Add(entity);
|
||||
UidEntityMap.Add(uid, entity.Uid);
|
||||
_entitiesToDeserialize.Add((entity, entityDef));
|
||||
|
||||
if (_loadOptions!.StoreMapUids)
|
||||
{
|
||||
var comp = entity.AddComponent<MapSaveIdComponent>();
|
||||
comp.Uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +528,7 @@ namespace Robust.Server.Maps
|
||||
{
|
||||
foreach (var compData in componentList)
|
||||
{
|
||||
CurrentReadingEntityComponents[compData["type"].AsString()] = (YamlMappingNode)compData;
|
||||
CurrentReadingEntityComponents[compData["type"].AsString()] = (YamlMappingNode) compData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,15 +632,55 @@ namespace Robust.Server.Maps
|
||||
|
||||
private void PopulateEntityList()
|
||||
{
|
||||
var withUid = new List<MapSaveIdComponent>();
|
||||
var withoutUid = new List<IEntity>();
|
||||
var takenIds = new HashSet<int>();
|
||||
|
||||
foreach (var entity in _serverEntityManager.GetEntities())
|
||||
{
|
||||
if (IsMapSavable(entity))
|
||||
{
|
||||
var uid = uidCounter++;
|
||||
EntityUidMap.Add(entity.Uid, uid);
|
||||
Entities.Add(entity);
|
||||
if (entity.TryGetComponent(out MapSaveIdComponent? mapSaveId))
|
||||
{
|
||||
withUid.Add(mapSaveId);
|
||||
}
|
||||
else
|
||||
{
|
||||
withoutUid.Add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go over entities with a MapSaveIdComponent and assign those.
|
||||
|
||||
foreach (var mapIdComp in withUid)
|
||||
{
|
||||
var uid = mapIdComp.Uid;
|
||||
if (takenIds.Contains(uid))
|
||||
{
|
||||
// Duplicate ID. Just pretend it doesn't have an ID and use the without path.
|
||||
withoutUid.Add(mapIdComp.Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityUidMap.Add(mapIdComp.Owner.Uid, uid);
|
||||
takenIds.Add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
var uidCounter = 0;
|
||||
foreach (var entity in withoutUid)
|
||||
{
|
||||
while (takenIds.Contains(uidCounter))
|
||||
{
|
||||
// Find next available UID.
|
||||
uidCounter += 1;
|
||||
}
|
||||
|
||||
EntityUidMap.Add(entity.Uid, uidCounter);
|
||||
takenIds.Add(uidCounter);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEntitySection()
|
||||
@@ -621,7 +688,7 @@ namespace Robust.Server.Maps
|
||||
var entities = new YamlSequenceNode();
|
||||
RootNode.Add("entities", entities);
|
||||
|
||||
foreach (var entity in Entities)
|
||||
foreach (var entity in Entities.OrderBy(e => EntityUidMap[e.Uid]))
|
||||
{
|
||||
CurrentWritingEntity = entity;
|
||||
var mapping = new YamlMappingNode
|
||||
@@ -638,6 +705,9 @@ namespace Robust.Server.Maps
|
||||
// See engine#636 for why the Distinct() call.
|
||||
foreach (var component in entity.GetAllComponents())
|
||||
{
|
||||
if (component is MapSaveIdComponent)
|
||||
continue;
|
||||
|
||||
var compMapping = new YamlMappingNode();
|
||||
CurrentWritingComponent = component.Name;
|
||||
var compSerializer = YamlObjectSerializer.NewWriter(compMapping, this);
|
||||
@@ -683,6 +753,7 @@ namespace Robust.Server.Maps
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == typeof(EntityUid))
|
||||
{
|
||||
if (node.AsString() == "null")
|
||||
@@ -694,7 +765,8 @@ namespace Robust.Server.Maps
|
||||
var val = node.AsInt();
|
||||
if (val >= Entities.Count)
|
||||
{
|
||||
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
|
||||
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.",
|
||||
val);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -702,12 +774,14 @@ namespace Robust.Server.Maps
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(IEntity).IsAssignableFrom(type))
|
||||
{
|
||||
var val = node.AsInt();
|
||||
if (val >= Entities.Count)
|
||||
{
|
||||
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
|
||||
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.",
|
||||
val);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -715,6 +789,7 @@ namespace Robust.Server.Maps
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
@@ -766,6 +841,7 @@ namespace Robust.Server.Maps
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
node = null;
|
||||
return false;
|
||||
}
|
||||
@@ -878,7 +954,7 @@ namespace Robust.Server.Maps
|
||||
}
|
||||
|
||||
Stream = stream;
|
||||
GridCount = ((YamlSequenceNode)RootNode["grids"]).Children.Count;
|
||||
GridCount = ((YamlSequenceNode) RootNode["grids"]).Children.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Linq;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network.Messages;
|
||||
|
||||
@@ -88,6 +89,12 @@ namespace Robust.Server.Placement
|
||||
|
||||
var coordinates = msg.EntityCoordinates;
|
||||
|
||||
if (!coordinates.IsValid(_entityManager))
|
||||
{
|
||||
Logger.WarningS("placement",
|
||||
$"{session} tried to place {msg.ObjType} at invalid coordinate {coordinates}");
|
||||
return;
|
||||
}
|
||||
|
||||
/* TODO: Redesign permission system, or document what this is supposed to be doing
|
||||
var permission = GetPermission(session.attachedEntity.Uid, alignRcv);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Prometheus;
|
||||
using Robust.Server.Interfaces;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input;
|
||||
@@ -91,8 +92,6 @@ namespace Robust.Server.Player
|
||||
|
||||
MaxPlayers = maxPlayers;
|
||||
|
||||
_network.RegisterNetMessage<MsgServerInfoReq>(MsgServerInfoReq.NAME, HandleWelcomeMessageReq);
|
||||
_network.RegisterNetMessage<MsgServerInfo>(MsgServerInfo.NAME);
|
||||
_network.RegisterNetMessage<MsgPlayerListReq>(MsgPlayerListReq.NAME, HandlePlayerListReq);
|
||||
_network.RegisterNetMessage<MsgPlayerList>(MsgPlayerList.NAME);
|
||||
|
||||
@@ -378,6 +377,8 @@ namespace Robust.Server.Player
|
||||
}
|
||||
|
||||
PlayerCountMetric.Set(PlayerCount);
|
||||
|
||||
IoCManager.Resolve<INetConfigurationManager>().SyncConnectingClient(args.Channel);
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
|
||||
@@ -414,18 +415,6 @@ namespace Robust.Server.Player
|
||||
Dirty();
|
||||
}
|
||||
|
||||
private void HandleWelcomeMessageReq(MsgServerInfoReq message)
|
||||
{
|
||||
var channel = message.MsgChannel;
|
||||
var netMsg = channel.CreateNetMessage<MsgServerInfo>();
|
||||
|
||||
netMsg.ServerName = _baseServer.ServerName;
|
||||
netMsg.ServerMaxPlayers = _baseServer.MaxPlayers;
|
||||
netMsg.TickRate = _timing.TickRate;
|
||||
|
||||
channel.SendMessage(netMsg);
|
||||
}
|
||||
|
||||
private void HandlePlayerListReq(MsgPlayerListReq message)
|
||||
{
|
||||
var channel = message.MsgChannel;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@@ -68,8 +69,8 @@ namespace Robust.Server.ServerStatus
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
|
||||
_httpSawmill.Error($"Exception in StatusHost: {e}");
|
||||
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -188,15 +189,17 @@ namespace Robust.Server.ServerStatus
|
||||
_listener!.Stop();
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649
|
||||
[JsonObject(ItemRequired = Required.DisallowNull)]
|
||||
private sealed class BuildInfo
|
||||
{
|
||||
[JsonProperty("engine_version")] public string EngineVersion = default!;
|
||||
[JsonProperty("hash")] public string? Hash;
|
||||
[JsonProperty("download")] public string? Download;
|
||||
[JsonProperty("download")] public string? Download = default;
|
||||
[JsonProperty("fork_id")] public string ForkId = default!;
|
||||
[JsonProperty("version")] public string Version = default!;
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
|
||||
private sealed class ContextImpl : IStatusHandlerContext
|
||||
{
|
||||
@@ -234,12 +237,12 @@ namespace Robust.Server.ServerStatus
|
||||
return serializer.Deserialize<T>(jsonReader);
|
||||
}
|
||||
|
||||
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = "text/plain")
|
||||
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = MediaTypeNames.Text.Plain)
|
||||
{
|
||||
Respond(text, (int) code, contentType);
|
||||
}
|
||||
|
||||
public void Respond(string text, int code = 200, string contentType = "text/plain")
|
||||
public void Respond(string text, int code = 200, string contentType = MediaTypeNames.Text.Plain)
|
||||
{
|
||||
_context.Response.StatusCode = code;
|
||||
_context.Response.ContentType = contentType;
|
||||
@@ -263,6 +266,8 @@ namespace Robust.Server.ServerStatus
|
||||
{
|
||||
using var streamWriter = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
|
||||
|
||||
_context.Response.ContentType = MediaTypeNames.Application.Json;
|
||||
|
||||
using var jsonWriter = new JsonTextWriter(streamWriter);
|
||||
|
||||
JsonSerializer.Serialize(jsonWriter, jsonData);
|
||||
|
||||
38
Robust.Server/Utility/WindowsTickPeriod.cs
Normal file
38
Robust.Server/Utility/WindowsTickPeriod.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Robust.Server.Utility
|
||||
{
|
||||
internal static class WindowsTickPeriod
|
||||
{
|
||||
private const uint TIMERR_NOERROR = 0;
|
||||
// This is an actual error code my god.
|
||||
private const uint TIMERR_NOCANDO = 97;
|
||||
|
||||
public static void TimeBeginPeriod(uint period)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var ret = timeBeginPeriod(period);
|
||||
if (ret != TIMERR_NOERROR)
|
||||
throw new InvalidOperationException($"timeBeginPeriod returned error: {ret}");
|
||||
}
|
||||
|
||||
public static void TimeEndPeriod(uint period)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var ret = timeEndPeriod(period);
|
||||
if (ret != TIMERR_NOERROR)
|
||||
throw new InvalidOperationException($"timeEndPeriod returned error: {ret}");
|
||||
}
|
||||
|
||||
[DllImport("Winmm.dll")]
|
||||
private static extern uint timeBeginPeriod(uint uPeriod);
|
||||
|
||||
[DllImport("Winmm.dll")]
|
||||
private static extern uint timeEndPeriod(uint uPeriod);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Robust.Shared.Scripting
|
||||
{
|
||||
return new EntityCoordinates(EntityUid.Invalid, ((float) x, (float) y));
|
||||
}
|
||||
|
||||
|
||||
return new EntityCoordinates(grid.GridEntityId, ((float) x, (float) y));
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ namespace Robust.Shared.Scripting
|
||||
return getent(eid(i));
|
||||
}
|
||||
|
||||
public T gcm<T>(int i)
|
||||
{
|
||||
return getent(i).GetComponent<T>();
|
||||
}
|
||||
|
||||
public IEntity getent(EntityUid uid)
|
||||
{
|
||||
return ent.GetEntity(uid);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
@@ -59,8 +60,19 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<bool> NetPredict =
|
||||
CVarDef.Create("net.predict", true, CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<int> NetPredictSize =
|
||||
CVarDef.Create("net.predict_size", 1, CVar.ARCHIVE);
|
||||
public static readonly CVarDef<int> NetPredictTickBias =
|
||||
CVarDef.Create("net.predict_tick_bias", 1, CVar.ARCHIVE);
|
||||
|
||||
// On Windows we default this to 16ms lag bias, to account for time period lag in the Lidgren thread.
|
||||
// Basically due to how time periods work on Windows, messages are (at worst) time period-delayed when sending.
|
||||
// BUT! Lidgren's latency calculation *never* measures this due to how it works.
|
||||
// This broke some prediction calculations quite badly so we bias them to mask it.
|
||||
// This is not necessary on Linux because Linux, for better or worse,
|
||||
// just has the Lidgren thread go absolute brr polling.
|
||||
public static readonly CVarDef<float> NetPredictLagBias = CVarDef.Create(
|
||||
"net.predict_lag_bias",
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 0.016f : 0,
|
||||
CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<int> NetStateBufMergeThreshold =
|
||||
CVarDef.Create("net.state_buf_merge_threshold", 5, CVar.ARCHIVE);
|
||||
@@ -77,6 +89,8 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> NetTickrate =
|
||||
CVarDef.Create("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
public static readonly CVarDef<int> SysWinTickPeriod =
|
||||
CVarDef.Create("sys.win_tick_period", 3, CVar.SERVERONLY);
|
||||
|
||||
#if DEBUG
|
||||
public static readonly CVarDef<float> NetFakeLoss = CVarDef.Create("net.fakeloss", 0f, CVar.CHEAT);
|
||||
@@ -151,10 +165,10 @@ namespace Robust.Shared
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> GameMaxPlayers =
|
||||
CVarDef.Create("game.maxplayers", 32, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
CVarDef.Create("game.maxplayers", 32, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
public static readonly CVarDef<string> GameHostName =
|
||||
CVarDef.Create("game.hostname", "MyServer", CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
CVarDef.Create("game.hostname", "MyServer", CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/*
|
||||
* LOG
|
||||
@@ -235,6 +249,9 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> DisplayLightMapDivider =
|
||||
CVarDef.Create("display.lightmapdivider", 2, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<int> DisplayMaxLightsPerScene =
|
||||
CVarDef.Create("display.maxlightsperscene", 128, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<bool> DisplaySoftShadows =
|
||||
CVarDef.Create("display.softshadows", true, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Nett;
|
||||
using Nett;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
@@ -13,12 +13,12 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// Stores and manages global configuration variables.
|
||||
/// </summary>
|
||||
internal sealed class ConfigurationManager : IConfigurationManagerInternal
|
||||
internal class ConfigurationManager : IConfigurationManagerInternal
|
||||
{
|
||||
private const char TABLE_DELIMITER = '.';
|
||||
private readonly Dictionary<string, ConfigVar> _configVars = new();
|
||||
protected readonly Dictionary<string, ConfigVar> _configVars = new();
|
||||
private string? _configFile;
|
||||
private bool _isServer;
|
||||
protected bool _isServer;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ConfigurationManager.
|
||||
@@ -79,6 +79,7 @@ namespace Robust.Shared.Configuration
|
||||
else // this is a key, add CVar
|
||||
{
|
||||
// if the CVar has already been registered
|
||||
var tomlValue = TypeConvert(obj);
|
||||
if (_configVars.TryGetValue(tablePath, out var cfgVar))
|
||||
{
|
||||
if ((cfgVar.Flags & CVar.SECURE) != 0)
|
||||
@@ -90,13 +91,14 @@ namespace Robust.Shared.Configuration
|
||||
return;
|
||||
}
|
||||
// overwrite the value with the saved one
|
||||
cfgVar.Value = TypeConvert(obj);
|
||||
cfgVar.Value = tomlValue;
|
||||
cfgVar.ValueChanged?.Invoke(cfgVar.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
cfgVar = new ConfigVar(tablePath, null, CVar.NONE) { Value = TypeConvert(obj) };
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
cfgVar = new ConfigVar(tablePath, 0, CVar.NONE) { Value = tomlValue };
|
||||
_configVars.Add(tablePath, cfgVar);
|
||||
}
|
||||
|
||||
@@ -131,6 +133,8 @@ namespace Robust.Shared.Configuration
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't write if Archive flag is not set.
|
||||
// Don't write if the cVar is the default value.
|
||||
if (!cVar.ConfigModified &&
|
||||
(cVar.Flags & CVar.ARCHIVE) == 0 || value.Equals(cVar.DefaultValue))
|
||||
{
|
||||
@@ -192,6 +196,7 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
|
||||
public void RegisterCVar<T>(string name, T defaultValue, CVar flags = CVar.NONE, Action<T>? onValueChanged = null)
|
||||
where T : notnull
|
||||
{
|
||||
Action<object>? valueChangedDelegate = null;
|
||||
if (onValueChanged != null)
|
||||
@@ -202,7 +207,7 @@ namespace Robust.Shared.Configuration
|
||||
RegisterCVar(name, typeof(T), defaultValue, flags, valueChangedDelegate);
|
||||
}
|
||||
|
||||
private void RegisterCVar(string name, Type type, object? defaultValue, CVar flags, Action<object>? onValueChanged)
|
||||
private void RegisterCVar(string name, Type type, object defaultValue, CVar flags, Action<object>? onValueChanged)
|
||||
{
|
||||
DebugTools.Assert(!type.IsEnum || type.GetEnumUnderlyingType() == typeof(int),
|
||||
$"{name}: Enum cvars must have int as underlying type.");
|
||||
@@ -305,10 +310,15 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetCVar(string name, object value)
|
||||
public virtual void SetCVar(string name, object value)
|
||||
{
|
||||
SetCVarInternal(name, value);
|
||||
}
|
||||
|
||||
private void SetCVarInternal(string name, object value, bool allowSecure = false)
|
||||
{
|
||||
//TODO: Make flags work, required non-derpy net system.
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered && (cVar.Flags & CVar.SECURE) == 0)
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered && (allowSecure || (cVar.Flags & CVar.SECURE) == 0))
|
||||
{
|
||||
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
|
||||
{
|
||||
@@ -348,6 +358,18 @@ namespace Robust.Shared.Configuration
|
||||
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
|
||||
}
|
||||
|
||||
public void SetSecureCVar(string name, object value)
|
||||
{
|
||||
SetCVarInternal(name, value, allowSecure: true);
|
||||
}
|
||||
|
||||
|
||||
public void SetSecureCVar<T>(CVarDef<T> def, T value) where T : notnull
|
||||
{
|
||||
SetSecureCVar(def.Name, value);
|
||||
}
|
||||
|
||||
|
||||
public T GetCVar<T>(CVarDef<T> def) where T : notnull
|
||||
{
|
||||
return GetCVar<T>(def.Name);
|
||||
@@ -377,13 +399,14 @@ namespace Robust.Shared.Configuration
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
var cVar = new ConfigVar(key, null, CVar.NONE) { OverrideValue = value };
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) { OverrideValue = value };
|
||||
_configVars.Add(key, cVar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object ParseOverrideValue(string value, Type? type)
|
||||
private static object ParseOverrideValue(string value, Type? type)
|
||||
{
|
||||
if (type == typeof(int))
|
||||
{
|
||||
@@ -434,7 +457,7 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// Holds the data for a single configuration variable.
|
||||
/// </summary>
|
||||
private class ConfigVar
|
||||
protected class ConfigVar
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a CVar.
|
||||
@@ -444,7 +467,7 @@ namespace Robust.Shared.Configuration
|
||||
/// everything after is the CVar name in the TOML document.</param>
|
||||
/// <param name="defaultValue">The default value of this CVar.</param>
|
||||
/// <param name="flags">Optional flags to modify the behavior of this CVar.</param>
|
||||
public ConfigVar(string name, object? defaultValue, CVar flags)
|
||||
public ConfigVar(string name, object defaultValue, CVar flags)
|
||||
{
|
||||
Name = name;
|
||||
DefaultValue = defaultValue;
|
||||
@@ -459,7 +482,7 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// The default value of this CVar.
|
||||
/// </summary>
|
||||
public object? DefaultValue { get; set; }
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional flags to modify the behavior of this CVar.
|
||||
|
||||
328
Robust.Shared/Configuration/NetConfigurationManager.cs
Normal file
328
Robust.Shared/Configuration/NetConfigurationManager.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A networked configuration manager that controls the replication of
|
||||
/// console variables between client and server.
|
||||
/// </summary>
|
||||
public interface INetConfigurationManager : IConfigurationManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up the networking for the config manager.
|
||||
/// </summary>
|
||||
void SetupNetworking();
|
||||
|
||||
/// <summary>
|
||||
/// Get a replicated client CVar for a specific client.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">CVar type.</typeparam>
|
||||
/// <param name="channel">channel of the connected client.</param>
|
||||
/// <param name="name">Name of the CVar.</param>
|
||||
/// <returns>Replicated CVar of the client.</returns>
|
||||
T GetClientCVar<T>(INetChannel channel, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Synchronize the CVars marked with <see cref="CVar.REPLICATED"/> with the client.
|
||||
/// This needs to be called once during the client connection.
|
||||
/// </summary>
|
||||
/// <param name="client">Client's NetChannel to sync replicated CVars with.</param>
|
||||
void SyncConnectingClient(INetChannel client);
|
||||
|
||||
/// <summary>
|
||||
/// Synchronize the CVars marked with <see cref="CVar.REPLICATED"/> with the server.
|
||||
/// This needs to be called once when connecting.
|
||||
/// </summary>
|
||||
void SyncWithServer();
|
||||
|
||||
/// <summary>
|
||||
/// Called every tick to process any incoming network messages.
|
||||
/// </summary>
|
||||
void TickProcessMessages();
|
||||
|
||||
/// <summary>
|
||||
/// Flushes any NwCVar messages in the receive buffer.
|
||||
/// </summary>
|
||||
void FlushMessages();
|
||||
|
||||
/// <summary>
|
||||
/// Clears internal flag for <see cref="ReceivedInitialNwVars"/>.
|
||||
/// Must be called upon disconnect.
|
||||
/// </summary>
|
||||
void ClearReceivedInitialNwVars();
|
||||
|
||||
public event EventHandler ReceivedInitialNwVars;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="INetConfigurationManager"/>
|
||||
internal class NetConfigurationManager : ConfigurationManager, INetConfigurationManager
|
||||
{
|
||||
[Dependency] private readonly INetManager _netManager = null!;
|
||||
[Dependency] private readonly IGameTiming _timing = null!;
|
||||
|
||||
private readonly Dictionary<INetChannel, Dictionary<string, object>> _replicatedCVars = new();
|
||||
private readonly List<MsgConVars> _netVarsMessages = new();
|
||||
|
||||
public event EventHandler? ReceivedInitialNwVars;
|
||||
private bool _receivedInitialNwVars;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetupNetworking()
|
||||
{
|
||||
if(_isServer)
|
||||
{
|
||||
_netManager.Connected += PeerConnected;
|
||||
_netManager.Disconnect += PeerDisconnected;
|
||||
}
|
||||
|
||||
_netManager.RegisterNetMessage<MsgConVars>(MsgConVars.NAME, HandleNetVarMessage);
|
||||
}
|
||||
|
||||
private void PeerConnected(object? sender, NetChannelArgs e)
|
||||
{
|
||||
_replicatedCVars.Add(e.Channel, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private void PeerDisconnected(object? sender, NetDisconnectedArgs e)
|
||||
{
|
||||
_replicatedCVars.Remove(e.Channel);
|
||||
}
|
||||
|
||||
private void HandleNetVarMessage(MsgConVars message)
|
||||
{
|
||||
if(!_receivedInitialNwVars)
|
||||
{
|
||||
_receivedInitialNwVars = true;
|
||||
|
||||
// apply the initial set immediately, so that they are available to
|
||||
// for the rest of connection building
|
||||
ApplyNetVarChange(message.MsgChannel, message.NetworkedVars);
|
||||
ReceivedInitialNwVars?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
else
|
||||
_netVarsMessages.Add(message);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TickProcessMessages()
|
||||
{
|
||||
if(!_timing.InSimulation || _timing.InPrediction)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _netVarsMessages.Count; i++)
|
||||
{
|
||||
var msg = _netVarsMessages[i];
|
||||
|
||||
if (msg.Tick > _timing.LastRealTick)
|
||||
continue;
|
||||
|
||||
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars);
|
||||
|
||||
if(msg.Tick < _timing.LastRealTick)
|
||||
Logger.WarningS("cfg", $"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.LastRealTick} ).");
|
||||
|
||||
_netVarsMessages.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void FlushMessages()
|
||||
{
|
||||
_netVarsMessages.Sort(((a, b) => a.Tick.Value.CompareTo(b.Tick.Value)));
|
||||
|
||||
foreach (var msg in _netVarsMessages)
|
||||
{
|
||||
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars);
|
||||
}
|
||||
|
||||
_netVarsMessages.Clear();
|
||||
}
|
||||
|
||||
private void ApplyNetVarChange(INetChannel msgChannel, List<(string name, object value)> networkedVars)
|
||||
{
|
||||
Logger.DebugS("cfg", "Handling replicated cvars...");
|
||||
|
||||
foreach (var (name, value) in networkedVars)
|
||||
{
|
||||
if (_netManager.IsClient) // Server sent us a CVar update.
|
||||
{
|
||||
// Actually set the CVar
|
||||
base.SetCVar(name, value);
|
||||
Logger.DebugS("cfg", $"name={name}, val={value}");
|
||||
}
|
||||
else // Client sent us a CVar update
|
||||
{
|
||||
if (!_configVars.TryGetValue(name, out var cVar))
|
||||
{
|
||||
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an unknown CVar '{name}.'");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cVar.Registered)
|
||||
{
|
||||
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an unregistered CVar '{name}.'");
|
||||
continue;
|
||||
}
|
||||
|
||||
if((cVar.Flags & CVar.REPLICATED) != 0)
|
||||
{
|
||||
var clientCVars = _replicatedCVars[msgChannel];
|
||||
|
||||
if (clientCVars.ContainsKey(name))
|
||||
clientCVars[name] = value;
|
||||
else
|
||||
clientCVars.Add(name, value);
|
||||
|
||||
Logger.DebugS("cfg", $"name={name}, val={value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an un-replicated CVar '{name}.'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetClientCVar<T>(INetChannel channel, string name)
|
||||
{
|
||||
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
|
||||
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
|
||||
|
||||
if (_replicatedCVars.TryGetValue(channel, out var clientCVars) && clientCVars.TryGetValue(name, out var value))
|
||||
{
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
return (T)(cVar.DefaultValue!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetCVar(string name, object value)
|
||||
{
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
{
|
||||
if (_netManager.IsClient)
|
||||
{
|
||||
if (_netManager.IsConnected)
|
||||
{
|
||||
if ((cVar.Flags & CVar.NOT_CONNECTED) != 0)
|
||||
{
|
||||
Logger.WarningS("cfg", $"'{name}' can only be changed when not connected to a server.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((cVar.Flags & CVar.SERVER) != 0)
|
||||
{
|
||||
Logger.WarningS("cfg", $"Only the server can change '{name}'.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
}
|
||||
|
||||
// Actually set the CVar
|
||||
base.SetCVar(name, value);
|
||||
|
||||
var cvar = _configVars[name];
|
||||
|
||||
// replicate if needed
|
||||
if (_netManager.IsClient)
|
||||
{
|
||||
if ((cvar.Flags & CVar.REPLICATED) == 0)
|
||||
return;
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = new List<(string name, object value)>
|
||||
{
|
||||
(name, value)
|
||||
};
|
||||
_netManager.ClientSendMessage(msg);
|
||||
}
|
||||
else // Server
|
||||
{
|
||||
if ((cvar.Flags & CVar.REPLICATED) == 0)
|
||||
return;
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = new List<(string name, object value)>
|
||||
{
|
||||
(name, value)
|
||||
};
|
||||
_netManager.ServerSendToAll(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SyncConnectingClient(INetChannel client)
|
||||
{
|
||||
DebugTools.Assert(_netManager.IsConnected);
|
||||
DebugTools.Assert(_netManager.IsServer);
|
||||
|
||||
Logger.InfoS("cfg", $"{client}: Sending server info...");
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = GetReplicatedVars();
|
||||
_netManager.ServerSendMessage(msg, client);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SyncWithServer()
|
||||
{
|
||||
DebugTools.Assert(_netManager.IsConnected);
|
||||
DebugTools.Assert(_netManager.IsClient);
|
||||
|
||||
Logger.InfoS("cfg", "Sending client info...");
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = GetReplicatedVars();
|
||||
_netManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
public void ClearReceivedInitialNwVars()
|
||||
{
|
||||
_receivedInitialNwVars = false;
|
||||
}
|
||||
|
||||
private List<(string name, object value)> GetReplicatedVars()
|
||||
{
|
||||
var nwVars = new List<(string name, object value)>();
|
||||
|
||||
foreach (var cVar in _configVars.Values)
|
||||
{
|
||||
if (!cVar.Registered)
|
||||
continue;
|
||||
|
||||
if ((cVar.Flags & CVar.REPLICATED) == 0)
|
||||
continue;
|
||||
|
||||
if (_netManager.IsClient && (cVar.Flags & CVar.SERVER) != 0)
|
||||
continue;
|
||||
|
||||
nwVars.Add((cVar.Name, cVar.Value ?? cVar.DefaultValue));
|
||||
|
||||
Logger.DebugS("cfg", $"name={cVar.Name}, val={(cVar.Value ?? cVar.DefaultValue)}");
|
||||
}
|
||||
|
||||
return nwVars;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ namespace Robust.Shared.ContentPack
|
||||
var fullPath = mountPath / filePath;
|
||||
Logger.DebugS("res.mod", $"Found module '{fullPath}'");
|
||||
|
||||
var asmFile = _res.ContentFileRead(fullPath);
|
||||
using var asmFile = _res.ContentFileRead(fullPath);
|
||||
var (asmRefs, asmName) = GetAssemblyReferenceData(asmFile);
|
||||
|
||||
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
|
||||
@@ -118,8 +118,8 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
else
|
||||
{
|
||||
var assemblyStream = _res.ContentFileRead(path);
|
||||
var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
|
||||
using var assemblyStream = _res.ContentFileRead(path);
|
||||
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
|
||||
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
|
||||
}
|
||||
}
|
||||
@@ -241,16 +241,34 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
if (_res.TryContentFileRead(dllPath, out var gameDll))
|
||||
{
|
||||
Logger.DebugS("srv", $"Loading {assemblyName} DLL");
|
||||
|
||||
// see if debug info is present
|
||||
if (_res.TryContentFileRead(new ResourcePath($@"/Assemblies/{assemblyName}.pdb"),
|
||||
out var gamePdb))
|
||||
using (gameDll)
|
||||
{
|
||||
Logger.DebugS("srv", $"Loading {assemblyName} DLL");
|
||||
|
||||
// see if debug info is present
|
||||
if (_res.TryContentFileRead(new ResourcePath($@"/Assemblies/{assemblyName}.pdb"),
|
||||
out var gamePdb))
|
||||
{
|
||||
using (gamePdb)
|
||||
{
|
||||
try
|
||||
{
|
||||
// load the assembly into the process, and bootstrap the GameServer entry point.
|
||||
LoadGameAssembly(gameDll, gamePdb);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("srv", $"Exception loading DLL {assemblyName}.dll: {e.ToStringBetter()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// load the assembly into the process, and bootstrap the GameServer entry point.
|
||||
LoadGameAssembly(gameDll, gamePdb);
|
||||
LoadGameAssembly(gameDll);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -259,18 +277,6 @@ namespace Robust.Shared.ContentPack
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// load the assembly into the process, and bootstrap the GameServer entry point.
|
||||
LoadGameAssembly(gameDll);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("srv", $"Exception loading DLL {assemblyName}.dll: {e.ToStringBetter()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.WarningS("eng", $"Could not load {assemblyName} DLL: {dllPath} does not exist in the VFS.");
|
||||
|
||||
@@ -216,7 +216,13 @@ namespace Robust.Shared.ContentPack
|
||||
/// <inheritdoc />
|
||||
public bool ContentFileExists(ResourcePath path)
|
||||
{
|
||||
return TryContentFileRead(path, out var _);
|
||||
if (TryContentFileRead(path, out var stream))
|
||||
{
|
||||
stream.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -442,6 +442,7 @@ Types:
|
||||
IsExternalInit: { All: True }
|
||||
IsReadOnlyAttribute: { All: True }
|
||||
IteratorStateMachineAttribute: { All: True }
|
||||
PreserveBaseOverridesAttribute: { All: True }
|
||||
RuntimeCompatibilityAttribute: { All: True }
|
||||
RuntimeHelpers: { All: True }
|
||||
TaskAwaiter: { All: True }
|
||||
|
||||
@@ -14,45 +14,7 @@ namespace Robust.Shared
|
||||
// On .NET Framework this doesn't need to run because:
|
||||
// On Windows, the DLL names should check out correctly to just work.
|
||||
// On Linux/macOS, Mono's DllMap handles it for us.
|
||||
#if NETCOREAPP
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// DLL names should line up on Windows by default.
|
||||
// So a hook won't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.SetDllImportResolver(assembly, (name, _, __) =>
|
||||
{
|
||||
if (name == $"{baseName}.dll")
|
||||
{
|
||||
var assemblyDir = Path.GetDirectoryName(assembly.Location);
|
||||
|
||||
if (assemblyDir == null)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
string libName;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
libName = $"lib{baseName}.so";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
libName = $"lib{baseName}.dylib";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(assemblyDir, libName));
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
});
|
||||
#endif
|
||||
RegisterExplicitMap(assembly, $"{baseName}.dll", $"lib{baseName}.so", $"lib{baseName}.dylib");
|
||||
}
|
||||
|
||||
[Conditional("NETCOREAPP")]
|
||||
@@ -62,32 +24,24 @@ namespace Robust.Shared
|
||||
// On Windows, the DLL names should check out correctly to just work.
|
||||
// On Linux/macOS, Mono's DllMap handles it for us.
|
||||
#if NETCOREAPP
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
NativeLibrary.SetDllImportResolver(assembly, (name, assembly, path) =>
|
||||
{
|
||||
// DLL names should line up on Windows by default.
|
||||
// So a hook won't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.SetDllImportResolver(assembly, (name, _, __) =>
|
||||
{
|
||||
if (name == baseName)
|
||||
// Please keep in sync with what GLFWNative does.
|
||||
// This particular API is only really used by the MIDI instruments stuff in SS14 right now,
|
||||
// which means when it breaks people don't notice or report.
|
||||
if (name != baseName)
|
||||
{
|
||||
string libName;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
libName = linuxName;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
libName = macName;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
return NativeLibrary.Load(libName);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return NativeLibrary.Load(linuxName, assembly, path);
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return NativeLibrary.Load(macName, assembly, path);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
|
||||
@@ -206,8 +206,6 @@ namespace Robust.Shared.GameObjects
|
||||
/// <inheritdoc />
|
||||
public void RemoveComponents(EntityUid uid)
|
||||
{
|
||||
_entCompIndex.Remove(uid);
|
||||
|
||||
foreach (var comp in InSafeOrder(_entCompIndex[uid]))
|
||||
{
|
||||
RemoveComponentDeferred(comp, uid, false);
|
||||
@@ -221,6 +219,10 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
RemoveComponentDeferred(comp, uid, true);
|
||||
}
|
||||
|
||||
// DisposeComponents means the entity is getting deleted.
|
||||
// Safe to wipe the entity out of the index.
|
||||
_entCompIndex.Remove(uid);
|
||||
}
|
||||
|
||||
private void RemoveComponentDeferred(Component component, EntityUid uid, bool removeProtected)
|
||||
@@ -306,6 +308,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var netId = component.NetID.Value;
|
||||
_entNetIdDict[netId].Remove(entityUid);
|
||||
_entCompIndex.Remove(entityUid, component);
|
||||
|
||||
// mark the owning entity as dirty for networking
|
||||
component.Owner.Dirty();
|
||||
|
||||
@@ -22,8 +22,8 @@ namespace Robust.Shared.GameObjects.Components.Appearance
|
||||
public abstract T GetData<T>(string key);
|
||||
public abstract T GetData<T>(Enum key);
|
||||
|
||||
public abstract bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data);
|
||||
public abstract bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data);
|
||||
public abstract bool TryGetData<T>(string key, [NotNullWhen(true)] out T data);
|
||||
public abstract bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected class AppearanceComponentState : ComponentState
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
@@ -41,6 +41,7 @@ namespace Robust.Shared.GameObjects.Components
|
||||
/// </summary>
|
||||
bool Anchored { get; set; }
|
||||
|
||||
[Obsolete("Use AnchoredChangedMessage instead")]
|
||||
event Action? AnchoredChanged;
|
||||
|
||||
bool Predict { get; set; }
|
||||
@@ -344,11 +345,15 @@ namespace Robust.Shared.GameObjects.Components
|
||||
return;
|
||||
|
||||
_anchored = value;
|
||||
#pragma warning disable 618
|
||||
AnchoredChanged?.Invoke();
|
||||
#pragma warning restore 618
|
||||
SendMessage(new AnchoredChangedMessage(Anchored));
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use AnchoredChangedMessage instead")]
|
||||
public event Action? AnchoredChanged;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -518,4 +523,14 @@ namespace Robust.Shared.GameObjects.Components
|
||||
return !Anchored && Mass > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnchoredChangedMessage : ComponentMessage
|
||||
{
|
||||
public readonly bool Anchored;
|
||||
|
||||
public AnchoredChangedMessage(bool anchored)
|
||||
{
|
||||
Anchored = anchored;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user