收口 Client 端,添加服务端约束

This commit is contained in:
SepComet 2026-03-29 15:06:17 +08:00
parent fbc09186f3
commit f6ca6fa6e4
12 changed files with 854 additions and 53 deletions

View File

@ -29,6 +29,24 @@ public static class ClientGameplayInputFlow
return true; return true;
} }
public static bool TryCreateShootInput(
string playerId,
long tick,
bool fireTriggered,
Vector3 aimDirection,
out ShootInput message,
string targetId = "")
{
if (!fireTriggered)
{
message = null;
return false;
}
message = CreateShootInput(playerId, tick, aimDirection, targetId);
return true;
}
public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "") public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "")
{ {
var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z); var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z);
@ -210,12 +228,14 @@ public class MovementComponent : MonoBehaviour
private ShootInput CaptureShootInput() private ShootInput CaptureShootInput()
{ {
if (!Input.GetMouseButtonDown(0)) return ClientGameplayInputFlow.TryCreateShootInput(
{ _master.PlayerId,
return null; Tick,
} Input.GetMouseButtonDown(0),
ResolveAimDirection(),
return ClientGameplayInputFlow.CreateShootInput(_master.PlayerId, Tick, ResolveAimDirection()); out var shootInput)
? shootInput
: null;
} }
private Vector3 ResolveAimDirection() private Vector3 ResolveAimDirection()

View File

@ -165,8 +165,6 @@ namespace Network.NetworkHost
if (input == null || if (input == null ||
string.IsNullOrWhiteSpace(input.PlayerId) || string.IsNullOrWhiteSpace(input.PlayerId) ||
string.IsNullOrWhiteSpace(input.TargetId) ||
string.Equals(input.PlayerId, input.TargetId, StringComparison.Ordinal) ||
!IsFinite(input.DirX) || !IsFinite(input.DirX) ||
!IsFinite(input.DirY)) !IsFinite(input.DirY))
{ {
@ -179,8 +177,13 @@ namespace Network.NetworkHost
return false; return false;
} }
if (!movementCoordinator.TryGetState(sender, out attackerState) || if (!movementCoordinator.TryGetState(sender, out attackerState) &&
!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) || !movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
{
return false;
}
if (!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
attackerState.IsDead || attackerState.IsDead ||
attackerState.Hp <= 0 || attackerState.Hp <= 0 ||
input.Tick <= attackerState.LastAcceptedShootTick) input.Tick <= attackerState.LastAcceptedShootTick)
@ -188,14 +191,93 @@ namespace Network.NetworkHost
return false; return false;
} }
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) || return TryResolveTargetState(input, attackerState, out targetState);
targetState.IsDead || }
targetState.Hp <= 0)
private bool TryResolveTargetState(
ShootInput input,
ServerAuthoritativeMovementState attackerState,
out ServerAuthoritativeMovementState targetState)
{
if (!string.IsNullOrWhiteSpace(input.TargetId))
{
if (string.Equals(attackerState.PlayerId, input.TargetId, StringComparison.Ordinal))
{
targetState = null;
return false;
}
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
targetState.IsDead ||
targetState.Hp <= 0)
{
targetState = null;
return false;
}
return true;
}
return TryResolveTargetStateFromAim(input, attackerState, out targetState);
}
private bool TryResolveTargetStateFromAim(
ShootInput input,
ServerAuthoritativeMovementState attackerState,
out ServerAuthoritativeMovementState targetState)
{
targetState = null;
var aimLength = MathF.Sqrt((input.DirX * input.DirX) + (input.DirY * input.DirY));
if (aimLength <= 0f)
{ {
return false; return false;
} }
return true; var aimX = input.DirX / aimLength;
var aimY = input.DirY / aimLength;
var bestAlignment = float.NegativeInfinity;
var bestDistanceSquared = float.PositiveInfinity;
foreach (var candidate in movementCoordinator.States)
{
if (candidate == null ||
string.Equals(candidate.PlayerId, attackerState.PlayerId, StringComparison.Ordinal) ||
candidate.IsDead ||
candidate.Hp <= 0)
{
continue;
}
var offsetX = candidate.PositionX - attackerState.PositionX;
var offsetY = candidate.PositionZ - attackerState.PositionZ;
var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY);
if (distanceSquared <= 0f)
{
continue;
}
var inverseDistance = 1f / MathF.Sqrt(distanceSquared);
var alignment = (offsetX * inverseDistance * aimX) + (offsetY * inverseDistance * aimY);
if (alignment <= 0f)
{
continue;
}
if (targetState == null ||
alignment > bestAlignment + 0.0001f ||
(MathF.Abs(alignment - bestAlignment) <= 0.0001f &&
(distanceSquared < bestDistanceSquared - 0.0001f ||
(MathF.Abs(distanceSquared - bestDistanceSquared) <= 0.0001f &&
string.CompareOrdinal(candidate.PlayerId, targetState.PlayerId) < 0))))
{
targetState = candidate;
bestAlignment = alignment;
bestDistanceSquared = distanceSquared;
}
}
return targetState != null;
} }
private void BroadcastRejectedShot(ShootInput input) private void BroadcastRejectedShot(ShootInput input)

View File

@ -41,6 +41,46 @@ namespace Network.NetworkHost
} }
} }
public bool EnsureState(IPEndPoint remoteEndPoint, string playerId, out ServerAuthoritativeMovementState state)
{
if (remoteEndPoint == null)
{
throw new ArgumentNullException(nameof(remoteEndPoint));
}
if (string.IsNullOrWhiteSpace(playerId))
{
state = null;
return false;
}
var normalizedSender = Normalize(remoteEndPoint);
var key = normalizedSender.ToString();
lock (gate)
{
if (statesByPeer.TryGetValue(key, out var existingState))
{
if (!string.Equals(existingState.PlayerId, playerId, StringComparison.Ordinal))
{
state = null;
return false;
}
state = CloneState(existingState);
return true;
}
var createdState = new ServerAuthoritativeMovementState(
normalizedSender,
playerId,
configuration.DefaultHp);
statesByPeer.Add(key, createdState);
state = CloneState(createdState);
return true;
}
}
public Task HandleMoveInputAsync(byte[] payload, IPEndPoint sender) public Task HandleMoveInputAsync(byte[] payload, IPEndPoint sender)
{ {
if (payload == null || sender == null) if (payload == null || sender == null)

View File

@ -15,6 +15,8 @@ namespace Network.NetworkHost
private readonly MessageManager messageManager; private readonly MessageManager messageManager;
private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator; private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator;
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator; private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
private readonly object playerIdentityGate = new();
private readonly Dictionary<string, string> playerIdsByPeer = new();
public ServerNetworkHost( public ServerNetworkHost(
ITransport transport, ITransport transport,
@ -97,6 +99,10 @@ namespace Network.NetworkHost
SessionCoordinator.RemoveAllSessions("Transport stopped"); SessionCoordinator.RemoveAllSessions("Transport stopped");
authoritativeMovementCoordinator.Clear(); authoritativeMovementCoordinator.Clear();
authoritativeCombatCoordinator.Clear(); authoritativeCombatCoordinator.Clear();
lock (playerIdentityGate)
{
playerIdsByPeer.Clear();
}
PublishMetricsSessionSnapshots(); PublishMetricsSessionSnapshots();
} }
@ -140,12 +146,20 @@ namespace Network.NetworkHost
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint) public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
{ {
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint); SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
BootstrapAuthoritativeMovementState(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint); PublishMetricsSessionSnapshot(remoteEndPoint);
} }
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
{
RememberPlayerId(remoteEndPoint, playerId);
NotifyLoginSucceeded(remoteEndPoint);
}
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null) public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
{ {
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason); SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
ForgetPlayerId(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint); PublishMetricsSessionSnapshot(remoteEndPoint);
} }
@ -188,6 +202,7 @@ namespace Network.NetworkHost
authoritativeMovementCoordinator.RemoveState(remoteEndPoint); authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
authoritativeCombatCoordinator.RemoveState(remoteEndPoint); authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
ForgetPlayerId(remoteEndPoint);
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected); RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport)) if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
@ -198,12 +213,111 @@ namespace Network.NetworkHost
return true; return true;
} }
private void HandleTransportReceive(byte[] _, IPEndPoint sender) private void HandleTransportReceive(byte[] data, IPEndPoint sender)
{ {
SessionCoordinator.ObserveTransportActivity(sender); SessionCoordinator.ObserveTransportActivity(sender);
ObservePlayerIdentity(data, sender);
PublishMetricsSessionSnapshot(sender); PublishMetricsSessionSnapshot(sender);
} }
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
{
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
{
return;
}
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
}
private void ObservePlayerIdentity(byte[] data, IPEndPoint sender)
{
if (data == null || sender == null)
{
return;
}
Envelope envelope;
try
{
envelope = Envelope.Parser.ParseFrom(data);
}
catch
{
return;
}
if ((MessageType)envelope.Type != MessageType.LoginRequest)
{
return;
}
LoginRequest request;
try
{
request = LoginRequest.Parser.ParseFrom(envelope.Payload);
}
catch
{
return;
}
RememberPlayerId(sender, request.PlayerId);
}
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
{
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
{
return;
}
var key = Normalize(remoteEndPoint).ToString();
lock (playerIdentityGate)
{
playerIdsByPeer[key] = playerId;
}
}
private bool TryGetKnownPlayerId(IPEndPoint remoteEndPoint, out string playerId)
{
playerId = null;
if (remoteEndPoint == null)
{
return false;
}
var key = Normalize(remoteEndPoint).ToString();
lock (playerIdentityGate)
{
return playerIdsByPeer.TryGetValue(key, out playerId);
}
}
private void ForgetPlayerId(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
{
return;
}
var key = Normalize(remoteEndPoint).ToString();
lock (playerIdentityGate)
{
playerIdsByPeer.Remove(key);
}
}
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
{
throw new ArgumentNullException(nameof(remoteEndPoint));
}
return new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port);
}
private void PublishMetricsSessionSnapshots() private void PublishMetricsSessionSnapshots()
{ {
foreach (var session in ManagedSessions) foreach (var session in ManagedSessions)

View File

@ -109,6 +109,133 @@ namespace Tests.EditMode.Network
Assert.That(remoteCombatPresentation.IsDead, Is.False); Assert.That(remoteCombatPresentation.IsDead, Is.False);
} }
[Test]
public void FakeTransportRoundTrip_IdleLoggedInPlayer_ReceivesPlayerState_AndCanShootWithoutMoveInput()
{
var clientReliableTransport = new GameplayFlowFakeTransport();
var clientSyncTransport = new GameplayFlowFakeTransport();
var clientRuntime = new SharedNetworkRuntime(
clientReliableTransport,
new MainThreadNetworkDispatcher(),
syncTransport: clientSyncTransport);
var clientHarness = new ClientGameplayTestHarness("player-a");
clientHarness.Register(clientRuntime.MessageManager);
var serverTransports = new Dictionary<int, GameplayFlowFakeTransport>();
var configuration = new ServerRuntimeConfiguration(9000)
{
SyncPort = 9001,
Dispatcher = new MainThreadNetworkDispatcher(),
TransportFactory = port => CreateTransport(serverTransports, port),
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
{
MoveSpeed = 10f,
BroadcastInterval = TimeSpan.FromMilliseconds(50),
DefaultHp = 100
},
AuthoritativeCombat = new ServerAuthoritativeCombatConfiguration
{
DamagePerShot = 30
}
};
clientRuntime.StartAsync().GetAwaiter().GetResult();
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
serverTransports[9000].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.LoginRequest,
new LoginRequest
{
PlayerId = "player-a",
Speed = 5
}),
ClientPeer);
serverTransports[9000].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.LoginRequest,
new LoginRequest
{
PlayerId = "player-b",
Speed = 5
}),
RemotePeer);
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer);
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer);
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
Assert.That(clientHarness.TryGetState("player-a", out var idleLocalState), Is.True);
Assert.That(idleLocalState.Tick, Is.EqualTo(1));
Assert.That(idleLocalState.Position.x, Is.EqualTo(0f).Within(0.0001f));
Assert.That(idleLocalState.Position.z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(idleLocalState.Hp, Is.EqualTo(100));
serverTransports[9001].ClearOutgoing();
serverTransports[9000].ClearOutgoing();
serverTransports[9001].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.MoveInput,
new MoveInput
{
PlayerId = "player-b",
Tick = 1,
MoveX = 1f,
MoveY = 0f
}),
RemotePeer);
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(100));
serverTransports[9001].ClearOutgoing();
serverTransports[9000].ClearOutgoing();
ClientGameplayInputFlow.SendShootInput(
clientRuntime.MessageManager,
"player-a",
3,
Vector3.right);
Assert.That(clientReliableTransport.SentMessages.Count, Is.EqualTo(1));
var outboundEnvelope = Envelope.Parser.ParseFrom(clientReliableTransport.SentMessages[0]);
var outboundShootInput = ShootInput.Parser.ParseFrom(outboundEnvelope.Payload);
Assert.That((MessageType)outboundEnvelope.Type, Is.EqualTo(MessageType.ShootInput));
Assert.That(outboundShootInput.TargetId, Is.EqualTo(string.Empty));
TransferSentMessages(clientReliableTransport, serverTransports[9000], ClientPeer);
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
TransferBroadcastMessages(serverTransports[9000], clientReliableTransport, ServerSender);
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localMovementState), Is.True);
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
Assert.That(localMovementState.LastAcceptedMoveTick, Is.EqualTo(0));
Assert.That(localMovementState.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(localMovementState.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
Assert.That(localCombatState.LastAcceptedShootTick, Is.EqualTo(3));
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True);
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
Assert.That(remoteCombatState.Hp, Is.EqualTo(70));
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
Assert.That(remoteCombatPresentation.LastEventType, Is.EqualTo(CombatEventType.DamageApplied));
Assert.That(remoteCombatPresentation.LastDamage, Is.EqualTo(30));
Assert.That(remoteCombatPresentation.IsDead, Is.False);
}
private static GameplayFlowFakeTransport CreateTransport(IDictionary<int, GameplayFlowFakeTransport> serverTransports, int port) private static GameplayFlowFakeTransport CreateTransport(IDictionary<int, GameplayFlowFakeTransport> serverTransports, int port)
{ {
var transport = new GameplayFlowFakeTransport(); var transport = new GameplayFlowFakeTransport();

View File

@ -138,6 +138,53 @@ namespace Tests.EditMode.Network
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f)); Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
} }
[Test]
public void NotifyLoginSucceeded_CreatesIdleAuthoritativeState_AndBroadcastsPlayerStateWithoutMoveInput()
{
var createdTransports = new Dictionary<int, FakeTransport>();
var configuration = new ServerRuntimeConfiguration(9000)
{
SyncPort = 9001,
Dispatcher = new MainThreadNetworkDispatcher(),
TransportFactory = port => CreateTransport(createdTransports, port),
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
{
MoveSpeed = 10f,
BroadcastInterval = TimeSpan.FromMilliseconds(50),
DefaultHp = 100
}
};
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.LoginRequest, new LoginRequest
{
PlayerId = "player-a",
Speed = 5
}), PeerA);
runtime.Host.NotifyLoginStarted(PeerA);
runtime.Host.NotifyLoginSucceeded(PeerA);
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
Assert.That(state.PlayerId, Is.EqualTo("player-a"));
Assert.That(state.LastAcceptedMoveTick, Is.EqualTo(0));
Assert.That(state.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(state.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(state.Hp, Is.EqualTo(100));
Assert.That(createdTransports[9001].BroadcastMessages.Count, Is.EqualTo(1));
var broadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
Assert.That(broadcast.PlayerId, Is.EqualTo("player-a"));
Assert.That(broadcast.Tick, Is.EqualTo(1));
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Hp, Is.EqualTo(100));
}
[Test] [Test]
public void UpdateAuthoritativeMovement_UsesReliableLaneWhenSyncTransportIsUnavailable() public void UpdateAuthoritativeMovement_UsesReliableLaneWhenSyncTransportIsUnavailable()
{ {

View File

@ -54,6 +54,33 @@ namespace Tests.EditMode.Network
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty)); Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
} }
[Test]
public void ClientGameplayInputFlow_TryCreateShootInput_LocalFirePathKeepsTargetOptional()
{
var created = ClientGameplayInputFlow.TryCreateShootInput(
"player-1",
21,
true,
new Vector3(2f, 0f, 0f),
out var shootInput);
var ignored = ClientGameplayInputFlow.TryCreateShootInput(
"player-1",
22,
false,
Vector3.forward,
out var ignoredShootInput);
Assert.That(created, Is.True);
Assert.That(shootInput, Is.Not.Null);
Assert.That(shootInput.PlayerId, Is.EqualTo("player-1"));
Assert.That(shootInput.Tick, Is.EqualTo(21));
Assert.That(shootInput.DirX, Is.EqualTo(1f));
Assert.That(shootInput.DirY, Is.EqualTo(0f));
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
Assert.That(ignored, Is.False);
Assert.That(ignoredShootInput, Is.Null);
}
[Test] [Test]
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs() public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
{ {

View File

@ -3,7 +3,7 @@
--- !u!129 &1 --- !u!129 &1
PlayerSettings: PlayerSettings:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
serializedVersion: 24 serializedVersion: 26
productGUID: edeee84d6a0456a4fa7942884ca6495f productGUID: edeee84d6a0456a4fa7942884ca6495f
AndroidProfiler: 0 AndroidProfiler: 0
AndroidFilterTouchesWhenObscured: 0 AndroidFilterTouchesWhenObscured: 0
@ -12,8 +12,8 @@ PlayerSettings:
targetDevice: 2 targetDevice: 2
useOnDemandResources: 0 useOnDemandResources: 0
accelerometerFrequency: 60 accelerometerFrequency: 60
companyName: DefaultCompany companyName: Comet
productName: NetworkFW productName: RUDPClient
defaultCursor: {fileID: 0} defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0} cursorHotspot: {x: 0, y: 0}
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
@ -49,14 +49,15 @@ PlayerSettings:
m_StereoRenderingPath: 0 m_StereoRenderingPath: 0
m_ActiveColorSpace: 1 m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0 unsupportedMSAAFallback: 0
m_SpriteBatchVertexThreshold: 300
m_MTRendering: 1 m_MTRendering: 1
mipStripping: 0 mipStripping: 0
numberOfMipsStripped: 0 numberOfMipsStripped: 0
numberOfMipsStrippedPerMipmapLimitGroup: {}
m_StackTraceTypes: 010000000100000001000000010000000100000001000000 m_StackTraceTypes: 010000000100000001000000010000000100000001000000
iosShowActivityIndicatorOnLoading: -1 iosShowActivityIndicatorOnLoading: -1
androidShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1
iosUseCustomAppBackgroundBehavior: 0 iosUseCustomAppBackgroundBehavior: 0
iosAllowHTTPDownload: 1
allowedAutorotateToPortrait: 1 allowedAutorotateToPortrait: 1
allowedAutorotateToPortraitUpsideDown: 1 allowedAutorotateToPortraitUpsideDown: 1
allowedAutorotateToLandscapeRight: 1 allowedAutorotateToLandscapeRight: 1
@ -76,6 +77,7 @@ PlayerSettings:
androidMinimumWindowHeight: 300 androidMinimumWindowHeight: 300
androidFullscreenMode: 1 androidFullscreenMode: 1
androidAutoRotationBehavior: 1 androidAutoRotationBehavior: 1
androidPredictiveBackSupport: 1
defaultIsNativeResolution: 1 defaultIsNativeResolution: 1
macRetinaSupport: 1 macRetinaSupport: 1
runInBackground: 1 runInBackground: 1
@ -83,15 +85,12 @@ PlayerSettings:
muteOtherAudioSources: 0 muteOtherAudioSources: 0
Prepare IOS For Recording: 0 Prepare IOS For Recording: 0
Force IOS Speakers When Recording: 0 Force IOS Speakers When Recording: 0
audioSpatialExperience: 0
deferSystemGesturesMode: 0 deferSystemGesturesMode: 0
hideHomeButton: 0 hideHomeButton: 0
submitAnalytics: 1 submitAnalytics: 1
usePlayerLog: 1 usePlayerLog: 1
autoStreaming: 0 dedicatedServerOptimizations: 0
useAnimationStreaming: 0
useFontStreaming: 0
autoStreamingId:
instantGameAppId:
bakeCollisionMeshes: 0 bakeCollisionMeshes: 0
forceSingleInstance: 0 forceSingleInstance: 0
useFlipModelSwapchain: 1 useFlipModelSwapchain: 1
@ -126,8 +125,11 @@ PlayerSettings:
switchNVNShaderPoolsGranularity: 33554432 switchNVNShaderPoolsGranularity: 33554432
switchNVNDefaultPoolsGranularity: 16777216 switchNVNDefaultPoolsGranularity: 16777216
switchNVNOtherPoolsGranularity: 16777216 switchNVNOtherPoolsGranularity: 16777216
switchGpuScratchPoolGranularity: 2097152
switchAllowGpuScratchShrinking: 0
switchNVNMaxPublicTextureIDCount: 0 switchNVNMaxPublicTextureIDCount: 0
switchNVNMaxPublicSamplerIDCount: 0 switchNVNMaxPublicSamplerIDCount: 0
switchNVNGraphicsFirmwareMemory: 32
switchMaxWorkerMultiple: 8 switchMaxWorkerMultiple: 8
stadiaPresentMode: 0 stadiaPresentMode: 0
stadiaTargetFramerate: 0 stadiaTargetFramerate: 0
@ -136,12 +138,9 @@ PlayerSettings:
vulkanEnablePreTransform: 1 vulkanEnablePreTransform: 1
vulkanEnableLateAcquireNextImage: 0 vulkanEnableLateAcquireNextImage: 0
vulkanEnableCommandBufferRecycling: 1 vulkanEnableCommandBufferRecycling: 1
m_SupportedAspectRatios: loadStoreDebugModeEnabled: 0
4:3: 1 visionOSBundleVersion: 1.0
5:4: 1 tvOSBundleVersion: 1.0
16:10: 1
16:9: 1
Others: 1
bundleVersion: 0.1 bundleVersion: 0.1
preloadedAssets: [] preloadedAssets: []
metroInputSource: 0 metroInputSource: 0
@ -154,8 +153,9 @@ PlayerSettings:
isWsaHolographicRemotingEnabled: 0 isWsaHolographicRemotingEnabled: 0
enableFrameTimingStats: 0 enableFrameTimingStats: 0
enableOpenGLProfilerGPURecorders: 1 enableOpenGLProfilerGPURecorders: 1
allowHDRDisplaySupport: 0
useHDRDisplay: 0 useHDRDisplay: 0
D3DHDRBitDepth: 0 hdrBitDepth: 0
m_ColorGamuts: 00000000 m_ColorGamuts: 00000000
targetPixelDensity: 30 targetPixelDensity: 30
resolutionScalingMode: 0 resolutionScalingMode: 0
@ -163,9 +163,10 @@ PlayerSettings:
androidSupportedAspectRatio: 1 androidSupportedAspectRatio: 1
androidMaxAspectRatio: 2.1 androidMaxAspectRatio: 2.1
applicationIdentifier: applicationIdentifier:
Standalone: com.DefaultCompany.NetworkFW Standalone: com.Comet.RUDPClient
buildNumber: buildNumber:
Standalone: 0 Standalone: 0
VisionOS: 0
iPhone: 0 iPhone: 0
tvOS: 0 tvOS: 0
overrideDefaultApplicationIdentifier: 0 overrideDefaultApplicationIdentifier: 0
@ -183,12 +184,17 @@ PlayerSettings:
APKExpansionFiles: 0 APKExpansionFiles: 0
keepLoadedShadersAlive: 0 keepLoadedShadersAlive: 0
StripUnusedMeshComponents: 1 StripUnusedMeshComponents: 1
strictShaderVariantMatching: 0
VertexChannelCompressionMask: 4054 VertexChannelCompressionMask: 4054
iPhoneSdkVersion: 988 iPhoneSdkVersion: 988
iOSSimulatorArchitecture: 0
iOSTargetOSVersionString: 12.0 iOSTargetOSVersionString: 12.0
tvOSSdkVersion: 0 tvOSSdkVersion: 0
tvOSSimulatorArchitecture: 0
tvOSRequireExtendedGameController: 0 tvOSRequireExtendedGameController: 0
tvOSTargetOSVersionString: 12.0 tvOSTargetOSVersionString: 12.0
VisionOSSdkVersion: 0
VisionOSTargetOSVersionString: 1.0
uIPrerenderedIcon: 0 uIPrerenderedIcon: 0
uIRequiresPersistentWiFi: 0 uIRequiresPersistentWiFi: 0
uIRequiresFullScreen: 1 uIRequiresFullScreen: 1
@ -231,13 +237,16 @@ PlayerSettings:
iOSMetalForceHardShadows: 0 iOSMetalForceHardShadows: 0
metalEditorSupport: 1 metalEditorSupport: 1
metalAPIValidation: 1 metalAPIValidation: 1
metalCompileShaderBinary: 0
iOSRenderExtraFrameOnPause: 0 iOSRenderExtraFrameOnPause: 0
iosCopyPluginsCodeInsteadOfSymlink: 0 iosCopyPluginsCodeInsteadOfSymlink: 0
appleDeveloperTeamID: appleDeveloperTeamID:
iOSManualSigningProvisioningProfileID: iOSManualSigningProvisioningProfileID:
tvOSManualSigningProvisioningProfileID: tvOSManualSigningProvisioningProfileID:
VisionOSManualSigningProvisioningProfileID:
iOSManualSigningProvisioningProfileType: 0 iOSManualSigningProvisioningProfileType: 0
tvOSManualSigningProvisioningProfileType: 0 tvOSManualSigningProvisioningProfileType: 0
VisionOSManualSigningProvisioningProfileType: 0
appleEnableAutomaticSigning: 0 appleEnableAutomaticSigning: 0
iOSRequireARKit: 0 iOSRequireARKit: 0
iOSAutomaticallyDetectAndAddCapabilities: 1 iOSAutomaticallyDetectAndAddCapabilities: 1
@ -260,6 +269,7 @@ PlayerSettings:
androidSplashScreen: {fileID: 0} androidSplashScreen: {fileID: 0}
AndroidKeystoreName: AndroidKeystoreName:
AndroidKeyaliasName: AndroidKeyaliasName:
AndroidEnableArmv9SecurityFeatures: 0
AndroidBuildApkPerCpuArchitecture: 0 AndroidBuildApkPerCpuArchitecture: 0
AndroidTVCompatibility: 0 AndroidTVCompatibility: 0
AndroidIsGame: 1 AndroidIsGame: 1
@ -456,7 +466,9 @@ PlayerSettings:
m_EncodingQuality: 1 m_EncodingQuality: 1
- m_BuildTarget: tvOS - m_BuildTarget: tvOS
m_EncodingQuality: 1 m_EncodingQuality: 1
m_BuildTargetGroupHDRCubemapEncodingQuality: []
m_BuildTargetGroupLightmapSettings: [] m_BuildTargetGroupLightmapSettings: []
m_BuildTargetGroupLoadStoreDebugModeSettings: []
m_BuildTargetNormalMapEncoding: m_BuildTargetNormalMapEncoding:
- m_BuildTarget: Android - m_BuildTarget: Android
m_Encoding: 1 m_Encoding: 1
@ -477,6 +489,7 @@ PlayerSettings:
locationUsageDescription: locationUsageDescription:
microphoneUsageDescription: microphoneUsageDescription:
bluetoothUsageDescription: bluetoothUsageDescription:
macOSTargetOSVersion: 10.13.0
switchNMETAOverride: switchNMETAOverride:
switchNetLibKey: switchNetLibKey:
switchSocketMemoryPoolSize: 6144 switchSocketMemoryPoolSize: 6144
@ -488,6 +501,7 @@ PlayerSettings:
switchLTOSetting: 0 switchLTOSetting: 0
switchApplicationID: 0x01004b9000490000 switchApplicationID: 0x01004b9000490000
switchNSODependencies: switchNSODependencies:
switchCompilerFlags:
switchTitleNames_0: switchTitleNames_0:
switchTitleNames_1: switchTitleNames_1:
switchTitleNames_2: switchTitleNames_2:
@ -613,6 +627,7 @@ PlayerSettings:
switchSocketBufferEfficiency: 4 switchSocketBufferEfficiency: 4
switchSocketInitializeEnabled: 1 switchSocketInitializeEnabled: 1
switchNetworkInterfaceManagerInitializeEnabled: 1 switchNetworkInterfaceManagerInitializeEnabled: 1
switchDisableHTCSPlayerConnection: 0
switchUseNewStyleFilepaths: 0 switchUseNewStyleFilepaths: 0
switchUseLegacyFmodPriorities: 1 switchUseLegacyFmodPriorities: 1
switchUseMicroSleepForYield: 1 switchUseMicroSleepForYield: 1
@ -702,6 +717,7 @@ PlayerSettings:
webGLMemorySize: 16 webGLMemorySize: 16
webGLExceptionSupport: 1 webGLExceptionSupport: 1
webGLNameFilesAsHashes: 0 webGLNameFilesAsHashes: 0
webGLShowDiagnostics: 0
webGLDataCaching: 1 webGLDataCaching: 1
webGLDebugSymbols: 0 webGLDebugSymbols: 0
webGLEmscriptenArgs: webGLEmscriptenArgs:
@ -714,12 +730,19 @@ PlayerSettings:
webGLLinkerTarget: 1 webGLLinkerTarget: 1
webGLThreadsSupport: 0 webGLThreadsSupport: 0
webGLDecompressionFallback: 0 webGLDecompressionFallback: 0
webGLInitialMemorySize: 32
webGLMaximumMemorySize: 2048
webGLMemoryGrowthMode: 2
webGLMemoryLinearGrowthStep: 16
webGLMemoryGeometricGrowthStep: 0.2
webGLMemoryGeometricGrowthCap: 96
webGLPowerPreference: 2 webGLPowerPreference: 2
scriptingDefineSymbols: {} scriptingDefineSymbols: {}
additionalCompilerArguments: {} additionalCompilerArguments: {}
platformArchitecture: {} platformArchitecture: {}
scriptingBackend: {} scriptingBackend: {}
il2cppCompilerConfiguration: {} il2cppCompilerConfiguration: {}
il2cppCodeGeneration: {}
managedStrippingLevel: managedStrippingLevel:
EmbeddedLinux: 1 EmbeddedLinux: 1
GameCoreScarlett: 1 GameCoreScarlett: 1
@ -738,11 +761,9 @@ PlayerSettings:
suppressCommonWarnings: 1 suppressCommonWarnings: 1
allowUnsafeCode: 0 allowUnsafeCode: 0
useDeterministicCompilation: 1 useDeterministicCompilation: 1
enableRoslynAnalyzers: 1
additionalIl2CppArgs: additionalIl2CppArgs:
scriptingRuntimeVersion: 1 scriptingRuntimeVersion: 1
gcIncremental: 1 gcIncremental: 1
assemblyVersionValidation: 1
gcWBarrierValidation: 0 gcWBarrierValidation: 0
apiCompatibilityLevelPerPlatform: {} apiCompatibilityLevelPerPlatform: {}
m_RenderingPath: 1 m_RenderingPath: 1
@ -816,6 +837,11 @@ PlayerSettings:
luminVersion: luminVersion:
m_VersionCode: 1 m_VersionCode: 1
m_VersionName: m_VersionName:
hmiPlayerDataPath:
hmiForceSRGBBlit: 1
embeddedLinuxEnableGamepadInput: 1
hmiLogStartupTiming: 0
hmiCpuConfiguration:
apiCompatibilityLevel: 6 apiCompatibilityLevel: 6
activeInputHandler: 0 activeInputHandler: 0
windowsGamepadBackendHint: 0 windowsGamepadBackendHint: 0
@ -826,6 +852,7 @@ PlayerSettings:
organizationId: organizationId:
cloudEnabled: 0 cloudEnabled: 0
legacyClampBlendShapeWeights: 0 legacyClampBlendShapeWeights: 0
playerDataPath: hmiLoadingImage: {fileID: 0}
forceSRGBBlit: 1 platformRequiresReadableAssets: 0
virtualTexturingSupportEnabled: 0 virtualTexturingSupportEnabled: 0
insecureHttpOption: 0

View File

@ -6,7 +6,7 @@ QualitySettings:
serializedVersion: 5 serializedVersion: 5
m_CurrentQuality: 5 m_CurrentQuality: 5
m_QualitySettings: m_QualitySettings:
- serializedVersion: 2 - serializedVersion: 3
name: Very Low name: Very Low
pixelLightCount: 0 pixelLightCount: 0
shadows: 0 shadows: 0
@ -19,17 +19,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0 shadowmaskMode: 0
skinWeights: 1 skinWeights: 1
textureQuality: 1 globalTextureMipmapLimit: 1
textureMipmapLimitSettings: []
anisotropicTextures: 0 anisotropicTextures: 0
antiAliasing: 0 antiAliasing: 0
softParticles: 0 softParticles: 0
softVegetation: 0 softVegetation: 0
realtimeReflectionProbes: 0 realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0 billboardsFaceCameraPosition: 0
useLegacyDetailDistribution: 1
vSyncCount: 0 vSyncCount: 0
realtimeGICPUUsage: 25 realtimeGICPUUsage: 25
lodBias: 0.3 lodBias: 0.3
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -42,8 +45,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: 0d67abbb2298fc9459f38304722fc15e, type: 2} customRenderPipeline: {fileID: 11400000, guid: 0d67abbb2298fc9459f38304722fc15e, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
- serializedVersion: 2 - serializedVersion: 3
name: Low name: Low
pixelLightCount: 0 pixelLightCount: 0
shadows: 0 shadows: 0
@ -56,17 +68,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0 shadowmaskMode: 0
skinWeights: 2 skinWeights: 2
textureQuality: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: []
anisotropicTextures: 0 anisotropicTextures: 0
antiAliasing: 0 antiAliasing: 0
softParticles: 0 softParticles: 0
softVegetation: 0 softVegetation: 0
realtimeReflectionProbes: 0 realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0 billboardsFaceCameraPosition: 0
useLegacyDetailDistribution: 1
vSyncCount: 0 vSyncCount: 0
realtimeGICPUUsage: 25 realtimeGICPUUsage: 25
lodBias: 0.4 lodBias: 0.4
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -79,8 +94,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: ee020ed1d7c224c41ba3c53ccc1f6df4, type: 2} customRenderPipeline: {fileID: 11400000, guid: ee020ed1d7c224c41ba3c53ccc1f6df4, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
- serializedVersion: 2 - serializedVersion: 3
name: Medium name: Medium
pixelLightCount: 1 pixelLightCount: 1
shadows: 1 shadows: 1
@ -93,17 +117,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0 shadowmaskMode: 0
skinWeights: 2 skinWeights: 2
textureQuality: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: []
anisotropicTextures: 1 anisotropicTextures: 1
antiAliasing: 0 antiAliasing: 0
softParticles: 0 softParticles: 0
softVegetation: 0 softVegetation: 0
realtimeReflectionProbes: 0 realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0 billboardsFaceCameraPosition: 0
useLegacyDetailDistribution: 1
vSyncCount: 1 vSyncCount: 1
realtimeGICPUUsage: 25 realtimeGICPUUsage: 25
lodBias: 0.7 lodBias: 0.7
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -116,8 +143,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: 9db80055e1a1b534c98757da268d2411, type: 2} customRenderPipeline: {fileID: 11400000, guid: 9db80055e1a1b534c98757da268d2411, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
- serializedVersion: 2 - serializedVersion: 3
name: High name: High
pixelLightCount: 2 pixelLightCount: 2
shadows: 2 shadows: 2
@ -130,17 +166,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1 shadowmaskMode: 1
skinWeights: 2 skinWeights: 2
textureQuality: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: []
anisotropicTextures: 1 anisotropicTextures: 1
antiAliasing: 0 antiAliasing: 0
softParticles: 0 softParticles: 0
softVegetation: 1 softVegetation: 1
realtimeReflectionProbes: 1 realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1 billboardsFaceCameraPosition: 1
useLegacyDetailDistribution: 1
vSyncCount: 1 vSyncCount: 1
realtimeGICPUUsage: 50 realtimeGICPUUsage: 50
lodBias: 1 lodBias: 1
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -153,8 +192,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: 37af5b7ddac898049aa900a75714c8e9, type: 2} customRenderPipeline: {fileID: 11400000, guid: 37af5b7ddac898049aa900a75714c8e9, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
- serializedVersion: 2 - serializedVersion: 3
name: Very High name: Very High
pixelLightCount: 3 pixelLightCount: 3
shadows: 2 shadows: 2
@ -167,17 +215,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1 shadowmaskMode: 1
skinWeights: 4 skinWeights: 4
textureQuality: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: []
anisotropicTextures: 2 anisotropicTextures: 2
antiAliasing: 2 antiAliasing: 2
softParticles: 1 softParticles: 1
softVegetation: 1 softVegetation: 1
realtimeReflectionProbes: 1 realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1 billboardsFaceCameraPosition: 1
useLegacyDetailDistribution: 1
vSyncCount: 1 vSyncCount: 1
realtimeGICPUUsage: 50 realtimeGICPUUsage: 50
lodBias: 1.5 lodBias: 1.5
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -190,8 +241,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: 49995bcf136441e4481296b98b9fbbef, type: 2} customRenderPipeline: {fileID: 11400000, guid: 49995bcf136441e4481296b98b9fbbef, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
- serializedVersion: 2 - serializedVersion: 3
name: Ultra name: Ultra
pixelLightCount: 4 pixelLightCount: 4
shadows: 2 shadows: 2
@ -204,17 +264,20 @@ QualitySettings:
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1 shadowmaskMode: 1
skinWeights: 4 skinWeights: 4
textureQuality: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: []
anisotropicTextures: 2 anisotropicTextures: 2
antiAliasing: 2 antiAliasing: 2
softParticles: 1 softParticles: 1
softVegetation: 1 softVegetation: 1
realtimeReflectionProbes: 1 realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1 billboardsFaceCameraPosition: 1
useLegacyDetailDistribution: 1
vSyncCount: 1 vSyncCount: 1
realtimeGICPUUsage: 100 realtimeGICPUUsage: 100
lodBias: 2 lodBias: 2
maximumLODLevel: 0 maximumLODLevel: 0
enableLODCrossFade: 1
streamingMipmapsActive: 0 streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1 streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512 streamingMipmapsMemoryBudget: 512
@ -227,7 +290,17 @@ QualitySettings:
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 11400000, guid: 4ac236add0ee4814c84ffdc469a9699d, type: 2} customRenderPipeline: {fileID: 11400000, guid: 4ac236add0ee4814c84ffdc469a9699d, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1
terrainBasemapDistance: 1000
terrainDetailDistance: 80
terrainTreeDistance: 5000
terrainBillboardStart: 50
terrainFadeLength: 5
terrainMaxTrees: 50
excludedTargetPlatforms: [] excludedTargetPlatforms: []
m_TextureMipmapLimitGroupNames: []
m_PerPlatformDefaultQuality: m_PerPlatformDefaultQuality:
Android: 2 Android: 2
GameCoreScarlett: 5 GameCoreScarlett: 5
@ -237,6 +310,7 @@ QualitySettings:
Nintendo Switch: 5 Nintendo Switch: 5
PS4: 5 PS4: 5
PS5: 5 PS5: 5
Server: 0
Stadia: 5 Stadia: 5
Standalone: 5 Standalone: 5
WebGL: 3 WebGL: 3

View File

@ -0,0 +1,238 @@
# Server Application Layer Conventions
## Purpose
This document describes the contracts that the server application layer must follow when built on top of the shared networking layer under `Assets/Scripts/Network/`.
The goal is to keep the server-side gameplay/application code aligned with the existing dual-lane transport, session lifecycle, tick filtering, and metrics model.
## Scope Boundary
- Shared networking concerns belong in `Assets/Scripts/Network/`.
- Server application concerns should sit above the shared network layer and consume it through `ServerNetworkHost`, `ServerRuntimeEntryPoint`, `ServerRuntimeHandle`, `MessageManager`, and the authoritative coordinators.
- Do not move Unity-only logic into shared network code.
- Do not make the shared network layer depend on scene objects, MonoBehaviours, or Unity presentation state.
## Startup Contract
- Start the dedicated server through `ServerRuntimeEntryPoint.StartAsync(...)` or `NetworkIntegrationFactory.CreateServerHost(...)`.
- Configure distinct reliable and sync ports when dual-lane behavior is required.
- Do not bind reliable and sync traffic to the same port when the intention is to validate mixed sync behavior. `NetworkIntegrationFactory` treats identical ports as invalid.
- Validate server-side tuning through `ServerRuntimeConfiguration`, `ServerAuthoritativeMovementConfiguration`, and `ServerAuthoritativeCombatConfiguration` instead of ad hoc constants spread across gameplay code.
## Session Lifecycle Contract
The server application layer is responsible for driving session state transitions at the correct time.
- Call `NotifyLoginStarted(remoteEndPoint)` when login processing begins.
- Call `NotifyLoginSucceeded(remoteEndPoint, playerId)` after the login has been accepted and the peer identity is known.
- Call `NotifyLoginFailed(remoteEndPoint, reason)` when login is rejected.
- Call `NotifyHeartbeatReceived(remoteEndPoint, serverTick)` when a heartbeat request is accepted and answered.
- Call `NotifyInboundActivity(remoteEndPoint)` only for accepted peer traffic that should refresh liveness.
- Call `RemoveSession(remoteEndPoint, reason)` when the player disconnects, logs out, times out permanently, or is forcefully removed.
Important implications:
- `NotifyLoginSucceeded(remoteEndPoint, playerId)` is not just bookkeeping. It also bootstraps the peer's authoritative movement state through `ServerNetworkHost`.
- If the application layer forgets to send login success into the host, a freshly logged-in idle player may never receive initial authoritative state and may fail later gameplay assumptions.
- Removing a session through the host also clears authoritative movement state, combat state, and remembered player identity for that peer.
## Message Routing Contract
The application layer must preserve the shared lane mapping.
- `MoveInput` uses `DeliveryPolicy.HighFrequencySync`.
- `PlayerState` uses `DeliveryPolicy.HighFrequencySync`.
- `ShootInput` uses `DeliveryPolicy.ReliableOrdered`.
- `CombatEvent` uses `DeliveryPolicy.ReliableOrdered`.
- Any new high-frequency "latest wins" snapshot-like message must be explicitly mapped to the sync lane.
- Any gameplay event that must not be dropped must remain on the reliable ordered lane.
Do not collapse movement and combat intent back into one broad input message if they require different delivery behavior.
## Envelope And Handler Contract
- All inbound and outbound gameplay messages must go through `MessageManager`.
- Do not bypass `MessageManager` by writing transport-specific send logic in application code.
- Register handlers on `MessageManager` using `MessageType`.
- Assume the shared layer always wraps payloads in `Envelope`.
Implications for server application code:
- If you add a new gameplay message, you need both a protobuf definition and a handler registration path.
- Broadcasts should use `MessageManager.BroadcastMessage(...)`.
- Directed replies should use `MessageManager.SendMessage(..., target)`.
## Tick And Sequence Contract
The shared sync filter currently applies stale-packet rejection to:
- `MoveInput`, keyed by sender + playerId + tick.
- `PlayerState`, keyed by playerId + tick.
Server application rules:
- Treat `tick` as required for all gameplay-relevant messages.
- Keep `MoveInput.Tick` monotonic per player.
- Keep `PlayerState.Tick` monotonic per player.
- Do not expect stale `MoveInput` packets to reach gameplay handlers.
- Do not add stale filtering assumptions for reliable gameplay events unless the shared layer is explicitly extended for that purpose.
## Authoritative Ownership Contract
The current shared/server runtime assumes the server application layer owns gameplay truth.
Server authoritative data includes:
- final position
- final rotation used for authoritative state
- HP
- accepted or rejected shooting
- hit resolution
- death state
Application-layer rules:
- Clients may submit intent only; they must not finalize gameplay outcomes.
- The server should apply `MoveInput` and `ShootInput` as requests, not as trusted outcomes.
- Authoritative snapshots sent back to clients must be derived from server state, not echoed from client state.
## Movement Contract
The movement-side server application layer should follow these rules:
- Ensure every logged-in player has authoritative movement state, even before the first non-zero `MoveInput`.
- Accept zero-vector `MoveInput` as valid current intent.
- Keep authoritative movement progression on the server, not on the client.
- Broadcast authoritative `PlayerState` on a fixed interval using server-owned timing.
- Include `position`, `rotation`, `hp`, and `velocity` when building `PlayerState`.
Do not assume a player who has not moved yet can be omitted from authoritative broadcast state.
## Combat Contract
The combat-side server application layer should follow these rules:
- Treat `ShootInput` as a reliable gameplay request.
- Validate shooter identity against the sending peer.
- Reject shots from dead players or from stale shoot ticks.
- Support both explicit-target and aim-based resolution if `targetId` is optional in the gameplay contract.
- Apply damage and death server-side before broadcasting combat results.
- Emit `CombatEvent` as the only authoritative combat result stream consumed by clients.
Current expected `CombatEvent` usage:
- `Hit` for resolved contact.
- `DamageApplied` for HP loss.
- `Death` for kill resolution.
- `ShootRejected` for invalid fire requests.
## Broadcast Contract
- Use `BroadcastMessage(PlayerState, MessageType.PlayerState)` for authoritative state snapshots.
- Use `BroadcastMessage(CombatEvent, MessageType.CombatEvent)` for authoritative combat outcomes.
- Do not use per-client divergent truth for shared gameplay state unless the design explicitly requires private information.
If a message represents common world truth, broadcast one authoritative result instead of recomputing or customizing it client by client.
## Metrics Contract
The transport layer already supports metrics and diagnostics capture. The server application layer should preserve that signal instead of bypassing it.
- Use transports that implement the existing metrics sink path when running diagnostics or bad-network tests.
- Keep session transitions routed through `ServerNetworkHost` so transport/application snapshots remain coherent.
- Record login success, heartbeat activity, disconnects, and authoritative tick progress through the host/runtime APIs rather than out-of-band state machines.
- When testing with tools like Clumsy, compare sync-lane degradation and reliable-lane survivability using the generated transport reports instead of subjective observation alone.
Practical expectation:
- sync lane may degrade in freshness under packet loss or jitter
- reliable lane may pay retransmission and latency cost
- gameplay semantics on the reliable lane should still remain correct and ordered
## Dispatcher Contract
- Client code may use deferred dispatchers such as `MainThreadNetworkDispatcher`.
- Server code defaults to immediate dispatch through `ServerNetworkHost`.
- If the server application layer injects a custom dispatcher, it must preserve deterministic handling expectations and must not accidentally require Unity main-thread semantics.
Do not introduce a Unity main-thread dependency into the dedicated server path.
## Identity Contract
- The authoritative identity for a peer is the combination of endpoint and accepted player id.
- Login handling must establish that mapping before gameplay is allowed to proceed.
- Any gameplay message whose `playerId` does not match the accepted identity for the sender should be treated as invalid.
This is especially important for:
- `MoveInput`
- `ShootInput`
- future player-owned gameplay messages
## Extending The Message Set
When adding a new gameplay message, the server application layer should answer all of the following before implementation:
1. Is this a sync message or a reliable gameplay event?
2. Does it require monotonic tick semantics?
3. Does stale filtering apply?
4. Is the message peer-owned input, server-owned state, or server-owned event?
5. Is the message broadcast world truth or a directed reply?
6. What metrics should confirm correct behavior under packet loss, latency, and jitter?
If these answers are unclear, the message design is not ready.
## Test Contract
Any server application-layer change that touches gameplay networking should add or update edit-mode regression coverage.
Minimum expectations:
- one test for lane routing if delivery behavior changes
- one test for authoritative server acceptance/rejection behavior
- one test for client-visible end-to-end outcome when the gameplay flow changes
- one test for a realistic edge case if the change fixes a regression
Examples of edge cases worth preserving:
- idle logged-in player can still receive `PlayerState`
- idle logged-in player can still shoot
- stale `MoveInput` is ignored
- `ShootInput` without `targetId` still resolves correctly when aim-based targeting is intended
## Anti-Patterns To Avoid
- Bypassing `MessageManager` and writing directly to `ITransport`.
- Letting the client authoritatively decide damage, death, or final position.
- Treating login/session state as a parallel system disconnected from `ServerNetworkHost`.
- Emitting authoritative gameplay state before the server has established peer identity.
- Depending on Unity scene state inside shared network code.
- Adding new gameplay messages without deciding their lane and tick behavior.
- Passing bad or missing player identity into `NotifyLoginSucceeded`.
- Forgetting to remove authoritative state when the session is removed.
## Checklist For New Server Application Features
- define protobuf message fields and ownership clearly
- choose reliable lane or sync lane explicitly
- decide tick semantics explicitly
- register handlers through `MessageManager`
- validate sender identity against accepted session identity
- update authoritative server state only on the server
- emit `PlayerState` or `CombatEvent` style outputs as needed
- route login/heartbeat/disconnect transitions through `ServerNetworkHost`
- verify metrics remain readable under Clumsy or equivalent bad-network simulation
- add regression tests
## Current Reference Types
- `Assets/Scripts/Network/NetworkApplication/MessageManager.cs`
- `Assets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.cs`
- `Assets/Scripts/Network/NetworkApplication/SyncSequenceTracker.cs`
- `Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs`
- `Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs`
- `Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs`
- `Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs`
- `Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs`
- `Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs`

View File

@ -5,15 +5,15 @@ Define the shared server-side combat authority contract that accepts `ShootInput
## Requirements ## Requirements
### Requirement: Server registers and validates `ShootInput` per peer ### Requirement: Server registers and validates `ShootInput` per peer
The shared server networking path SHALL register `ShootInput` handling through the server host/runtime composition and SHALL validate each inbound shooting request against the sending managed peer before applying any authoritative combat result. Validation MUST reject malformed numeric input, missing or mismatched player identity, non-increasing shoot ticks for that sender, zero-direction fire requests, and targets that do not resolve to a living managed peer. The shared server networking path SHALL register `ShootInput` handling through the server host/runtime composition and SHALL validate each inbound shooting request against the sending managed peer before applying any authoritative combat result. Validation MUST reject malformed numeric input, missing or mismatched player identity, non-increasing shoot ticks for that sender, zero-direction fire requests, and shot requests that do not resolve to a living managed peer either through an explicit `targetId` or through server-side aim-direction fallback when `targetId` is omitted.
#### Scenario: Valid `ShootInput` is accepted for the sending peer #### Scenario: Valid `ShootInput` is accepted for the sending peer
- **WHEN** a managed peer sends a well-formed `ShootInput` whose `playerId` maps to that sender, whose tick is newer than the sender's last accepted shoot tick, and whose `targetId` resolves to another living managed peer - **WHEN** a managed peer sends a well-formed `ShootInput` whose `playerId` maps to that sender, whose tick is newer than the sender's last accepted shoot tick, and whose target resolves to another living managed peer either from explicit `targetId` data or from server-side aim-direction fallback
- **THEN** the server accepts the request for that sender only - **THEN** the server accepts the request for that sender only
- **THEN** the sender's last accepted shoot tick is updated without changing other peers' combat bookkeeping - **THEN** the sender's last accepted shoot tick is updated without changing other peers' combat bookkeeping
#### Scenario: Invalid `ShootInput` is rejected without mutating authoritative combat state #### Scenario: Invalid `ShootInput` is rejected without mutating authoritative combat state
- **WHEN** a managed peer sends a `ShootInput` with malformed direction data, a stale tick, a mismatched `playerId`, or a target that is missing, self-targeted, or already dead - **WHEN** a managed peer sends a `ShootInput` with malformed direction data, a stale tick, a mismatched `playerId`, or targeting data that resolves to no living non-self managed peer
- **THEN** the server rejects that request - **THEN** the server rejects that request
- **THEN** no authoritative damage or death state is applied to any peer from that rejected request - **THEN** no authoritative damage or death state is applied to any peer from that rejected request
@ -21,7 +21,7 @@ The shared server networking path SHALL register `ShootInput` handling through t
The shared server networking path SHALL own final combat resolution for accepted shots, including hit acceptance, damage application, authoritative HP mutation, and death determination. Combat resolution MUST update the authoritative server-owned state of both the attacker and target as needed without delegating gameplay truth to client-side prediction or presentation code. The shared server networking path SHALL own final combat resolution for accepted shots, including hit acceptance, damage application, authoritative HP mutation, and death determination. Combat resolution MUST update the authoritative server-owned state of both the attacker and target as needed without delegating gameplay truth to client-side prediction or presentation code.
#### Scenario: Accepted shot applies damage to the authoritative target state #### Scenario: Accepted shot applies damage to the authoritative target state
- **WHEN** the server accepts a `ShootInput` that targets a living managed peer - **WHEN** the server accepts a `ShootInput` that resolves a living managed peer
- **THEN** it resolves the shot as an authoritative hit against that target - **THEN** it resolves the shot as an authoritative hit against that target
- **THEN** it reduces the target's authoritative HP according to the configured damage rule before later state snapshots are broadcast - **THEN** it reduces the target's authoritative HP according to the configured damage rule before later state snapshots are broadcast

View File

@ -5,7 +5,12 @@ Define the shared server-side movement authority contract that accepts `MoveInpu
## Requirements ## Requirements
### Requirement: Server registers and validates `MoveInput` per peer ### Requirement: Server registers and validates `MoveInput` per peer
The shared server networking path SHALL register `MoveInput` handling through the server host/runtime composition and SHALL validate inbound movement input against the sending peer before mutating authoritative state. Validation MUST reject stale ticks for that peer, malformed numeric values, and payloads that do not map to the sender's managed movement state. The shared server networking path SHALL register `MoveInput` handling through the server host/runtime composition and SHALL validate inbound movement input against the sending peer before mutating authoritative state. The server SHALL also bootstrap authoritative movement state for a managed peer once login succeeds so idle players can receive authoritative snapshots before any movement input is sent. Validation MUST reject stale ticks for that peer, malformed numeric values, and payloads whose player identity does not match the sender's managed movement state.
#### Scenario: Login success bootstraps idle authoritative movement state
- **WHEN** a managed peer completes login before sending any `MoveInput`
- **THEN** the server creates authoritative movement state for that peer with zero movement intent and default authoritative HP
- **THEN** later authority broadcasts can include that idle peer even before movement input arrives
#### Scenario: Accepted `MoveInput` updates the sender's authoritative movement intent #### Scenario: Accepted `MoveInput` updates the sender's authoritative movement intent
- **WHEN** a managed peer sends a well-formed `MoveInput` with a tick newer than the last accepted movement tick for that peer - **WHEN** a managed peer sends a well-formed `MoveInput` with a tick newer than the last accepted movement tick for that peer