using System;
using Gameplay;
using JetBrains.Annotations;
using Unity.Entities;
using Unity.NetCode;
using Unity.Collections;
using Unity.MegaCity.Gameplay;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Burst;
using Unity.MegaCity.UI;
using Unity.Networking.Transport;
using UnityEngine;
using static Unity.Entities.SystemAPI;
using Unity.Jobs;
using Unity.Cn.Multiverse;
namespace Unity.MegaCity.Traffic
{
///
/// The bootstrap needs to extend `ClientServerBootstrap`, there can only be one class extending it in the project
///
[UnityEngine.Scripting.Preserve]
public class NetCodeBootstrap : ClientServerBootstrap
{
public static NetworkEndpoint MegaCityServerIp => NetworkEndpoint.Parse("128.14.159.58", 7979);
///
/// Limitation imposed to ensure UTP send/receiveQueueSize's are set appropriately.
/// .
///
public const int MaxPlayerCount = 200;
// The initialize method is what entities calls to create the default worlds
public override bool Initialize(string defaultWorldName)
{
// Handle max player count globally.
NetworkStreamReceiveSystem.DriverConstructor = new MegaCityDriverConstructor();
#if UNITY_SERVER && !UNITY_EDITOR
UnityEngine.Application.targetFrameRate = 60;
CreateDefaultClientServerWorlds();
// Start Multiverse through the wrapper.
MultiverseSDKWrapper.Instance.StartMultiverse();
// Niki.Walker: Disabled as UNITY_SERVER does not support creating thin client worlds.
// // On the server, also create thin clients (if requested).
// TryCreateThinClientsIfRequested();
return true;
#else
// Try and auto-connect.
#if UNITY_EDITOR
if (RequestedPlayType == PlayType.Client)
{
if (MultiplayerPlayModePreferences.IsEditorInputtedAddressValidForConnect(out var editorSpecifiedEndpoint))
{
AutoConnectPort = editorSpecifiedEndpoint.Port;
DefaultConnectAddress = editorSpecifiedEndpoint;
UnityEngine.Debug.Log($"Detected auto-connection preference in 'Multiplayer PlayMode Tool' targeting '{editorSpecifiedEndpoint}' (Port: '{AutoConnectPort}')!");
}
}
#else
// We always set the DefaultConnectAddress in a player, because it's unlikely you'll want to test locally here.
DefaultConnectAddress = ModeBootstrap.Options.UserSpecifiedEndpoint;
if (TryCreateThinClientsIfRequested())
return true;
#endif
// Netcode worlds are always created, regardless.
CreateDefaultClientServerWorlds();
return true;
#endif
}
[UsedImplicitly]
private static bool TryCreateThinClientsIfRequested()
{
if (ModeBootstrap.Options.IsThinClient)
{
var requestedNumThinClients = ModeBootstrap.Options.TargetThinClientWorldCount;
if (requestedNumThinClients > 0)
{
// Hardcoded DefaultConnectAddress for the MegaCity demo.
AutoConnectPort = ModeBootstrap.Options.UserSpecifiedEndpoint.Port;
for (var i = 0; i < requestedNumThinClients; i++)
{
Debug.Log($"Creating a Thin Client World! {(i + 1)} of {requestedNumThinClients}...");
var world = CreateThinClientWorld();
if (i == 0 || World.DefaultGameObjectInjectionWorld == null || !World.DefaultGameObjectInjectionWorld.IsCreated)
{
World.DefaultGameObjectInjectionWorld = world;
Debug.Log($"Setting DefaultGameObjectInjectionWorld to world '{world.Name}'.");
}
}
Debug.Log($"Detected headless client! Automatically creating {requestedNumThinClients} ThinClients, and connecting them to the hardcoded endpoint '{DefaultConnectAddress}' (Port: '{AutoConnectPort}')!");
return true;
}
Debug.LogError($"Detected headless client, but TargetThinClientWorldCount is {requestedNumThinClients}! Cannot initialize!");
}
return false;
}
}
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(InitializationSystemGroup))]
[CreateAfter(typeof(NetworkStreamReceiveSystem))]
public partial struct ServerInGame : ISystem
{
#region Jobs
[BurstCompile]
private partial struct GetPositionJob : IJob
{
public NativeArray SpawnPoints;
public NativeList UsedPositions;
public Mathematics.Random Random;
[BurstCompile]
public void Execute()
{
var availablePositions = CreateAvailablePositions();
// If all player positions have been used, reset the list
if (availablePositions.Length == 0)
{
UsedPositions.Clear();
availablePositions = CreateAvailablePositions();
}
// Choose a random position from the list of available player names
var randomIndex = Random.NextInt(0, availablePositions.Length);
var position = availablePositions[randomIndex];
UsedPositions.Add(position);
}
private NativeList CreateAvailablePositions()
{
var availablePositions = new NativeList(Allocator.TempJob);
// Get a list of spawnPoints that have not been used
foreach (var position in SpawnPoints)
{
if (!UsedPositions.Contains(position.Value))
{
availablePositions.Add(position.Value);
}
}
return availablePositions;
}
}
[BurstCompile]
partial struct UpdateConnectionPositionSystemJob : IJobEntity
{
[ReadOnly] public ComponentLookup transformLookup;
public void Execute(ref GhostConnectionPosition conPos, in CommandTarget target)
{
if (!transformLookup.HasComponent(target.targetEntity))
return;
conPos = new GhostConnectionPosition
{
Position = transformLookup[target.targetEntity].Position
};
}
}
#endregion
private NativeList m_UsedPositions;
private Mathematics.Random m_Random;
private bool m_IsInitialized;
public void OnCreate(ref SystemState state)
{
m_IsInitialized = false;
state.RequireForUpdate();
state.RequireForUpdate();
m_UsedPositions = new NativeList(Allocator.Persistent);
var currentTime = DateTime.Now;
var seed = currentTime.Minute + currentTime.Second + currentTime.Millisecond + 1;
m_Random = new Mathematics.Random((uint)seed);
GetSingletonRW().ValueRW.Listen(NetworkEndpoint.AnyIpv4.WithPort(ModeBootstrap.Options.UserSpecifiedEndpoint.Port));
const int tileSize = 256;
var grid = state.EntityManager.CreateEntity();
state.EntityManager.SetName(grid, "GhostImportanceSingleton");
state.EntityManager.AddComponentData(grid, new GhostDistanceData
{
TileSize = new int3(tileSize, 1024 * 8, tileSize),
TileCenter = new int3(0, 0, 0),
TileBorderWidth = new float3(5f),
});
state.EntityManager.AddComponentData(grid, new GhostImportance
{
ScaleImportanceFunction = GhostDistanceImportance.ScaleFunctionPointer,
GhostConnectionComponentType = ComponentType.ReadOnly(),
GhostImportanceDataType = ComponentType.ReadOnly(),
GhostImportancePerChunkDataType = ComponentType.ReadOnly(),
});
}
public void OnUpdate(ref SystemState state)
{
#if UNITY_SERVER && !UNITY_EDITOR
if(!m_IsInitialized)
{
m_IsInitialized = true;
MultiverseSDKWrapper.Instance.ReadyMultiverse();
}
#endif
var spawnBuffer = GetSingletonBuffer();
var prefab = GetSingleton().Player;
var cmdBuffer = new EntityCommandBuffer(Allocator.Temp);
var originalTrans = state.EntityManager.GetComponentData(prefab);
var health = state.EntityManager.GetComponentData(prefab);
state.EntityManager.GetName(prefab, out var prefabName);
foreach (var (netId, entity) in Query>().WithNone()
.WithEntityAccess())
{
var findNewPosition = new GetPositionJob
{
SpawnPoints = spawnBuffer.ToNativeArray(Allocator.TempJob),
UsedPositions = m_UsedPositions,
Random = m_Random
};
state.Dependency = findNewPosition.Schedule(state.Dependency);
state.Dependency.Complete();
cmdBuffer.AddComponent(entity);
var player = cmdBuffer.Instantiate(prefab);
var networkIdValue = netId.ValueRO.Value;
cmdBuffer.SetComponent(player, new GhostOwner { NetworkId = networkIdValue });
var newTrans = originalTrans;
newTrans.Position = m_UsedPositions[m_UsedPositions.Length - 1];
cmdBuffer.SetComponent(player, newTrans);
cmdBuffer.AppendToBuffer(entity, new LinkedEntityGroup { Value = player });
cmdBuffer.SetComponent(player, health);
cmdBuffer.AddComponent(entity);
cmdBuffer.SetComponent(entity, new CommandTarget { targetEntity = player });
}
cmdBuffer.Playback(state.EntityManager);
var updateJob = new UpdateConnectionPositionSystemJob
{
transformLookup = GetComponentLookup(true)
};
state.Dependency = updateJob.ScheduleParallel(state.Dependency);
}
}
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
[UpdateInGroup(typeof(InitializationSystemGroup))]
public partial struct ClientInGame : ISystem
{
private bool m_HasRegisteredSmoothingAction;
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate();
var tickRate = NetworkTimeSystem.DefaultClientTickRate;
tickRate.MaxExtrapolationTimeSimTicks = 120;
tickRate.InterpolationTimeMS = 150;
state.EntityManager.CreateSingleton(tickRate);
// Niki.Walker: Client-side optimizations:
var ghostSendSystemData = new GhostSendSystemData
{
MinSendImportance = 2
};
// Don't frequently resend the same bot vehicles.
//ghostSendSystemData.FirstSendImportanceMultiplier = 100; // Significantly bias towards sending new ghosts.
// Disabled as it ruins the start of the game.
state.EntityManager.CreateSingleton(ghostSendSystemData);
}
public void OnUpdate(ref SystemState state)
{
if (!m_HasRegisteredSmoothingAction && TryGetSingletonRW(out var ghostPredictionSmoothing))
{
m_HasRegisteredSmoothingAction = true;
ghostPredictionSmoothing.ValueRW.RegisterSmoothingAction(state.EntityManager, MegaCitySmoothingAction.Action);
}
var cmdBuffer = new EntityCommandBuffer(Allocator.Temp);
foreach (var (netId, entity) in Query>().WithNone()
.WithEntityAccess())
{
cmdBuffer.AddComponent(entity);
}
cmdBuffer.Playback(state.EntityManager);
}
}
}