diff --git a/Source/Game/Audio/AudioManager.cs b/Source/Game/Audio/AudioManager.cs index e06e8dc..8b42e1f 100644 --- a/Source/Game/Audio/AudioManager.cs +++ b/Source/Game/Audio/AudioManager.cs @@ -6,251 +6,250 @@ using FlaxEngine; using Console = Game.Console; using Object = FlaxEngine.Object; -namespace Game -{ - [Flags] - public enum AudioFlags - { - None = 0, +namespace Game; - /// Avoid replacing the existing playing audio source in this channel. - ContinuePlayingExistingSource = 1 +[Flags] +public enum AudioFlags +{ + None = 0, + + /// Avoid replacing the existing playing audio source in this channel. + ContinuePlayingExistingSource = 1 +} + +public static class AudioManager +{ + private static readonly Random random = new Random(); + + private static readonly Dictionary cachedAudioInfos = new Dictionary(); + + private static readonly Dictionary actorAudioChannels = + new Dictionary(); + + public static void PlaySound(string soundName, Actor actor, Float3 position, float volume = 1f) + { + PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, Float2.One, Float2.Zero); } - public static class AudioManager + public static void PlaySound(string soundName, Actor actor, Float3 position, float volume, Float2 pitchRange) { - private static readonly Random random = new Random(); + PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, pitchRange, Float2.Zero); + } - private static readonly Dictionary cachedAudioInfos = new Dictionary(); + public static void PlaySound(string soundName, Actor actor, int channel, Float3 position, float volume = 1f) + { + PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, Float2.Zero); + } - private static readonly Dictionary actorAudioChannels = - new Dictionary(); + public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, + float volume = 1f) + { + PlaySoundInternal(soundName, actor, channel, flags, position, volume, Float2.One, Float2.Zero); + } - public static void PlaySound(string soundName, Actor actor, Float3 position, float volume = 1f) + public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, + float volume, Float2 pitchRange) + { + PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, Float2.Zero); + } + + public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, + Float3 position, float volume = 1f) + { + PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, delayRange); + } + + public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, + Float3 position, float volume, Float2 pitchRange) + { + PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange, delayRange); + } + + public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, + AudioFlags flags, Float3 position, float volume, Float2 pitchRange) + { + PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, delayRange); + } + + private static void PlaySoundInternal(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, + float volume, Float2 pitchRange, Float2 delayRange) + { + AudioInfo audio = GetSound(soundName); + if (audio.AudioClips.Length == 0) + return; + + channel = Math.Max(channel, 0); + ActorAudioChannels actorChannels = null; + if (channel > 0) { - PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, Float2.One, Float2.Zero); - } - - public static void PlaySound(string soundName, Actor actor, Float3 position, float volume, Float2 pitchRange) - { - PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, pitchRange, Float2.Zero); - } - - public static void PlaySound(string soundName, Actor actor, int channel, Float3 position, float volume = 1f) - { - PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, Float2.Zero); - } - - public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, - float volume = 1f) - { - PlaySoundInternal(soundName, actor, channel, flags, position, volume, Float2.One, Float2.Zero); - } - - public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, - float volume, Float2 pitchRange) - { - PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, Float2.Zero); - } - - public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, - Float3 position, float volume = 1f) - { - PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, delayRange); - } - - public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, - Float3 position, float volume, Float2 pitchRange) - { - PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange, delayRange); - } - - public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, - AudioFlags flags, Float3 position, float volume, Float2 pitchRange) - { - PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, delayRange); - } - - private static void PlaySoundInternal(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, - float volume, Float2 pitchRange, Float2 delayRange) - { - AudioInfo audio = GetSound(soundName); - if (audio.AudioClips.Length == 0) - return; - - channel = Math.Max(channel, 0); - ActorAudioChannels actorChannels = null; - if (channel > 0) + if (!actorAudioChannels.TryGetValue(actor, out actorChannels)) { - if (!actorAudioChannels.TryGetValue(actor, out actorChannels)) - { - actorChannels = new ActorAudioChannels(); - actorAudioChannels.Add(actor, actorChannels); - } - - if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) - actorChannels.channelSources.Add(channel, null); - - if (existingAudioSource && existingAudioSource != null && - existingAudioSource.State == AudioSource.States.Playing) - { - if (flags.HasFlag(AudioFlags.ContinuePlayingExistingSource)) - return; - - existingAudioSource.Stop(); - Object.Destroy(existingAudioSource); - actorChannels.channelSources[channel] = null; - } + actorChannels = new ActorAudioChannels(); + actorAudioChannels.Add(actor, actorChannels); } - AudioClip audioClip; - if (audio.AudioClips.Length > 1) - { - // Randomize selected clip while avoiding the last used clip - int randomIndex = 0; - for (int i = 0; i < 10; i++) - { - randomIndex = random.Next(audio.AudioClips.Length); - if (randomIndex != audio.lastAudioPlayed) - break; - } - - audioClip = audio.AudioClips[randomIndex]; - audio.lastAudioPlayed = randomIndex; - } - else - { - audioClip = audio.AudioClips[0]; - } - - // Randomized pitch - float pitch; - if (pitchRange[0] < pitchRange[1]) - pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0])); - else - pitch = pitchRange[0]; - - // Randomized start delay - float randomDelay; - if (delayRange[0] < delayRange[1]) - randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); - else - randomDelay = delayRange[0]; - - AudioSourceDelayed audioSource = new AudioSourceDelayed(); - audioSource.Delay = randomDelay; - audioSource.Clip = audioClip; - audioSource.Position = position; - audioSource.Parent = actor.Parent; - audioSource.Pitch = pitch; - audioSource.Name = Path.GetFileNameWithoutExtension(audioClip.Path); - audioSource.Volume = volume; - audioSource.PlayOnStart = randomDelay == 0; - - if (volume != 1f) - audioSource.Name += ", vol: " + volume; - if (pitch != 1f) - audioSource.Name += ", pitch: " + pitch; - if (pitch != 0f) - audioSource.Name += ", delay: " + randomDelay; - Console.Print("playing sound " + audioSource.Name); - - if (channel > 0) - actorChannels.channelSources[channel] = audioSource; - } - - public static void StopSound(Actor actor, int channel) - { - if (channel <= 0) - return; - - if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) - return; - if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) - return; + actorChannels.channelSources.Add(channel, null); if (existingAudioSource && existingAudioSource != null && existingAudioSource.State == AudioSource.States.Playing) { + if (flags.HasFlag(AudioFlags.ContinuePlayingExistingSource)) + return; + existingAudioSource.Stop(); Object.Destroy(existingAudioSource); actorChannels.channelSources[channel] = null; } } - public static bool IsSoundPlaying(Actor actor, int channel) + AudioClip audioClip; + if (audio.AudioClips.Length > 1) { - if (channel <= 0) - return false; + // Randomize selected clip while avoiding the last used clip + int randomIndex = 0; + for (int i = 0; i < 10; i++) + { + randomIndex = random.Next(audio.AudioClips.Length); + if (randomIndex != audio.lastAudioPlayed) + break; + } - if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) - return false; + audioClip = audio.AudioClips[randomIndex]; + audio.lastAudioPlayed = randomIndex; + } + else + { + audioClip = audio.AudioClips[0]; + } - if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) - return false; + // Randomized pitch + float pitch; + if (pitchRange[0] < pitchRange[1]) + pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0])); + else + pitch = pitchRange[0]; - if (existingAudioSource && existingAudioSource != null && - existingAudioSource.State == AudioSource.States.Playing) - return true; + // Randomized start delay + float randomDelay; + if (delayRange[0] < delayRange[1]) + randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); + else + randomDelay = delayRange[0]; + AudioSourceDelayed audioSource = new AudioSourceDelayed(); + audioSource.Delay = randomDelay; + audioSource.Clip = audioClip; + audioSource.Position = position; + audioSource.Parent = actor.Parent; + audioSource.Pitch = pitch; + audioSource.Name = Path.GetFileNameWithoutExtension(audioClip.Path); + audioSource.Volume = volume; + audioSource.PlayOnStart = randomDelay == 0; + + if (volume != 1f) + audioSource.Name += ", vol: " + volume; + if (pitch != 1f) + audioSource.Name += ", pitch: " + pitch; + if (pitch != 0f) + audioSource.Name += ", delay: " + randomDelay; + Console.Print("playing sound " + audioSource.Name); + + if (channel > 0) + actorChannels.channelSources[channel] = audioSource; + } + + public static void StopSound(Actor actor, int channel) + { + if (channel <= 0) + return; + + if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) + return; + + if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) + return; + + if (existingAudioSource && existingAudioSource != null && + existingAudioSource.State == AudioSource.States.Playing) + { + existingAudioSource.Stop(); + Object.Destroy(existingAudioSource); + actorChannels.channelSources[channel] = null; + } + } + + public static bool IsSoundPlaying(Actor actor, int channel) + { + if (channel <= 0) return false; - } - private static AudioInfo GetSound(string soundName) + if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) + return false; + + if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) + return false; + + if (existingAudioSource && existingAudioSource != null && + existingAudioSource.State == AudioSource.States.Playing) + return true; + + return false; + } + + private static AudioInfo GetSound(string soundName) + { + if (cachedAudioInfos.TryGetValue(soundName, out AudioInfo cachedAudio)) + return cachedAudio; + + AudioInfo audio = new AudioInfo(); + + string audioBasePath = Path.Combine(AssetManager.ContentPath, "Audio"); + AudioClip audioClip = Content.Load(Path.Combine(audioBasePath, soundName + ".flax")); + if (audioClip != null) { - if (cachedAudioInfos.TryGetValue(soundName, out AudioInfo cachedAudio)) - return cachedAudio; - - AudioInfo audio = new AudioInfo(); - - string audioBasePath = Path.Combine(AssetManager.ContentPath, "Audio"); - AudioClip audioClip = Content.Load(Path.Combine(audioBasePath, soundName + ".flax")); - if (audioClip != null) + audio.AudioClips = new[] { audioClip }; + } + else + { + // Check if this audio has multiple variations + var audioClips = new List(); + for (int i = 1; i < 50; i++) { - audio.AudioClips = new[] { audioClip }; + // TODO: make this more efficient, maybe get a list of assets and filter by name? + AudioClip audioClipVariation = + Content.Load(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax")); + if (audioClipVariation == null) + break; + + audioClips.Add(audioClipVariation); } + + if (audioClips.Count > 0) + audio.AudioClips = audioClips.ToArray(); else - { - // Check if this audio has multiple variations - var audioClips = new List(); - for (int i = 1; i < 50; i++) - { - // TODO: make this more efficient, maybe get a list of assets and filter by name? - AudioClip audioClipVariation = - Content.Load(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax")); - if (audioClipVariation == null) - break; - - audioClips.Add(audioClipVariation); - } - - if (audioClips.Count > 0) - audio.AudioClips = audioClips.ToArray(); - else - Console.PrintError("AudioClip '" + soundName + "' not found"); - } - - audio.lastAudioPlayed = audio.AudioClips.Length + 1; - - cachedAudioInfos.Add(soundName, audio); - return audio; + Console.PrintError("AudioClip '" + soundName + "' not found"); } - private class AudioInfo - { - public AudioClip[] AudioClips; - public int lastAudioPlayed; - } + audio.lastAudioPlayed = audio.AudioClips.Length + 1; - private class ActorAudioChannels - { - public readonly Dictionary channelSources; + cachedAudioInfos.Add(soundName, audio); + return audio; + } - public ActorAudioChannels() - { - channelSources = new Dictionary(); - } + private class AudioInfo + { + public AudioClip[] AudioClips; + public int lastAudioPlayed; + } + + private class ActorAudioChannels + { + public readonly Dictionary channelSources; + + public ActorAudioChannels() + { + channelSources = new Dictionary(); } } } \ No newline at end of file diff --git a/Source/Game/Audio/AudioSourceDelayed.cs b/Source/Game/Audio/AudioSourceDelayed.cs index 7246d78..a3ba5f4 100644 --- a/Source/Game/Audio/AudioSourceDelayed.cs +++ b/Source/Game/Audio/AudioSourceDelayed.cs @@ -5,43 +5,42 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Game -{ - public class AudioSourceDelayed : AudioSource - { - public float Delay = 0f; - public override void OnBeginPlay() - { - if (Delay >= 0f) - { - PlayOnStart = false; - var script = AddScript(); - script.PlayStartTime = Delay + FlaxEngine.Time.GameTime; - } +namespace Game; - base.OnBeginPlay(); +public class AudioSourceDelayed : AudioSource +{ + public float Delay = 0f; + public override void OnBeginPlay() + { + if (Delay >= 0f) + { + PlayOnStart = false; + var script = AddScript(); + script.PlayStartTime = Delay + FlaxEngine.Time.GameTime; } + base.OnBeginPlay(); } - public class AudioSourceDelayedScript : Script +} + +public class AudioSourceDelayedScript : Script +{ + [NoSerialize] + [HideInEditor] + public float PlayStartTime = 0f; + + public override void OnStart() { - [NoSerialize] - [HideInEditor] - public float PlayStartTime = 0f; + FlaxEngine.Object.Destroy(Actor, Actor.As().Clip.Length + Actor.As().Delay); + } - public override void OnStart() + public override void OnFixedUpdate() + { + if (FlaxEngine.Time.GameTime >= PlayStartTime) { - FlaxEngine.Object.Destroy(Actor, Actor.As().Clip.Length + Actor.As().Delay); - } - - public override void OnFixedUpdate() - { - if (FlaxEngine.Time.GameTime >= PlayStartTime) - { - PlayStartTime = float.MaxValue; - Actor.As().Play(); - } + PlayStartTime = float.MaxValue; + Actor.As().Play(); } } } diff --git a/Source/Game/Camera/CameraMovement.cs b/Source/Game/Camera/CameraMovement.cs index 3b5f70e..16f2fd5 100644 --- a/Source/Game/Camera/CameraMovement.cs +++ b/Source/Game/Camera/CameraMovement.cs @@ -1,99 +1,98 @@ using FlaxEngine; -namespace Game +namespace Game; + +public class CameraMovement : Script { - public class CameraMovement : Script + private float viewPitch; + private float viewRoll; + private float viewYaw; + + [Limit(0, 9000)] + [Tooltip("Camera speed")] + public float MoveSpeed { get; set; } = 400; + + public override void OnStart() { - private float viewPitch; - private float viewRoll; - private float viewYaw; + Float3 initialEulerAngles = Actor.Orientation.EulerAngles; + viewPitch = initialEulerAngles.X; + viewYaw = initialEulerAngles.Y; + viewRoll = initialEulerAngles.Z; + } - [Limit(0, 9000)] - [Tooltip("Camera speed")] - public float MoveSpeed { get; set; } = 400; + public override void OnUpdate() + { + Transform camTrans = Actor.Transform; + Actor rootActor = Actor.GetChild(0); + Camera camera = rootActor.GetChild(); - public override void OnStart() + float xAxis = InputManager.GetAxisRaw("Mouse X"); + float yAxis = InputManager.GetAxisRaw("Mouse Y"); + if (xAxis != 0.0f || yAxis != 0.0f) { - Float3 initialEulerAngles = Actor.Orientation.EulerAngles; - viewPitch = initialEulerAngles.X; - viewYaw = initialEulerAngles.Y; - viewRoll = initialEulerAngles.Z; + viewPitch += yAxis; + viewYaw += xAxis; + + viewPitch = Mathf.Clamp(viewPitch, -90.0f, 90.0f); + + // root orientation must be set first + rootActor.Orientation = Quaternion.Euler(0, viewYaw, 0); + camera.Orientation = Quaternion.Euler(viewPitch, viewYaw, viewRoll); } - public override void OnUpdate() + float inputH = InputManager.GetAxis("Horizontal"); + float inputV = InputManager.GetAxis("Vertical"); + Float3 move = new Float3(inputH, 0.0f, inputV); + + if (!move.IsZero) { - Transform camTrans = Actor.Transform; - Actor rootActor = Actor.GetChild(0); - Camera camera = rootActor.GetChild(); + move.Normalize(); + move = camera.Transform.TransformDirection(move) * MoveSpeed; - float xAxis = InputManager.GetAxisRaw("Mouse X"); - float yAxis = InputManager.GetAxisRaw("Mouse Y"); - if (xAxis != 0.0f || yAxis != 0.0f) { - viewPitch += yAxis; - viewYaw += xAxis; + Float3 delta = move * Time.UnscaledDeltaTime; + float movementLeft = delta.Length; - viewPitch = Mathf.Clamp(viewPitch, -90.0f, 90.0f); + // TODO: check multiple times in case we get stuck in walls - // root orientation must be set first - rootActor.Orientation = Quaternion.Euler(0, viewYaw, 0); - camera.Orientation = Quaternion.Euler(viewPitch, viewYaw, viewRoll); - } - - float inputH = InputManager.GetAxis("Horizontal"); - float inputV = InputManager.GetAxis("Vertical"); - Float3 move = new Float3(inputH, 0.0f, inputV); - - if (!move.IsZero) - { - move.Normalize(); - move = camera.Transform.TransformDirection(move) * MoveSpeed; + float sphereRadius = 10.0f; // TODO: use collider radius + RayCastHit[] hitInfos; + float moveDist = delta.Length; + Physics.SphereCastAll(Actor.Transform.Translation, sphereRadius, move.Normalized, out hitInfos, + moveDist); + //bool nohit = true; + float hitDistance = moveDist; + Float3 hitNormal = move.Normalized; + foreach (RayCastHit hitInfo in hitInfos) { - Float3 delta = move * Time.UnscaledDeltaTime; - float movementLeft = delta.Length; + if (hitInfo.Collider.Parent == Parent) + continue; - // TODO: check multiple times in case we get stuck in walls - - float sphereRadius = 10.0f; // TODO: use collider radius - RayCastHit[] hitInfos; - float moveDist = delta.Length; - Physics.SphereCastAll(Actor.Transform.Translation, sphereRadius, move.Normalized, out hitInfos, - moveDist); - - //bool nohit = true; - float hitDistance = moveDist; - Float3 hitNormal = move.Normalized; - foreach (RayCastHit hitInfo in hitInfos) + if (hitInfo.Distance < hitDistance) { - if (hitInfo.Collider.Parent == Parent) - continue; - - if (hitInfo.Distance < hitDistance) - { - hitDistance = hitInfo.Distance; - hitNormal = hitInfo.Normal; - } - //nohit = false; - //break; + hitDistance = hitInfo.Distance; + hitNormal = hitInfo.Normal; } - - if (hitDistance != moveDist) - //camTrans.Translation = Float3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance); - - //projected = normal * dot(direction, normal); - //direction = direction - projected - - //camTrans.Translation += hitNormal * (moveDist - hitDistance); // correct? - //camTrans.Translation = hitNormal * (move * hitNormal); // correct? - //camTrans.Translation = Actor.Transform.Translation; - delta += -Float3.Dot(delta, hitNormal) * hitNormal; // correct? - - camTrans.Translation += delta; + //nohit = false; + //break; } - } - Actor.Transform = camTrans; + if (hitDistance != moveDist) + //camTrans.Translation = Float3.Lerp(Actor.Transform.Translation, camTrans.Translation, hitDistance); + + //projected = normal * dot(direction, normal); + //direction = direction - projected + + //camTrans.Translation += hitNormal * (moveDist - hitDistance); // correct? + //camTrans.Translation = hitNormal * (move * hitNormal); // correct? + //camTrans.Translation = Actor.Transform.Translation; + delta += -Float3.Dot(delta, hitNormal) * hitNormal; // correct? + + camTrans.Translation += delta; + } } + + Actor.Transform = camTrans; } } \ No newline at end of file diff --git a/Source/Game/Camera/CameraRender.cs b/Source/Game/Camera/CameraRender.cs index 2fa2448..deb4b52 100644 --- a/Source/Game/Camera/CameraRender.cs +++ b/Source/Game/Camera/CameraRender.cs @@ -1,182 +1,181 @@ using FlaxEditor.Content.Settings; using FlaxEngine; -namespace Game +namespace Game; + +/// +/// CameraRender Script. +/// +[ExecuteInEditMode] +public class CameraRender : PostProcessEffect { - /// - /// CameraRender Script. - /// - [ExecuteInEditMode] - public class CameraRender : PostProcessEffect + public Camera camera; + + private bool lastEnabled; + public Material material; + + private MaterialInstance materialInstance; + private SceneRenderTask sceneTask; + private SceneRenderTask sceneTask2; + + private GPUTexture texture; + private GPUTexture texture2; + + //public override PostProcessEffectLocation Location => PostProcessEffectLocation.Default; + //public override int Order => 110; + //public override bool CanRender => camera.IsActive; + + private bool useMainCamera = true; + public bool rescaleModel = true; + + private Actor viewModelHolder; + + private void CreateTextures(int width, int height) { - public Camera camera; + GPUTextureDescription textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm); - private bool lastEnabled; - public Material material; + // Prepare texture and SceneRenderTask for viewmodel camera + if (texture == null) + texture = new GPUTexture(); + if (texture.Init(ref textureDesc)) + Console.Print("Failed to create camera texture"); - private MaterialInstance materialInstance; - private SceneRenderTask sceneTask; - private SceneRenderTask sceneTask2; + // Prepare depth texture and SceneRenderTask for viewmodel camera + textureDesc.Format = PixelFormat.R8_UNorm; + if (texture2 == null) + texture2 = new GPUTexture(); + if (texture2.Init(ref textureDesc)) + Console.Print("Failed to create camera depth texture"); + } - private GPUTexture texture; - private GPUTexture texture2; - - //public override PostProcessEffectLocation Location => PostProcessEffectLocation.Default; - //public override int Order => 110; - //public override bool CanRender => camera.IsActive; - - private bool useMainCamera = true; - public bool rescaleModel = true; - - private Actor viewModelHolder; - - private void CreateTextures(int width, int height) + public override void OnAwake() + { + viewModelHolder = Parent.Parent.Parent.Parent.GetChild("ViewModelHolder"); + if (useMainCamera) { - GPUTextureDescription textureDesc = GPUTextureDescription.New2D(width, height, PixelFormat.R8G8B8A8_UNorm); - - // Prepare texture and SceneRenderTask for viewmodel camera - if (texture == null) - texture = new GPUTexture(); - if (texture.Init(ref textureDesc)) - Console.Print("Failed to create camera texture"); - - // Prepare depth texture and SceneRenderTask for viewmodel camera - textureDesc.Format = PixelFormat.R8_UNorm; - if (texture2 == null) - texture2 = new GPUTexture(); - if (texture2.Init(ref textureDesc)) - Console.Print("Failed to create camera depth texture"); - } - - public override void OnAwake() - { - viewModelHolder = Parent.Parent.Parent.Parent.GetChild("ViewModelHolder"); - if (useMainCamera) + camera.IsActive = false; + void foo(Actor actor) { - camera.IsActive = false; - void foo(Actor actor) - { - actor.Layer = 0; - foreach (Actor actChild in actor.GetChildren()) - foo(actChild); - } - - foo(viewModelHolder); + actor.Layer = 0; + foreach (Actor actChild in actor.GetChildren()) + foo(actChild); } - if (!camera.IsActive) - return; - - CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height); - - // Color pass - sceneTask = new SceneRenderTask(); - sceneTask.Order = -1; - sceneTask.Camera = camera; - sceneTask.ViewMode = ViewMode.Default; - sceneTask.Output = texture; - sceneTask.ViewFlags = ViewFlags.DefaultGame; - sceneTask.Enabled = true; - sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage; - - // Depth pass - sceneTask2 = new SceneRenderTask(); - sceneTask2.Order = -2; - sceneTask2.Camera = camera; - sceneTask2.ViewMode = ViewMode.Depth; - sceneTask2.Output = texture2; - sceneTask2.ViewFlags = ViewFlags.None; - sceneTask2.Enabled = true; - sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage; - - // Setup material instance and parameters - if (materialInstance == null) - materialInstance = material.CreateVirtualInstance(); - materialInstance.SetParameterValue("Input", texture); - materialInstance.SetParameterValue("Depth", texture2); - - materialInstance.SetParameterValue("New parameter", true); - materialInstance.SetParameterValue("New parameter 0", ChannelMask.Blue); - materialInstance.SetParameterValue("New parameter 1", new Color(0.67f)); - - materialInstance.SetParameterValue("New parameter 3", 123f); - materialInstance.SetParameterValue("New parameter 4", new Float2(1,2)); - materialInstance.SetParameterValue("New parameter 5", new Float3(1,2,3)); - materialInstance.SetParameterValue("New parameter 6", new Float4(1,2,3,4)); - - materialInstance.SetParameterValue("New parameter 8", 123); - materialInstance.SetParameterValue("New parameter 9", new Matrix(0.666f)); - - materialInstance.SetParameterValue("New parameter 11", new Quaternion(0.5f, 0.5f, 0.5f, 0.5f)); - - materialInstance.SetParameterValue("New parameter 13", new Vector2(1,2)); - materialInstance.SetParameterValue("New parameter 14", new Vector3(1,2,3)); - materialInstance.SetParameterValue("New parameter 15", new Vector4(1,2,3,4)); - //materialInstance.SetParameterValue("New parameter 16", new Transform(new Vector3(1,2,3), new Quaternion(0.5f, 0.5f, 0.5f, 0.5f))); - - lastEnabled = true; + foo(viewModelHolder); } - public override void OnDestroy() - { - Destroy(ref sceneTask); - Destroy(ref sceneTask2); - Destroy(ref texture); - Destroy(ref texture2); - } + if (!camera.IsActive) + return; - public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, - GPUTexture output) - { - if (texture == null || texture2 == null) - return; + CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height); - Renderer.DrawPostFxMaterial(context, ref renderContext, materialInstance, output, input.View()); - } + // Color pass + sceneTask = new SceneRenderTask(); + sceneTask.Order = -1; + sceneTask.Camera = camera; + sceneTask.ViewMode = ViewMode.Default; + sceneTask.Output = texture; + sceneTask.ViewFlags = ViewFlags.DefaultGame; + sceneTask.Enabled = true; + sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage; - private bool lastRescale = false; - public override void OnUpdate() - { + // Depth pass + sceneTask2 = new SceneRenderTask(); + sceneTask2.Order = -2; + sceneTask2.Camera = camera; + sceneTask2.ViewMode = ViewMode.Depth; + sceneTask2.Output = texture2; + sceneTask2.ViewFlags = ViewFlags.None; + sceneTask2.Enabled = true; + sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage; + + // Setup material instance and parameters + if (materialInstance == null) + materialInstance = material.CreateVirtualInstance(); + materialInstance.SetParameterValue("Input", texture); + materialInstance.SetParameterValue("Depth", texture2); + + materialInstance.SetParameterValue("New parameter", true); + materialInstance.SetParameterValue("New parameter 0", ChannelMask.Blue); + materialInstance.SetParameterValue("New parameter 1", new Color(0.67f)); + + materialInstance.SetParameterValue("New parameter 3", 123f); + materialInstance.SetParameterValue("New parameter 4", new Float2(1,2)); + materialInstance.SetParameterValue("New parameter 5", new Float3(1,2,3)); + materialInstance.SetParameterValue("New parameter 6", new Float4(1,2,3,4)); + + materialInstance.SetParameterValue("New parameter 8", 123); + materialInstance.SetParameterValue("New parameter 9", new Matrix(0.666f)); + + materialInstance.SetParameterValue("New parameter 11", new Quaternion(0.5f, 0.5f, 0.5f, 0.5f)); + + materialInstance.SetParameterValue("New parameter 13", new Vector2(1,2)); + materialInstance.SetParameterValue("New parameter 14", new Vector3(1,2,3)); + materialInstance.SetParameterValue("New parameter 15", new Vector4(1,2,3,4)); + //materialInstance.SetParameterValue("New parameter 16", new Transform(new Vector3(1,2,3), new Quaternion(0.5f, 0.5f, 0.5f, 0.5f))); + + lastEnabled = true; + } + + public override void OnDestroy() + { + Destroy(ref sceneTask); + Destroy(ref sceneTask2); + Destroy(ref texture); + Destroy(ref texture2); + } + + public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, + GPUTexture output) + { + if (texture == null || texture2 == null) + return; + + Renderer.DrawPostFxMaterial(context, ref renderContext, materialInstance, output, input.View()); + } + + private bool lastRescale = false; + public override void OnUpdate() + { #if FLAX_EDITOR - if (Input.GetKeyDown(KeyboardKeys.F7)) - { - PhysicsSettings physicsSettings = GameSettings.Load(); - physicsSettings.EnableSubstepping = !physicsSettings.EnableSubstepping; - GameSettings.Save(physicsSettings); - //GameSettings.Apply(); - } + if (Input.GetKeyDown(KeyboardKeys.F7)) + { + PhysicsSettings physicsSettings = GameSettings.Load(); + physicsSettings.EnableSubstepping = !physicsSettings.EnableSubstepping; + GameSettings.Save(physicsSettings); + //GameSettings.Apply(); + } #endif - if (lastEnabled != camera.IsActive) - { - lastEnabled = camera.IsActive; - sceneTask.Enabled = lastEnabled; - sceneTask2.Enabled = lastEnabled; - sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f; - sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f; - } - - if (useMainCamera && rescaleModel != lastRescale) - { - lastRescale = rescaleModel; - if (rescaleModel) - viewModelHolder.Scale = new Float3(0.75f); - else - viewModelHolder.Scale = new Float3(1.0f); - } - - if (!camera.IsActive) - return; - if (texture == null) - OnAwake(); - - if ((int)camera.Viewport.Width != texture.Width || (int)camera.Viewport.Height != texture.Height) - CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height); - } - - public override void OnEnable() + if (lastEnabled != camera.IsActive) { - //OnAwake(); + lastEnabled = camera.IsActive; + sceneTask.Enabled = lastEnabled; + sceneTask2.Enabled = lastEnabled; + sceneTask.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f; + sceneTask2.RenderingPercentage = MainRenderTask.Instance.RenderingPercentage * 0.5f; } + + if (useMainCamera && rescaleModel != lastRescale) + { + lastRescale = rescaleModel; + if (rescaleModel) + viewModelHolder.Scale = new Float3(0.75f); + else + viewModelHolder.Scale = new Float3(1.0f); + } + + if (!camera.IsActive) + return; + if (texture == null) + OnAwake(); + + if ((int)camera.Viewport.Width != texture.Width || (int)camera.Viewport.Height != texture.Height) + CreateTextures((int)camera.Viewport.Width, (int)camera.Viewport.Height); + } + + public override void OnEnable() + { + //OnAwake(); } } \ No newline at end of file diff --git a/Source/Game/Camera/CameraSpring.cs b/Source/Game/Camera/CameraSpring.cs index 4953c5e..1289a92 100644 --- a/Source/Game/Camera/CameraSpring.cs +++ b/Source/Game/Camera/CameraSpring.cs @@ -1,75 +1,74 @@ using FlaxEngine; -namespace Game +namespace Game; + +public class CameraSpring : Script { - public class CameraSpring : Script + private bool lastGround; + private Float3 lastPosition; + public float percY; + + private Actor playerActor; + private PlayerMovement playerMovement; + + public float speed = 240f; + private Float3 targetOffset; + private Actor viewModelHolder; + + public override void OnStart() { - private bool lastGround; - private Float3 lastPosition; - public float percY; + playerActor = Actor.Parent.Parent; + playerMovement = playerActor.GetScript(); + viewModelHolder = playerActor.GetChild("ViewModelHolder"); - private Actor playerActor; - private PlayerMovement playerMovement; + lastGround = playerMovement.OnGround; + targetOffset = Actor.LocalPosition; + } - public float speed = 240f; - private Float3 targetOffset; - private Actor viewModelHolder; + private void UpdatePosition(Float3 position) + { + Actor.Position = position; + viewModelHolder.Position = position; + } - public override void OnStart() + public override void OnUpdate() + { + Float3 position = Actor.Parent.Position + targetOffset; + Float3 targetPosition = position; + + if (playerMovement.OnGround) { - playerActor = Actor.Parent.Parent; - playerMovement = playerActor.GetScript(); - viewModelHolder = playerActor.GetChild("ViewModelHolder"); - - lastGround = playerMovement.OnGround; - targetOffset = Actor.LocalPosition; - } - - private void UpdatePosition(Float3 position) - { - Actor.Position = position; - viewModelHolder.Position = position; - } - - public override void OnUpdate() - { - Float3 position = Actor.Parent.Position + targetOffset; - Float3 targetPosition = position; - - if (playerMovement.OnGround) + float deltaY = position.Y - lastPosition.Y; + //if (Mathf.Abs(deltaY) < 10f) + if (deltaY > 0) { - float deltaY = position.Y - lastPosition.Y; - //if (Mathf.Abs(deltaY) < 10f) - if (deltaY > 0) + if (deltaY > 100f) { - if (deltaY > 100f) - { - // Teleported, snap instantly - UpdatePosition(position); - } - else - { - const float catchUpDistance = 10f; - const float catchUpMinMultip = 0.25f; - percY = Mathf.Abs(deltaY) / catchUpDistance; - percY = Mathf.Min(1.0f, percY + catchUpMinMultip); - percY *= percY; + // Teleported, snap instantly + UpdatePosition(position); + } + else + { + const float catchUpDistance = 10f; + const float catchUpMinMultip = 0.25f; + percY = Mathf.Abs(deltaY) / catchUpDistance; + percY = Mathf.Min(1.0f, percY + catchUpMinMultip); + percY *= percY; - float adjustSpeed = speed * Time.DeltaTime * percY; + float adjustSpeed = speed * Time.DeltaTime * percY; - position.Y = lastPosition.Y; //-= deltaY; - position.Y = Mathf.MoveTowards(position.Y, targetPosition.Y, adjustSpeed); - UpdatePosition(position); - } + position.Y = lastPosition.Y; //-= deltaY; + position.Y = Mathf.MoveTowards(position.Y, targetPosition.Y, adjustSpeed); + UpdatePosition(position); } } - else - { - UpdatePosition(position); - } - - lastPosition = position; - lastGround = playerMovement.OnGround; } + else + { + UpdatePosition(position); + } + + lastPosition = position; + lastGround = playerMovement.OnGround; } } \ No newline at end of file diff --git a/Source/Game/Camera/WeaponSway.cs b/Source/Game/Camera/WeaponSway.cs index c41cc24..7b8c814 100644 --- a/Source/Game/Camera/WeaponSway.cs +++ b/Source/Game/Camera/WeaponSway.cs @@ -1,83 +1,82 @@ using System; using FlaxEngine; -namespace Game +namespace Game; + +public class WeaponSway : Script { - public class WeaponSway : Script + private Actor cameraHolder; + + private Actor rootActor; + public float swaySpeed = 3000f; + + private float timeRemainder; + + public override void OnStart() { - private Actor cameraHolder; + rootActor = Actor.Parent.GetChild("RootActor"); + cameraHolder = rootActor.GetChild("CameraHolder"); + Actor.LocalOrientation = GetRotation(); + } - private Actor rootActor; - public float swaySpeed = 3000f; + private Quaternion GetRotation() + { + Quaternion pitch = cameraHolder.LocalOrientation; + Quaternion yawRoll = rootActor.LocalOrientation; + return yawRoll * pitch; + } - private float timeRemainder; + public override void OnLateUpdate() + { + Quaternion rotation = GetRotation(); - public override void OnStart() + Float3 targetAngles = rotation.EulerAngles; + Float3 angles = Actor.LocalOrientation.EulerAngles; + + // Ensure the swaying is smooth when framerate fluctuates slightly + float remaining = Time.DeltaTime + timeRemainder; + const float minTime = 1f / 120f; + do { - rootActor = Actor.Parent.GetChild("RootActor"); - cameraHolder = rootActor.GetChild("CameraHolder"); - Actor.LocalOrientation = GetRotation(); - } + float stepTime = Mathf.Min(Time.DeltaTime, minTime); + remaining -= stepTime; + float swaySpeedScaled = swaySpeed * stepTime; - private Quaternion GetRotation() - { - Quaternion pitch = cameraHolder.LocalOrientation; - Quaternion yawRoll = rootActor.LocalOrientation; - return yawRoll * pitch; - } + float deltaX = Mathf.DeltaAngle(angles.X, targetAngles.X); + float deltaY = Mathf.DeltaAngle(angles.Y, targetAngles.Y); + float deltaZ = Mathf.DeltaAngle(angles.Z, targetAngles.Z); - public override void OnLateUpdate() - { - Quaternion rotation = GetRotation(); + const float maxAngle = 30f; + if (deltaX > maxAngle) + angles.X -= maxAngle - deltaX; + else if (deltaX < -maxAngle) + angles.X += maxAngle + deltaX; + if (deltaY > maxAngle) + angles.Y -= maxAngle - deltaY; + else if (deltaY < -maxAngle) + angles.Y += maxAngle + deltaY; + if (deltaZ > maxAngle) + angles.Z -= maxAngle - deltaZ; + else if (deltaZ < -maxAngle) + angles.Z += maxAngle + deltaZ; - Float3 targetAngles = rotation.EulerAngles; - Float3 angles = Actor.LocalOrientation.EulerAngles; + float percX = Mathf.Abs(deltaX) / maxAngle; + float percY = Mathf.Abs(deltaY) / maxAngle; + float percZ = Mathf.Abs(deltaZ) / maxAngle; + float minSpeed = swaySpeedScaled * 0.00001f * 0f; - // Ensure the swaying is smooth when framerate fluctuates slightly - float remaining = Time.DeltaTime + timeRemainder; - const float minTime = 1f / 120f; - do - { - float stepTime = Mathf.Min(Time.DeltaTime, minTime); - remaining -= stepTime; - float swaySpeedScaled = swaySpeed * stepTime; + Func fun = f => Mathf.Pow(f, 1.3f); - float deltaX = Mathf.DeltaAngle(angles.X, targetAngles.X); - float deltaY = Mathf.DeltaAngle(angles.Y, targetAngles.Y); - float deltaZ = Mathf.DeltaAngle(angles.Z, targetAngles.Z); + angles.X = Mathf.MoveTowardsAngle(angles.X, targetAngles.X, + Math.Max(swaySpeedScaled * fun(percX), minSpeed)); + angles.Y = Mathf.MoveTowardsAngle(angles.Y, targetAngles.Y, + Math.Max(swaySpeedScaled * fun(percY), minSpeed)); + angles.Z = Mathf.MoveTowardsAngle(angles.Z, targetAngles.Z, + Math.Max(swaySpeedScaled * fun(percZ), minSpeed)); + } while (remaining > minTime); - const float maxAngle = 30f; - if (deltaX > maxAngle) - angles.X -= maxAngle - deltaX; - else if (deltaX < -maxAngle) - angles.X += maxAngle + deltaX; - if (deltaY > maxAngle) - angles.Y -= maxAngle - deltaY; - else if (deltaY < -maxAngle) - angles.Y += maxAngle + deltaY; - if (deltaZ > maxAngle) - angles.Z -= maxAngle - deltaZ; - else if (deltaZ < -maxAngle) - angles.Z += maxAngle + deltaZ; + timeRemainder -= remaining; - float percX = Mathf.Abs(deltaX) / maxAngle; - float percY = Mathf.Abs(deltaY) / maxAngle; - float percZ = Mathf.Abs(deltaZ) / maxAngle; - float minSpeed = swaySpeedScaled * 0.00001f * 0f; - - Func fun = f => Mathf.Pow(f, 1.3f); - - angles.X = Mathf.MoveTowardsAngle(angles.X, targetAngles.X, - Math.Max(swaySpeedScaled * fun(percX), minSpeed)); - angles.Y = Mathf.MoveTowardsAngle(angles.Y, targetAngles.Y, - Math.Max(swaySpeedScaled * fun(percY), minSpeed)); - angles.Z = Mathf.MoveTowardsAngle(angles.Z, targetAngles.Z, - Math.Max(swaySpeedScaled * fun(percZ), minSpeed)); - } while (remaining > minTime); - - timeRemainder -= remaining; - - Actor.LocalOrientation = Quaternion.Euler(angles); - } + Actor.LocalOrientation = Quaternion.Euler(angles); } } \ No newline at end of file diff --git a/Source/Game/Console/CommonCommands.cs b/Source/Game/Console/CommonCommands.cs index 7c72cf6..ca666d1 100644 --- a/Source/Game/Console/CommonCommands.cs +++ b/Source/Game/Console/CommonCommands.cs @@ -1,42 +1,41 @@ using System; -namespace Game +namespace Game; + +// Holds common miscellaneous Console variables and commands +public static class CommonCommands { - // Holds common miscellaneous Console variables and commands - public static class CommonCommands + [ConsoleVariable("developer")] + public static string Developer { - [ConsoleVariable("developer")] - public static string Developer + get => Console.DebugVerbosity.ToString(); + set { - get => Console.DebugVerbosity.ToString(); - set - { - if (int.TryParse(value, out int intValue) && intValue >= 0) - Console.DebugVerbosity = intValue; - } - } - - [ConsoleCommand("")] - public static void NullCommand() - { - } - - [ConsoleCommand("echo")] - public static void EchoCommand() - { - Console.Print("nothing"); - } - - [ConsoleCommand("echo")] - public static void EchoCommand(string[] text) - { - Console.Print(string.Join(" ", text)); - } - - [ConsoleCommand("debugthrow")] - public static void DebugThrowCommand(string[] text) - { - throw new Exception(string.Join(" ", text)); + if (int.TryParse(value, out int intValue) && intValue >= 0) + Console.DebugVerbosity = intValue; } } + + [ConsoleCommand("")] + public static void NullCommand() + { + } + + [ConsoleCommand("echo")] + public static void EchoCommand() + { + Console.Print("nothing"); + } + + [ConsoleCommand("echo")] + public static void EchoCommand(string[] text) + { + Console.Print(string.Join(" ", text)); + } + + [ConsoleCommand("debugthrow")] + public static void DebugThrowCommand(string[] text) + { + throw new Exception(string.Join(" ", text)); + } } \ No newline at end of file diff --git a/Source/Game/Console/Config.cs b/Source/Game/Console/Config.cs index ff89e4b..45a5ec2 100644 --- a/Source/Game/Console/Config.cs +++ b/Source/Game/Console/Config.cs @@ -1,34 +1,33 @@ using System.Collections.Generic; -namespace Game +namespace Game; + +public class Config { - public class Config + private Dictionary dictionary = new Dictionary(); + + public Config() { - private Dictionary dictionary = new Dictionary(); + } - public Config() - { - } + public string this[string key] + { + get => dictionary[key]; + set => dictionary[key] = value; + } - public string this[string key] - { - get => dictionary[key]; - set => dictionary[key] = value; - } + // This is for debugging only, remove this later + public string[] Commands; - // This is for debugging only, remove this later - public string[] Commands; + public string[] GetLines() + { + string[] lines = new string[dictionary.Count + Commands.Length]; + int lineIndex = 0; + foreach (var kvp in dictionary) + lines[lineIndex++] = $"{kvp.Key} {kvp.Value}"; + foreach (var cmd in Commands) + lines[lineIndex++] = cmd; - public string[] GetLines() - { - string[] lines = new string[dictionary.Count + Commands.Length]; - int lineIndex = 0; - foreach (var kvp in dictionary) - lines[lineIndex++] = $"{kvp.Key} {kvp.Value}"; - foreach (var cmd in Commands) - lines[lineIndex++] = cmd; - - return lines; - } + return lines; } } \ No newline at end of file diff --git a/Source/Game/Console/ConfigParser.cs b/Source/Game/Console/ConfigParser.cs index d7d6440..cee2f2f 100644 --- a/Source/Game/Console/ConfigParser.cs +++ b/Source/Game/Console/ConfigParser.cs @@ -1,51 +1,50 @@ using System.Collections.Generic; using System.IO; -namespace Game +namespace Game; + +public class ConfigParser { - public class ConfigParser + private ConfigParser() { - private ConfigParser() + + } + + public static Config ParseFile(string path) + { + Config config = new Config(); + if (!File.Exists(path)) { - - } - - public static Config ParseFile(string path) - { - Config config = new Config(); - if (!File.Exists(path)) - { - Console.Print($"Config file not found in path: {path}"); - return config; - } - /*using*/ FileStream file = File.OpenRead(path); - /*using*/ StreamReader sr = new StreamReader(file); - - List commands = new List(); - string line; - while ((line = sr.ReadLine()?.Trim()) != null) - { - if (line.StartsWith(@"//")) - continue; - - int spacePos = line.IndexOf(' '); - if (spacePos == -1) - { - commands.Add(line); - continue; - } - - string key = line.Substring(0, spacePos); - string value = line.Substring(spacePos+1); - - value = value.Trim('"'); - - config[key] = value; - } - - config.Commands = commands.ToArray(); - + Console.Print($"Config file not found in path: {path}"); return config; } + /*using*/ FileStream file = File.OpenRead(path); + /*using*/ StreamReader sr = new StreamReader(file); + + List commands = new List(); + string line; + while ((line = sr.ReadLine()?.Trim()) != null) + { + if (line.StartsWith(@"//")) + continue; + + int spacePos = line.IndexOf(' '); + if (spacePos == -1) + { + commands.Add(line); + continue; + } + + string key = line.Substring(0, spacePos); + string value = line.Substring(spacePos+1); + + value = value.Trim('"'); + + config[key] = value; + } + + config.Commands = commands.ToArray(); + + return config; } } \ No newline at end of file diff --git a/Source/Game/Console/Console.cs b/Source/Game/Console/Console.cs index d6feb16..0cec629 100644 --- a/Source/Game/Console/Console.cs +++ b/Source/Game/Console/Console.cs @@ -8,557 +8,556 @@ using System.Runtime.InteropServices; using FlaxEditor; using FlaxEngine; -namespace Game +namespace Game; + +public struct ConsoleLine { - public struct ConsoleLine + public string content; + + internal ConsoleLine(string line) { - public string content; + content = line; + } - internal ConsoleLine(string line) - { - content = line; - } + public static implicit operator string(ConsoleLine line) + { + return line.content; + } - public static implicit operator string(ConsoleLine line) - { - return line.content; - } + public static explicit operator ConsoleLine(string line) + { + return new ConsoleLine(line); + } - public static explicit operator ConsoleLine(string line) - { - return new ConsoleLine(line); - } + public override string ToString() + { + return content; + } +} - public override string ToString() +public static class Console +{ + private static ConsoleInstance instance; + + // Returns if Console window open right now. + public static bool IsOpen => instance.IsOpen; + + // For debugging only: Returns true when Console was not closed during the same frame. + // Needed when Escape-key both closes the console and exits the game. + public static bool IsSafeToQuit => instance.IsSafeToQuit; + + // Called when Console is opened. + public static Action OnOpen + { + get => instance.OnOpen; + set => instance.OnOpen = value; + } + + // Called when Console is closed. + public static Action OnClose + { + get => instance.OnClose; + set => instance.OnClose = value; + } + + // Called when a line of text was printed in Console. + public static Action OnPrint + { + get => instance.OnPrint; + set => instance.OnPrint = value; + } + + public static bool ShowExecutedLines => instance.ShowExecutedLines; + + public static int DebugVerbosity + { + get => instance.DebugVerbosity; + set => instance.DebugVerbosity = value; + } + + public static string LinePrefix => instance.LinePrefix; + + public static ReadOnlySpan Lines => instance.Lines; + + + public static void Init() + { + if (instance != null) + return; + + Destroy(); + instance = new ConsoleInstance(); + instance.InitConsoleSubsystems(); + +#if FLAX_EDITOR + ScriptsBuilder.ScriptsReload += Destroy; + Editor.Instance.PlayModeEnd += OnEditorPlayModeChanged; +#endif + } + + public static void Destroy() + { + if (instance != null) { - return content; + instance.Dispose(); + instance = null; + +#if FLAX_EDITOR + ScriptsBuilder.ScriptsReload -= Destroy; + Editor.Instance.PlayModeEnd -= OnEditorPlayModeChanged; +#endif } } - public static class Console +#if FLAX_EDITOR + private static void OnEditorPlayModeChanged() { - private static ConsoleInstance instance; + //AssetManager.Globals.ResetValues(); + // Clear console buffer when leaving play mode + if (instance != null) + Clear(); + } +#endif - // Returns if Console window open right now. - public static bool IsOpen => instance.IsOpen; + public static string GetBufferHistory(int index) + { + return instance.GetBufferHistory(index); + } - // For debugging only: Returns true when Console was not closed during the same frame. - // Needed when Escape-key both closes the console and exits the game. - public static bool IsSafeToQuit => instance.IsSafeToQuit; + // Echoes text to Console + public static void Print(string text) + { + instance.Print(text); + } - // Called when Console is opened. - public static Action OnOpen + // Echoes warning text to Console + public static void PrintWarning(string text) + { + instance.PrintWarning(text); + } + + // Echoes error text to Console + [DebuggerHidden] + public static void PrintError(string text) + { + instance.PrintError(text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(string text) + { + instance.PrintDebug(1, false, text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(int verbosity, string text) + { + instance.PrintDebug(verbosity, false, text); + } + + // Echoes developer/debug text to Console + public static void PrintDebug(int verbosity, bool noRepeat, string text) + { + instance.PrintDebug(verbosity, noRepeat, text); + } + + // Opens the Console + public static void Open() + { + instance.Open(); + } + + // Closes the Console + public static void Close() + { + instance.Close(); + } + + // Clears the content of the Console + public static void Clear() + { + instance.Clear(); + } + + public static void Execute(string str, bool bufferInput = false, bool noOutput = false) + { + instance.Execute(str, bufferInput, noOutput); + } + + public static string GetVariable(string variableName) + { + return instance.GetVariable(variableName); + } +} + +public class ConsoleInstance : IDisposable +{ + private readonly List consoleBufferHistory = new List(); + private readonly Dictionary consoleCommands = new Dictionary(); + + //private static List consoleLines = new List(); + private readonly List consoleLines = new List(); + + private readonly Dictionary consoleVariables = + new Dictionary(); + + private readonly Stopwatch stopwatch = Stopwatch.StartNew(); + + // Echoes developer/debug text to Console + private string debugLastLine = ""; + public Action OnClose; + + public Action OnOpen; + public Action OnPrint; + + public bool ShowExecutedLines = true; + + private StreamWriter logStream; + + internal ConsoleInstance() + { + // Try different filename when running multiple instances + string logFilename = "console.log"; + const int attempts = 16; + for (int i = 0; i < attempts;) { - get => instance.OnOpen; - set => instance.OnOpen = value; + try + { +#if FLAX_EDITOR + logStream = new StreamWriter(Path.Combine(@"C:\dev\GoakeFlax", logFilename), false); +#else + logStream = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), logFilename), false); +#endif + } + catch (Exception) + { + logFilename = $"console-{++i}.log"; + continue; + } + break; } + } - // Called when Console is closed. - public static Action OnClose + public bool IsOpen { get; internal set; } = true; + + public bool IsSafeToQuit => stopwatch.Elapsed.TotalSeconds > 0.1; + + public int DebugVerbosity { get; set; } = 1; + public string LinePrefix { get; internal set; } = "]"; + + public ReadOnlySpan Lines => CollectionsMarshal.AsSpan(consoleLines); + + public void Dispose() + { + if (logStream != null) { - get => instance.OnClose; - set => instance.OnClose = value; + logStream.Flush(); + logStream.Dispose(); + logStream = null; } + } - // Called when a line of text was printed in Console. - public static Action OnPrint + // Initializes the Console system. + internal void InitConsoleSubsystems() + { + //AppDomain currentDomain = AppDomain.CurrentDomain; +#if USE_NETCORE + var assemblies = Utils.GetAssemblies(); +#else + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); +#endif + + foreach (Assembly assembly in assemblies) { - get => instance.OnPrint; - set => instance.OnPrint = value; + // Skip common assemblies + string assemblyName = assembly.GetName().Name; + if (assemblyName == "System" || + assemblyName.StartsWith("System.") || + assemblyName.StartsWith("Mono.") || + assemblyName == "mscorlib" || + assemblyName == "Newtonsoft.Json" || + assemblyName.StartsWith("FlaxEngine.") || + assemblyName.StartsWith("JetBrains.") || + assemblyName.StartsWith("Microsoft.") || + assemblyName.StartsWith("nunit.")) + continue; + + foreach (Type type in assembly.GetTypes()) + { + var cmdParsed = new Dictionary(); + var cmdMethods = new Dictionary>(); + MethodInfo cmdInitializer = null; + + foreach (MethodInfo method in type.GetMethods()) + { + if (!method.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(method); + + foreach (Attribute attr in attributes) + if (attr is ConsoleCommandAttribute cmdAttribute) + { + //Console.Print("found cmd '" + cmdAttribute.name + "' bound to field '" + method.Name + "'"); + + // Defer constructing the command until we have parsed all the methods for it in this assembly. + + List methods; + if (!cmdMethods.TryGetValue(cmdAttribute.name, out methods)) + { + methods = new List(); + cmdMethods.Add(cmdAttribute.name, methods); + } + + methods.Add(method); + + ConsoleCommand cmd = new ConsoleCommand(cmdAttribute.name, null); + if (!cmdParsed.ContainsKey(cmdAttribute.name)) + cmdParsed.Add(cmdAttribute.name, cmd); + foreach (string alias in cmdAttribute.aliases) + { + if (!cmdParsed.ContainsKey(alias)) + cmdParsed.Add(alias, cmd); + + List aliasMethods; + if (!cmdMethods.TryGetValue(alias, out aliasMethods)) + { + aliasMethods = new List(); + cmdMethods.Add(alias, aliasMethods); + } + + aliasMethods.Add(method); + } + } + else if (attr is ConsoleSubsystemInitializer) + { + cmdInitializer = method; + } + } + + foreach (var kv in cmdParsed) + { + var methods = cmdMethods[kv.Key]; + ConsoleCommand definition = kv.Value; + + ConsoleCommand cmd = new ConsoleCommand(definition.name, methods.ToArray()); + consoleCommands.Add(kv.Key, cmd); + } + + foreach (FieldInfo field in type.GetFields()) + { + if (!field.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(field); + + foreach (Attribute attr in attributes) + if (attr is ConsoleVariableAttribute cvarAttribute) + { + //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); + consoleVariables.Add(cvarAttribute.name, + new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, field)); + foreach (string alias in cvarAttribute.aliases) + consoleVariables.Add(alias, + new ConsoleVariable(cvarAttribute.name, + cvarAttribute.flags | ConsoleFlags.NoSerialize, field)); + } + } + + foreach (PropertyInfo prop in type.GetProperties()) + { + MethodInfo getter = prop.GetGetMethod(); + MethodInfo setter = prop.GetSetMethod(); + if (getter == null || setter == null || !getter.IsStatic || !setter.IsStatic) + continue; + + var attributes = Attribute.GetCustomAttributes(prop); + + foreach (Attribute attr in attributes) + if (attr is ConsoleVariableAttribute cvarAttribute) + { + //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); + consoleVariables.Add(cvarAttribute.name, + new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, getter, setter)); + foreach (string alias in cvarAttribute.aliases) + consoleVariables.Add(alias, + new ConsoleVariable(cvarAttribute.name, + cvarAttribute.flags | ConsoleFlags.NoSerialize, getter, setter)); + } + } + + if (cmdInitializer != null) + { + Console.PrintDebug(2, "Initializing " + type.Name); + cmdInitializer.Invoke(null, null); + } + } } + } - public static bool ShowExecutedLines => instance.ShowExecutedLines; + public string GetBufferHistory(int index) + { + if (consoleBufferHistory.Count == 0) + return null; - public static int DebugVerbosity + if (index > consoleBufferHistory.Count - 1 || index < 0) + return null; + + return consoleBufferHistory[index]; + } + + // Echoes text to Console + public void Print(string text) + { + debugLastLine = text; + + PrintLine(text); + } + + + // Echoes warning text to Console + public void PrintWarning(string text) + { + PrintLine(text); + } + + // Echoes error text to Console + //[DebuggerNonUserCode] + [DebuggerHidden] + public void PrintError(string text) + { + PrintLine(text); + + /*if (Debugger.IsAttached) + Debugger.Break(); + else*/ + throw new Exception(text); + } + + private void PrintLine(string text) + { + if (text.IndexOf('\n') != -1) { - get => instance.DebugVerbosity; - set => instance.DebugVerbosity = value; + // Avoid generating extra garbage in single-line cases + foreach (string line in text.Split('\n')) + { + ConsoleLine lineEntry = new ConsoleLine(line); + consoleLines.Add(lineEntry); + logStream.WriteLine(line); + OnPrint?.Invoke(text); + } } - - public static string LinePrefix => instance.LinePrefix; - - public static ReadOnlySpan Lines => instance.Lines; - - - public static void Init() + else { - if (instance != null) + ConsoleLine lineEntry = new ConsoleLine(text); + consoleLines.Add(lineEntry); + logStream.WriteLine(text); + OnPrint?.Invoke(text); + } + logStream.Flush(); + if (Debugger.IsAttached) + System.Diagnostics.Debug.WriteLine(text); + } + + public void PrintDebug(int verbosity, bool noRepeat, string text) + { + if (DebugVerbosity < verbosity) + return; + + if (noRepeat) + if (debugLastLine.Length == text.Length && debugLastLine == text) return; - Destroy(); - instance = new ConsoleInstance(); - instance.InitConsoleSubsystems(); + debugLastLine = text; -#if FLAX_EDITOR - ScriptsBuilder.ScriptsReload += Destroy; - Editor.Instance.PlayModeEnd += OnEditorPlayModeChanged; -#endif - } - - public static void Destroy() - { - if (instance != null) - { - instance.Dispose(); - instance = null; - -#if FLAX_EDITOR - ScriptsBuilder.ScriptsReload -= Destroy; - Editor.Instance.PlayModeEnd -= OnEditorPlayModeChanged; -#endif - } - } - -#if FLAX_EDITOR - private static void OnEditorPlayModeChanged() - { - //AssetManager.Globals.ResetValues(); - // Clear console buffer when leaving play mode - if (instance != null) - Clear(); - } -#endif - - public static string GetBufferHistory(int index) - { - return instance.GetBufferHistory(index); - } - - // Echoes text to Console - public static void Print(string text) - { - instance.Print(text); - } - - // Echoes warning text to Console - public static void PrintWarning(string text) - { - instance.PrintWarning(text); - } - - // Echoes error text to Console - [DebuggerHidden] - public static void PrintError(string text) - { - instance.PrintError(text); - } - - // Echoes developer/debug text to Console - public static void PrintDebug(string text) - { - instance.PrintDebug(1, false, text); - } - - // Echoes developer/debug text to Console - public static void PrintDebug(int verbosity, string text) - { - instance.PrintDebug(verbosity, false, text); - } - - // Echoes developer/debug text to Console - public static void PrintDebug(int verbosity, bool noRepeat, string text) - { - instance.PrintDebug(verbosity, noRepeat, text); - } - - // Opens the Console - public static void Open() - { - instance.Open(); - } - - // Closes the Console - public static void Close() - { - instance.Close(); - } - - // Clears the content of the Console - public static void Clear() - { - instance.Clear(); - } - - public static void Execute(string str, bool bufferInput = false, bool noOutput = false) - { - instance.Execute(str, bufferInput, noOutput); - } - - public static string GetVariable(string variableName) - { - return instance.GetVariable(variableName); - } + PrintLine(text); } - public class ConsoleInstance : IDisposable + // Opens the Console + public void Open() { - private readonly List consoleBufferHistory = new List(); - private readonly Dictionary consoleCommands = new Dictionary(); + if (IsOpen) + return; - //private static List consoleLines = new List(); - private readonly List consoleLines = new List(); + IsOpen = true; + OnOpen?.Invoke(); + } - private readonly Dictionary consoleVariables = - new Dictionary(); + // Closes the Console + public void Close() + { + if (!IsOpen) + return; - private readonly Stopwatch stopwatch = Stopwatch.StartNew(); + IsOpen = false; + OnClose?.Invoke(); - // Echoes developer/debug text to Console - private string debugLastLine = ""; - public Action OnClose; + stopwatch.Restart(); + } - public Action OnOpen; - public Action OnPrint; + // Clears the content of the Console + public void Clear() + { + consoleLines.Clear(); + } - public bool ShowExecutedLines = true; + public void Execute(string str, bool bufferInput = false, bool noOutput = false) + { + if (bufferInput) + consoleBufferHistory.Insert(0, str); - private StreamWriter logStream; + str = str.Trim(); - internal ConsoleInstance() + if (ShowExecutedLines && !noOutput) + Console.Print(LinePrefix + str); + + string[] strs = str.Split(' '); + string execute = strs[0]; + string executeLower = execute.ToLowerInvariant(); + string value = strs.Length > 1 ? str.Substring(execute.Length + 1) : null; + + //Console.PrintDebug("Executed '" + execute + "' with params: '" + value + "'"); + + try { - // Try different filename when running multiple instances - string logFilename = "console.log"; - const int attempts = 16; - for (int i = 0; i < attempts;) + if (consoleCommands.TryGetValue(executeLower, out ConsoleCommand cmd)) { - try - { -#if FLAX_EDITOR - logStream = new StreamWriter(Path.Combine(@"C:\dev\GoakeFlax", logFilename), false); -#else - logStream = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), logFilename), false); -#endif - } - catch (Exception) - { - logFilename = $"console-{++i}.log"; - continue; - } - break; + string[] values = strs.Skip(1).ToArray(); + if (values.Length > 0) + cmd.Invoke(values); + else + cmd.Invoke(); + //Console.Print("Command bound to '" + execute + "' is '" + cmd.method.Name + "'"); } - } - - public bool IsOpen { get; internal set; } = true; - - public bool IsSafeToQuit => stopwatch.Elapsed.TotalSeconds > 0.1; - - public int DebugVerbosity { get; set; } = 1; - public string LinePrefix { get; internal set; } = "]"; - - public ReadOnlySpan Lines => CollectionsMarshal.AsSpan(consoleLines); - - public void Dispose() - { - if (logStream != null) + else if (consoleVariables.TryGetValue(executeLower, out ConsoleVariable cvar)) { - logStream.Flush(); - logStream.Dispose(); - logStream = null; - } - } + if (value != null) + cvar.SetValue(value); - // Initializes the Console system. - internal void InitConsoleSubsystems() - { - //AppDomain currentDomain = AppDomain.CurrentDomain; -#if USE_NETCORE - var assemblies = Utils.GetAssemblies(); -#else - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); -#endif - - foreach (Assembly assembly in assemblies) - { - // Skip common assemblies - string assemblyName = assembly.GetName().Name; - if (assemblyName == "System" || - assemblyName.StartsWith("System.") || - assemblyName.StartsWith("Mono.") || - assemblyName == "mscorlib" || - assemblyName == "Newtonsoft.Json" || - assemblyName.StartsWith("FlaxEngine.") || - assemblyName.StartsWith("JetBrains.") || - assemblyName.StartsWith("Microsoft.") || - assemblyName.StartsWith("nunit.")) - continue; - - foreach (Type type in assembly.GetTypes()) - { - var cmdParsed = new Dictionary(); - var cmdMethods = new Dictionary>(); - MethodInfo cmdInitializer = null; - - foreach (MethodInfo method in type.GetMethods()) - { - if (!method.IsStatic) - continue; - - var attributes = Attribute.GetCustomAttributes(method); - - foreach (Attribute attr in attributes) - if (attr is ConsoleCommandAttribute cmdAttribute) - { - //Console.Print("found cmd '" + cmdAttribute.name + "' bound to field '" + method.Name + "'"); - - // Defer constructing the command until we have parsed all the methods for it in this assembly. - - List methods; - if (!cmdMethods.TryGetValue(cmdAttribute.name, out methods)) - { - methods = new List(); - cmdMethods.Add(cmdAttribute.name, methods); - } - - methods.Add(method); - - ConsoleCommand cmd = new ConsoleCommand(cmdAttribute.name, null); - if (!cmdParsed.ContainsKey(cmdAttribute.name)) - cmdParsed.Add(cmdAttribute.name, cmd); - foreach (string alias in cmdAttribute.aliases) - { - if (!cmdParsed.ContainsKey(alias)) - cmdParsed.Add(alias, cmd); - - List aliasMethods; - if (!cmdMethods.TryGetValue(alias, out aliasMethods)) - { - aliasMethods = new List(); - cmdMethods.Add(alias, aliasMethods); - } - - aliasMethods.Add(method); - } - } - else if (attr is ConsoleSubsystemInitializer) - { - cmdInitializer = method; - } - } - - foreach (var kv in cmdParsed) - { - var methods = cmdMethods[kv.Key]; - ConsoleCommand definition = kv.Value; - - ConsoleCommand cmd = new ConsoleCommand(definition.name, methods.ToArray()); - consoleCommands.Add(kv.Key, cmd); - } - - foreach (FieldInfo field in type.GetFields()) - { - if (!field.IsStatic) - continue; - - var attributes = Attribute.GetCustomAttributes(field); - - foreach (Attribute attr in attributes) - if (attr is ConsoleVariableAttribute cvarAttribute) - { - //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); - consoleVariables.Add(cvarAttribute.name, - new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, field)); - foreach (string alias in cvarAttribute.aliases) - consoleVariables.Add(alias, - new ConsoleVariable(cvarAttribute.name, - cvarAttribute.flags | ConsoleFlags.NoSerialize, field)); - } - } - - foreach (PropertyInfo prop in type.GetProperties()) - { - MethodInfo getter = prop.GetGetMethod(); - MethodInfo setter = prop.GetSetMethod(); - if (getter == null || setter == null || !getter.IsStatic || !setter.IsStatic) - continue; - - var attributes = Attribute.GetCustomAttributes(prop); - - foreach (Attribute attr in attributes) - if (attr is ConsoleVariableAttribute cvarAttribute) - { - //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); - consoleVariables.Add(cvarAttribute.name, - new ConsoleVariable(cvarAttribute.name, cvarAttribute.flags, getter, setter)); - foreach (string alias in cvarAttribute.aliases) - consoleVariables.Add(alias, - new ConsoleVariable(cvarAttribute.name, - cvarAttribute.flags | ConsoleFlags.NoSerialize, getter, setter)); - } - } - - if (cmdInitializer != null) - { - Console.PrintDebug(2, "Initializing " + type.Name); - cmdInitializer.Invoke(null, null); - } - } - } - } - - public string GetBufferHistory(int index) - { - if (consoleBufferHistory.Count == 0) - return null; - - if (index > consoleBufferHistory.Count - 1 || index < 0) - return null; - - return consoleBufferHistory[index]; - } - - // Echoes text to Console - public void Print(string text) - { - debugLastLine = text; - - PrintLine(text); - } - - - // Echoes warning text to Console - public void PrintWarning(string text) - { - PrintLine(text); - } - - // Echoes error text to Console - //[DebuggerNonUserCode] - [DebuggerHidden] - public void PrintError(string text) - { - PrintLine(text); - - /*if (Debugger.IsAttached) - Debugger.Break(); - else*/ - throw new Exception(text); - } - - private void PrintLine(string text) - { - if (text.IndexOf('\n') != -1) - { - // Avoid generating extra garbage in single-line cases - foreach (string line in text.Split('\n')) - { - ConsoleLine lineEntry = new ConsoleLine(line); - consoleLines.Add(lineEntry); - logStream.WriteLine(line); - OnPrint?.Invoke(text); - } + if (!noOutput) + Console.Print("'" + execute + "' is '" + cvar.GetValueString() + "'"); } else { - ConsoleLine lineEntry = new ConsoleLine(text); - consoleLines.Add(lineEntry); - logStream.WriteLine(text); - OnPrint?.Invoke(text); - } - logStream.Flush(); - if (Debugger.IsAttached) - System.Diagnostics.Debug.WriteLine(text); - } - - public void PrintDebug(int verbosity, bool noRepeat, string text) - { - if (DebugVerbosity < verbosity) - return; - - if (noRepeat) - if (debugLastLine.Length == text.Length && debugLastLine == text) - return; - - debugLastLine = text; - - PrintLine(text); - } - - // Opens the Console - public void Open() - { - if (IsOpen) - return; - - IsOpen = true; - OnOpen?.Invoke(); - } - - // Closes the Console - public void Close() - { - if (!IsOpen) - return; - - IsOpen = false; - OnClose?.Invoke(); - - stopwatch.Restart(); - } - - // Clears the content of the Console - public void Clear() - { - consoleLines.Clear(); - } - - public void Execute(string str, bool bufferInput = false, bool noOutput = false) - { - if (bufferInput) - consoleBufferHistory.Insert(0, str); - - str = str.Trim(); - - if (ShowExecutedLines && !noOutput) - Console.Print(LinePrefix + str); - - string[] strs = str.Split(' '); - string execute = strs[0]; - string executeLower = execute.ToLowerInvariant(); - string value = strs.Length > 1 ? str.Substring(execute.Length + 1) : null; - - //Console.PrintDebug("Executed '" + execute + "' with params: '" + value + "'"); - - try - { - if (consoleCommands.TryGetValue(executeLower, out ConsoleCommand cmd)) - { - string[] values = strs.Skip(1).ToArray(); - if (values.Length > 0) - cmd.Invoke(values); - else - cmd.Invoke(); - //Console.Print("Command bound to '" + execute + "' is '" + cmd.method.Name + "'"); - } - else if (consoleVariables.TryGetValue(executeLower, out ConsoleVariable cvar)) - { - if (value != null) - cvar.SetValue(value); - - if (!noOutput) - Console.Print("'" + execute + "' is '" + cvar.GetValueString() + "'"); - } - else - { - Console.Print("Unknown command '" + execute + "'"); - } - } - catch (Exception e) - { - var message = e.InnerException != null ? e.InnerException.Message : e.Message; - Console.Print("Command failed: " + message); - throw; + Console.Print("Unknown command '" + execute + "'"); } } - - public string GetVariable(string variableName) + catch (Exception e) { - if (consoleVariables.TryGetValue(variableName, out ConsoleVariable cvar)) - { - string value = cvar.GetValueString(); - return value; - } - - return null; + var message = e.InnerException != null ? e.InnerException.Message : e.Message; + Console.Print("Command failed: " + message); + throw; } } + + public string GetVariable(string variableName) + { + if (consoleVariables.TryGetValue(variableName, out ConsoleVariable cvar)) + { + string value = cvar.GetValueString(); + return value; + } + + return null; + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleAttributes.cs b/Source/Game/Console/ConsoleAttributes.cs index 404c986..79aebde 100644 --- a/Source/Game/Console/ConsoleAttributes.cs +++ b/Source/Game/Console/ConsoleAttributes.cs @@ -2,48 +2,48 @@ using System.Collections.Generic; using System.Linq; -namespace Game +namespace Game; + +[AttributeUsage(AttributeTargets.All)] +public abstract class ConsoleBaseAttribute : Attribute { - [AttributeUsage(AttributeTargets.All)] - public abstract class ConsoleBaseAttribute : Attribute + // Additional aliases for this command, these should only be used with user interaction. + // Commands such as 'cvarlist' should not list these in order to avoid clutter. + internal string[] aliases = new string[0]; + internal string name; + + public ConsoleBaseAttribute(string name) { - // Additional aliases for this command, these should only be used with user interaction. - // Commands such as 'cvarlist' should not list these in order to avoid clutter. - internal string[] aliases = new string[0]; - internal string name; - - public ConsoleBaseAttribute(string name) - { - this.name = name.ToLowerInvariant(); - } - - public ConsoleBaseAttribute(params string[] names) - { - name = names[0].ToLowerInvariant(); - aliases = new List(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray(); - } - - public ConsoleFlags flags { get; private set; } + this.name = name.ToLowerInvariant(); } - [AttributeUsage(AttributeTargets.All)] - public class ConsoleVariableAttribute : ConsoleBaseAttribute + public ConsoleBaseAttribute(params string[] names) { - public ConsoleVariableAttribute(string name) : base(name) - { - } + name = names[0].ToLowerInvariant(); + aliases = new List(names).Skip(1).Select(x => x.ToLowerInvariant()).ToArray(); } - [AttributeUsage(AttributeTargets.All)] - public class ConsoleCommandAttribute : ConsoleBaseAttribute + public ConsoleFlags flags { get; private set; } +} + +[AttributeUsage(AttributeTargets.All)] +public class ConsoleVariableAttribute : ConsoleBaseAttribute +{ + public ConsoleVariableAttribute(string name) : base(name) { + } +} + +[AttributeUsage(AttributeTargets.All)] +public class ConsoleCommandAttribute : ConsoleBaseAttribute +{ /// /// Registers a command to Console system. /// /// Name used for calling this command. public ConsoleCommandAttribute(string name) : base(name) - { - } + { + } /// /// Registers a command to Console system. @@ -53,15 +53,14 @@ namespace Game /// names are aliases. /// public ConsoleCommandAttribute(params string[] names) : base(names) - { - } - } - - /// - /// Constructor for the subsystem, must be called first before registering console commands. - /// - [AttributeUsage(AttributeTargets.All)] - public class ConsoleSubsystemInitializer : Attribute { } +} + +/// +/// Constructor for the subsystem, must be called first before registering console commands. +/// +[AttributeUsage(AttributeTargets.All)] +public class ConsoleSubsystemInitializer : Attribute +{ } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleCommand.cs b/Source/Game/Console/ConsoleCommand.cs index d7a18e4..622eee6 100644 --- a/Source/Game/Console/ConsoleCommand.cs +++ b/Source/Game/Console/ConsoleCommand.cs @@ -1,78 +1,77 @@ using System; using System.Reflection; -namespace Game +namespace Game; + +internal struct ConsoleCommand { - internal struct ConsoleCommand + public string name { get; } + + private readonly MethodInfo[] methods; + + public ConsoleCommand(string name, MethodInfo[] method) { - public string name { get; } + this.name = name; + methods = method; + } - private readonly MethodInfo[] methods; - - public ConsoleCommand(string name, MethodInfo[] method) + public void Invoke() + { + foreach (MethodInfo method in methods) { - this.name = name; - methods = method; + var methodParameters = method.GetParameters(); + if (methodParameters.Length == 0) + method.Invoke(null, null); + else if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) + method.Invoke(null, new object[] { Array.Empty() }); + else + continue; + return; } - public void Invoke() + throw new Exception("Unexpected number of parameters."); + } + + public void Invoke(string[] parameters) + { + MethodInfo match = null; + foreach (MethodInfo method in methods) { - foreach (MethodInfo method in methods) + var methodParameters = method.GetParameters(); + if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) { - var methodParameters = method.GetParameters(); - if (methodParameters.Length == 0) - method.Invoke(null, null); - else if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) - method.Invoke(null, new object[] { Array.Empty() }); - else - continue; - return; + match = method; + continue; } - throw new Exception("Unexpected number of parameters."); - } + if (methodParameters.Length != parameters.Length) + continue; - public void Invoke(string[] parameters) - { - MethodInfo match = null; - foreach (MethodInfo method in methods) - { - var methodParameters = method.GetParameters(); - if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(string[])) - { - match = method; + // TODO: try to parse string parameters to needed types first, + // may require finding the exact match first instead of first matching one. + for (int i = 0; i < methodParameters.Length; i++) + //if (methodParameters[i].ParameterType != parameters[i].GetType()) + if (methodParameters[i].ParameterType != typeof(string)) continue; - } + if (match != null) + // Prefer exact number of parameters over string[] match if (methodParameters.Length != parameters.Length) continue; - // TODO: try to parse string parameters to needed types first, - // may require finding the exact match first instead of first matching one. - for (int i = 0; i < methodParameters.Length; i++) - //if (methodParameters[i].ParameterType != parameters[i].GetType()) - if (methodParameters[i].ParameterType != typeof(string)) - continue; - - if (match != null) - // Prefer exact number of parameters over string[] match - if (methodParameters.Length != parameters.Length) - continue; - - match = method; - } - - if (match != null) - { - if (match.GetParameters().Length == 1 && match.GetParameters()[0].ParameterType == typeof(string[])) - match.Invoke(null, new object[] { parameters }); - else - match.Invoke(null, parameters); - - return; - } - - throw new Exception("Unexpected number of parameters."); + match = method; } + + if (match != null) + { + if (match.GetParameters().Length == 1 && match.GetParameters()[0].ParameterType == typeof(string[])) + match.Invoke(null, new object[] { parameters }); + else + match.Invoke(null, parameters); + + return; + } + + throw new Exception("Unexpected number of parameters."); } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleContentTextBox.cs b/Source/Game/Console/ConsoleContentTextBox.cs index b2686e9..9f5e021 100644 --- a/Source/Game/Console/ConsoleContentTextBox.cs +++ b/Source/Game/Console/ConsoleContentTextBox.cs @@ -6,608 +6,233 @@ using System.Text; using FlaxEngine; using FlaxEngine.GUI; -namespace Game +namespace Game; + +public class ConsoleContentTextBox : Control { - public class ConsoleContentTextBox : Control + private readonly FontReference Font; + + protected TextLayoutOptions _layout; + public Color BackgroundSelectedColor = Color.Transparent; + public float BackgroundSelectedFlashSpeed = 0; + public Color BorderColor; + public Color BorderSelectedColor = Color.Transparent; + public float CaretFlashSpeed = 0; + + public int DefaultMargin = 1; + + private float heightMultiplier = 1.0f; + + [HideInEditor] public ConsoleInputTextBox inputBox; + + public float LineSpacing = 1.0f; + public int ScrollMouseLines = 3; + public int ScrollOffset; + + public bool SelectionAllowed = true; + private bool selectionActive; + + public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); + private int selectionEndChar; + private int selectionEndLine; + private int selectionStartChar; + + private int selectionStartLine; + public Color TextColor = Color.White; + + public bool CopyOnSelect = true; + + public TextWrapping Wrapping; + + public ConsoleContentTextBox() { - private readonly FontReference Font; + } - protected TextLayoutOptions _layout; - public Color BackgroundSelectedColor = Color.Transparent; - public float BackgroundSelectedFlashSpeed = 0; - public Color BorderColor; - public Color BorderSelectedColor = Color.Transparent; - public float CaretFlashSpeed = 0; + public ConsoleContentTextBox(FontReference font, ConsoleInputTextBox inputBox, float x, float y, float width, + float height) : base( + x, y, width, height) + { + this.inputBox = inputBox; + Height = height; - public int DefaultMargin = 1; + Font = font; - private float heightMultiplier = 1.0f; + _layout = TextLayoutOptions.Default; + _layout.VerticalAlignment = TextAlignment.Near; + _layout.TextWrapping = TextWrapping.WrapChars; + _layout.Bounds = new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); - [HideInEditor] public ConsoleInputTextBox inputBox; - public float LineSpacing = 1.0f; - public int ScrollMouseLines = 3; - public int ScrollOffset; + //IsMultiline = true; + //IsReadOnly = true; + //CaretColor = new Color(0f, 0f, 0f, 0f); + AutoFocus = false; + } - public bool SelectionAllowed = true; - private bool selectionActive; + public override void OnDestroy() + { + base.OnDestroy(); + EndMouseCapture(); + } - public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); - private int selectionEndChar; - private int selectionEndLine; - private int selectionStartChar; + public int FontHeight => Font.GetFont().Height; - private int selectionStartLine; - public Color TextColor = Color.White; - - public bool CopyOnSelect = true; - - public TextWrapping Wrapping; - - public ConsoleContentTextBox() - { - } - - public ConsoleContentTextBox(FontReference font, ConsoleInputTextBox inputBox, float x, float y, float width, - float height) : base( - x, y, width, height) - { - this.inputBox = inputBox; - Height = height; - - Font = font; - - _layout = TextLayoutOptions.Default; - _layout.VerticalAlignment = TextAlignment.Near; - _layout.TextWrapping = TextWrapping.WrapChars; - _layout.Bounds = new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); - - - //IsMultiline = true; - //IsReadOnly = true; - //CaretColor = new Color(0f, 0f, 0f, 0f); - AutoFocus = false; - } - - public override void OnDestroy() - { - base.OnDestroy(); - EndMouseCapture(); - } - - public int FontHeight => Font.GetFont().Height; - - public float HeightMultiplier - { - get => heightMultiplier; - set - { - heightMultiplier = value; - UpdateHeight(); - } - } - - public bool HasSelection => !(selectionStartLine == selectionEndLine && selectionStartChar == selectionEndChar); - - private int GetFontCharacterWidth() - { - Font font = Font?.GetFont(); - if (!font) - return 0; - return (int)font.MeasureText("a").X; // hacky, but works for fixed-size fonts... - } - - public int GetFontHeight() - { - Font font = Font?.GetFont(); - if (font == null) - return (int)Height; - - return (int)Mathf.Round(LineSpacing * (font.Height / Platform.DpiScale) * Scale.Y); - } - - private int GetHeightInLines() - { - Font font = Font?.GetFont(); - if (!font) - return 0; - return (int)(Height / (font.Height / Platform.DpiScale)); // number of fully visible lines - } - - protected override void OnParentChangedInternal() - { - base.OnParentChangedInternal(); - - if (Parent != null) - OnParentResized(); - } - - public override void OnParentResized() + public float HeightMultiplier + { + get => heightMultiplier; + set { + heightMultiplier = value; UpdateHeight(); - base.OnParentResized(); } + } - private void UpdateHeight() + public bool HasSelection => !(selectionStartLine == selectionEndLine && selectionStartChar == selectionEndChar); + + private int GetFontCharacterWidth() + { + Font font = Font?.GetFont(); + if (!font) + return 0; + return (int)font.MeasureText("a").X; // hacky, but works for fixed-size fonts... + } + + public int GetFontHeight() + { + Font font = Font?.GetFont(); + if (font == null) + return (int)Height; + + return (int)Mathf.Round(LineSpacing * (font.Height / Platform.DpiScale) * Scale.Y); + } + + private int GetHeightInLines() + { + Font font = Font?.GetFont(); + if (!font) + return 0; + return (int)(Height / (font.Height / Platform.DpiScale)); // number of fully visible lines + } + + protected override void OnParentChangedInternal() + { + base.OnParentChangedInternal(); + + if (Parent != null) + OnParentResized(); + } + + public override void OnParentResized() + { + UpdateHeight(); + base.OnParentResized(); + } + + private void UpdateHeight() + { + if (Parent != null && Parent.Parent != null) + Height = Parent.Parent.Size.Y * HeightMultiplier - GetFontHeight(); + } + + + private void CalculateVisibleLines(ReadOnlySpan lines, out int firstVisibleLine, + out int lastVisibleLine, out LineInfo[] wrappedLines) + { + wrappedLines = null; + firstVisibleLine = 0; + lastVisibleLine = 0; + + Font font = Font.GetFont(); + if (!font) + return; + + float fontWidth = GetFontCharacterWidth(); + int lineMaxChars = (int)(Width / fontWidth); + int lineMaxLines = GetHeightInLines(); + int numLines = 0; + var lineInfos = new List(lineMaxLines + 1); + int linesSkipped = 0; + //foreach (string line in lines.Reverse()) + int lineIndex = lines.Length - 1; + for (; lineIndex >= 0; lineIndex--) { - if (Parent != null && Parent.Parent != null) - Height = Parent.Parent.Size.Y * HeightMultiplier - GetFontHeight(); - } - - - private void CalculateVisibleLines(ReadOnlySpan lines, out int firstVisibleLine, - out int lastVisibleLine, out LineInfo[] wrappedLines) - { - wrappedLines = null; - firstVisibleLine = 0; - lastVisibleLine = 0; - - Font font = Font.GetFont(); - if (!font) - return; - - float fontWidth = GetFontCharacterWidth(); - int lineMaxChars = (int)(Width / fontWidth); - int lineMaxLines = GetHeightInLines(); - int numLines = 0; - var lineInfos = new List(lineMaxLines + 1); - int linesSkipped = 0; - //foreach (string line in lines.Reverse()) - int lineIndex = lines.Length - 1; - for (; lineIndex >= 0; lineIndex--) + //lineIndex--; + if (linesSkipped < ScrollOffset) { - //lineIndex--; - if (linesSkipped < ScrollOffset) - { - linesSkipped++; - continue; - } - - string line = lines[lineIndex]; - - int numChars = 0; - int startIndex = lineInfos.Count; - while (numChars < line.Length) - { - LineInfo li = new LineInfo(); - li.lineIndex = lineIndex; - li.lineOffset = numChars; - li.lineLength = Math.Min(line.Length - numChars, lineMaxChars); - lineInfos.Add(li); - - numChars += lineMaxChars; - } - - if (lineInfos.Count - startIndex > 1) - lineInfos.Reverse(startIndex, lineInfos.Count - startIndex); - numLines++; - - if (lineInfos.Count > lineMaxLines) - break; + linesSkipped++; + continue; } - lineInfos.Reverse(); - wrappedLines = lineInfos.ToArray(); + string line = lines[lineIndex]; - //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... - lastVisibleLine = lineIndex; - firstVisibleLine = lastVisibleLine - numLines; - } - - public override void Draw() - { - // Cache data - Rectangle rect = new Rectangle(Float2.Zero, Size); - Font font = Font.GetFont(); - if (!font) - return; - - // Background - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Background"); - Color backColor = BackgroundColor; - if (IsMouseOver) - backColor = BackgroundSelectedColor; - if (backColor.A > 0.0f) - Render2D.FillRectangle(rect, backColor); - - Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; - if (borderColor.A > 0.0f) - Render2D.DrawRectangle(rect, borderColor); - Profiler.EndEvent(); - - Profiler.BeginEvent("ConsoleContentTextBoxDraw_FetchLines"); - var lines = Console.Lines; - Profiler.EndEvent(); - if (lines.Length > 0) + int numChars = 0; + int startIndex = lineInfos.Count; + while (numChars < line.Length) { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines"); + LineInfo li = new LineInfo(); + li.lineIndex = lineIndex; + li.lineOffset = numChars; + li.lineLength = Math.Min(line.Length - numChars, lineMaxChars); + lineInfos.Add(li); - // Apply view offset and clip mask - Rectangle textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); - Render2D.PushClip(textClipRectangle); - - Profiler.BeginEvent("ConsoleContentTextBoxDraw_CalcVisLines"); - // Make sure lengthy lines are split - CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); - Profiler.EndEvent(); - - float lineHeight = font.Height / Platform.DpiScale; - float accumHeight = wrappedLines.Length * lineHeight; - - // selection in line-space, wrapping ignored - int selectionLeftLine = selectionStartLine; - int selectionLeftChar = selectionStartChar; - int selectionRightLine = selectionEndLine; - int selectionRightChar = selectionEndChar; - - if (selectionLeftLine > selectionRightLine || - (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) - { - selectionLeftLine = selectionEndLine; - selectionLeftChar = selectionEndChar; - selectionRightLine = selectionStartLine; - selectionRightChar = selectionStartChar; - } - - // render selection - if (selectionActive) - { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Selection"); - //float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); - //alpha = alpha * alpha; - Color selectionColor = SelectionColor; // * alpha; - - TextLayoutOptions layout = _layout; - layout.Bounds = rect; - layout.Bounds.Y -= accumHeight - Height; - //for (int i = startLine; i < lastLine; i++) - foreach (LineInfo li in wrappedLines) - { - int lineIndex = li.lineIndex; - string fullLine = lines[lineIndex]; - string line = fullLine.Substring(li.lineOffset, li.lineLength); - - int leftChar = selectionLeftChar; - int rightChar = selectionRightChar; - - Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); - - // apply selection - if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) - { - if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) - { - // whole line is selected - Float2 lineSize = font.MeasureText(line); - selectionRect.Width = lineSize.X; - selectionRect.Height = lineSize.Y; - } - else if (lineIndex == selectionLeftLine) - { - if (lineIndex < selectionRightLine) - { - // right side of the line is selected - Float2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); - Float2 rightSize = font.MeasureText(fullLine.Substring(leftChar)); - selectionRect.X += leftSize.X; - selectionRect.Width = rightSize.X; - selectionRect.Height = rightSize.Y; - - //int diff = line.Length - selectionLeftChar; - //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : ""); - } - else if (lineIndex == selectionRightLine && leftChar != rightChar) - { - // selecting middle of the one line - Float2 lineSize = font.MeasureText(fullLine); - Float2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); - Float2 midSize = - font.MeasureText(fullLine.Substring(leftChar, rightChar - leftChar)); - - selectionRect.X += leftSize.X; - selectionRect.Width = midSize.X; - selectionRect.Height = lineSize.Y; - - //int diff = selectionRightChar - selectionLeftChar; - //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); - } - } - else if (lineIndex == selectionRightLine) - { - // left side of the line is selected - Float2 leftSize = font.MeasureText(fullLine.Substring(0, rightChar)); - selectionRect.Width = leftSize.X; - selectionRect.Height = leftSize.Y; - - //line = (selectionRightChar > 0 ? new string('X', selectionRightChar) : "") + line.Substring(selectionRightChar); - } - } - - Render2D.FillRectangle(selectionRect, selectionColor); - - layout.Bounds.Y += lineHeight; - } - - Profiler.EndEvent(); - } - - // render lines - { - Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines_Render"); - TextLayoutOptions layout = _layout; - layout.Bounds = rect; - layout.Bounds.Y -= accumHeight - Height; - foreach (LineInfo li in wrappedLines) - { - int lineIndex = li.lineIndex; - string line = lines[lineIndex].content.Substring(li.lineOffset, li.lineLength); - Render2D.DrawText(font, line, TextColor, ref layout); - layout.Bounds.Y += lineHeight; - } - - Profiler.EndEvent(); - } - - /*if (CaretPosition > -1) - { - var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2(); - var caretBounds = CaretBounds; - caretBounds.X += prefixSize.X; - - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; - Render2D.FillRectangle(caretBounds, CaretColor * alpha); - }*/ - - Render2D.PopClip(); - - Profiler.EndEvent(); + numChars += lineMaxChars; } + + if (lineInfos.Count - startIndex > 1) + lineInfos.Reverse(startIndex, lineInfos.Count - startIndex); + numLines++; + + if (lineInfos.Count > lineMaxLines) + break; } + lineInfos.Reverse(); + wrappedLines = lineInfos.ToArray(); - private void OnSelectingBegin() + //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... + lastVisibleLine = lineIndex; + firstVisibleLine = lastVisibleLine - numLines; + } + + public override void Draw() + { + // Cache data + Rectangle rect = new Rectangle(Float2.Zero, Size); + Font font = Font.GetFont(); + if (!font) + return; + + // Background + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Background"); + Color backColor = BackgroundColor; + if (IsMouseOver) + backColor = BackgroundSelectedColor; + if (backColor.A > 0.0f) + Render2D.FillRectangle(rect, backColor); + + Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; + if (borderColor.A > 0.0f) + Render2D.DrawRectangle(rect, borderColor); + Profiler.EndEvent(); + + Profiler.BeginEvent("ConsoleContentTextBoxDraw_FetchLines"); + var lines = Console.Lines; + Profiler.EndEvent(); + if (lines.Length > 0) { - if (selectionActive) - return; + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines"); - selectionActive = true; - StartMouseCapture(); - } + // Apply view offset and clip mask + Rectangle textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); + Render2D.PushClip(textClipRectangle); - private void OnSelectingEnd() - { - if (!selectionActive) - return; - - selectionActive = false; - EndMouseCapture(); - } - - public bool HitTestText(Float2 location, out int hitLine, out int hitChar) - { - hitLine = 0; - hitChar = 0; - Font font = Font.GetFont(); - if (font == null) - return false; - - var lines = Console.Lines; + Profiler.BeginEvent("ConsoleContentTextBoxDraw_CalcVisLines"); + // Make sure lengthy lines are split CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + Profiler.EndEvent(); - TextLayoutOptions layout = _layout; - layout.Bounds = new Rectangle(0, 0, Size); - float lineHeightNormalized = font.Height; - float lineHeight = lineHeightNormalized / Platform.DpiScale; - float visibleHeight = wrappedLines.Length * lineHeight; - float top = (layout.Bounds.Bottom - visibleHeight) / - Platform.DpiScale; // UI coordinate space remains normalized - int lineMaxLines = (int)(Height / lineHeight); - - int hiddenLines = 0; - if (wrappedLines.Length > lineMaxLines) - hiddenLines = wrappedLines.Length - lineMaxLines; - //if (top < layout.Bounds.Top) - // hiddenLines = (int)Math.Ceiling((layout.Bounds.Top - top) / (float)lineHeight); - - int hitWrappedLine = (int)((location.Y - top) / lineHeight); //+ hiddenLines; - if (hitWrappedLine < 0 || hitWrappedLine >= wrappedLines.Length) - return false; - - hitLine = wrappedLines[hitWrappedLine].lineIndex; - string line = lines[hitLine].content.Substring(wrappedLines[hitWrappedLine].lineOffset, - wrappedLines[hitWrappedLine].lineLength); - - layout.Bounds.Y = top + hitWrappedLine * lineHeight; - layout.Bounds.Height = top + 9999; //(visibleHeight / Platform.DpiScale); - /*if (layout.Bounds.Y < 0) - { - layout.Bounds.Y = 1; - }*/ - - hitChar = font.HitTestText(line, location, ref layout); - hitChar += wrappedLines[hitWrappedLine].lineOffset; - - //FlaxEngine.Debug.Log(string.Format("hit line {0}/{1}, char {2}", hitWrappedLine, wrappedLines.Length, hitChar)); - return true; - } - - /*public int CharIndexAtPoint(ref Float2 location) - { - return HitTestText(location + _viewOffset); - }*/ - - public override bool OnKeyDown(KeyboardKeys key) - { - if (!SelectionAllowed) - return false; - - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - - if (!CopyOnSelect) - { - // This is not working for some reason, the event is never called while selecting text - if ((shiftDown && key == KeyboardKeys.Delete) || (ctrlDown && key == KeyboardKeys.Insert) || - (ctrlDown && key == KeyboardKeys.C) || (ctrlDown && key == KeyboardKeys.X)) - { - Copy(); - return true; - } - } - - if (key == KeyboardKeys.PageUp) - { - ScrollOffset += GetHeightInLines() / 2; - // should count the wrapped line count here over Console.Lines.Count - //var maxOffset = Console.Lines.Count - GetHeightInLines(); - int maxOffset = Console.Lines.Length - 1; - if (ScrollOffset > maxOffset) - ScrollOffset = maxOffset; - return true; - } - else if (key == KeyboardKeys.PageDown) - { - ScrollOffset -= GetHeightInLines() / 2; - if (ScrollOffset < 0) - ScrollOffset = 0; - return true; - } - - //else if (ctrlDown && key == KeyboardKeys.A) - // SelectAll(); - if (!base.OnKeyDown(key)) - { - inputBox.Focus(); - return inputBox.OnKeyDown(key); - } - return true; - } - - public override bool OnMouseWheel(Float2 location, float delta) - { - if (!SelectionAllowed) - return false; - - if (delta < 0) - { - ScrollOffset -= ScrollMouseLines; - if (ScrollOffset < 0) - ScrollOffset = 0; - } - else if (delta > 0) - { - ScrollOffset += ScrollMouseLines; - int maxOffset = Console.Lines.Length - GetHeightInLines(); - if (ScrollOffset > maxOffset) - ScrollOffset = maxOffset; - } - - return false; - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (!SelectionAllowed) - return false; - - if (!Bounds.Contains(location)) - { - OnSelectingEnd(); - return false; - } - - bool ret = false; - if (/*button == MouseButton.Left &&*/ !IsFocused) - { - //inputBox.Focus(); - //ret = true; - } - - if (button == MouseButton.Left && Console.Lines.Length > 0) - { - bool selectionStarted = !selectionActive; - //Focus(); - OnSelectingBegin(); - - //FlaxEngine.Debug.Log("mousedown, started: " + selectionStarted.ToString()); - - if (HitTestText(location, out int hitLine, out int hitChar)) - { - selectionStartLine = hitLine; - selectionStartChar = hitChar; - selectionEndLine = hitLine; - selectionEndChar = hitChar; - //FlaxEngine.Debug.Log(string.Format("start line {0} char {1}", hitLine, hitChar)); - } - - - // Select range with shift - /*if (_selectionStart != -1 && RootWindow.GetKey(KeyboardKeys.Shift) && SelectionLength == 0) - { - if (hitPos < _selectionStart) - SetSelection(hitPos, _selectionStart); - else - SetSelection(_selectionStart, hitPos); - } - else - { - SetSelection(hitPos); - }*/ - - return true; - } - - return ret; - } - - public override void OnMouseMove(Float2 location) - { - if (!SelectionAllowed) - return; - - if (selectionActive) - if (HitTestText(location, out int hitLine, out int hitChar)) - { - selectionEndLine = hitLine; - selectionEndChar = hitChar; - if (CopyOnSelect) - Copy(); - //FlaxEngine.Debug.Log(string.Format("end line {0} char {1}", hitLine, hitChar)); - } - } - - /// - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (!SelectionAllowed) - return false; - - if (button == MouseButton.Left) - OnSelectingEnd(); - if (Bounds.Contains(location)) - Focus(inputBox); - return true; - } - - public override void OnMouseLeave() - { - base.OnMouseLeave(); - - if (!SelectionAllowed) - return; - - if (selectionActive) - { - OnSelectingEnd(); - Focus(inputBox); - } - } - - protected void Copy() - { - if (!selectionActive) - return; + float lineHeight = font.Height / Platform.DpiScale; + float accumHeight = wrappedLines.Length * lineHeight; // selection in line-space, wrapping ignored int selectionLeftLine = selectionStartLine; @@ -624,61 +249,435 @@ namespace Game selectionRightChar = selectionStartChar; } - var lines = Console.Lines; - CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); - - StringBuilder selectedText = new StringBuilder(); - int lastLineIndex = -1; - - foreach (LineInfo li in wrappedLines) + // render selection + if (selectionActive) { - int lineIndex = li.lineIndex; - if (lineIndex < selectionLeftLine || lineIndex > selectionRightLine) - continue; + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Selection"); + //float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); + //alpha = alpha * alpha; + Color selectionColor = SelectionColor; // * alpha; - if (lastLineIndex != lineIndex && lastLineIndex != -1) - selectedText.AppendLine(); - lastLineIndex = lineIndex; - - string fullLine = lines[lineIndex]; - string line = fullLine.Substring(li.lineOffset, li.lineLength); - - int leftChar = selectionLeftChar; - int rightChar = selectionRightChar; - - if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) + TextLayoutOptions layout = _layout; + layout.Bounds = rect; + layout.Bounds.Y -= accumHeight - Height; + //for (int i = startLine; i < lastLine; i++) + foreach (LineInfo li in wrappedLines) { - if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) + int lineIndex = li.lineIndex; + string fullLine = lines[lineIndex]; + string line = fullLine.Substring(li.lineOffset, li.lineLength); + + int leftChar = selectionLeftChar; + int rightChar = selectionRightChar; + + Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); + + // apply selection + if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) { - // whole line is selected - selectedText.Append(line); - } - else if (lineIndex == selectionLeftLine) - { - if (lineIndex < selectionRightLine) - // right side of the line is selected - selectedText.Append(fullLine.Substring(leftChar)); - else if (lineIndex == selectionRightLine && leftChar != rightChar) - // selecting middle of the one line - selectedText.Append(fullLine.Substring(leftChar, rightChar - leftChar)); - } - else if (lineIndex == selectionRightLine) - { - // left side of the line is selected - selectedText.Append(fullLine.Substring(0, rightChar)); + if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) + { + // whole line is selected + Float2 lineSize = font.MeasureText(line); + selectionRect.Width = lineSize.X; + selectionRect.Height = lineSize.Y; + } + else if (lineIndex == selectionLeftLine) + { + if (lineIndex < selectionRightLine) + { + // right side of the line is selected + Float2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); + Float2 rightSize = font.MeasureText(fullLine.Substring(leftChar)); + selectionRect.X += leftSize.X; + selectionRect.Width = rightSize.X; + selectionRect.Height = rightSize.Y; + + //int diff = line.Length - selectionLeftChar; + //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : ""); + } + else if (lineIndex == selectionRightLine && leftChar != rightChar) + { + // selecting middle of the one line + Float2 lineSize = font.MeasureText(fullLine); + Float2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); + Float2 midSize = + font.MeasureText(fullLine.Substring(leftChar, rightChar - leftChar)); + + selectionRect.X += leftSize.X; + selectionRect.Width = midSize.X; + selectionRect.Height = lineSize.Y; + + //int diff = selectionRightChar - selectionLeftChar; + //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); + } + } + else if (lineIndex == selectionRightLine) + { + // left side of the line is selected + Float2 leftSize = font.MeasureText(fullLine.Substring(0, rightChar)); + selectionRect.Width = leftSize.X; + selectionRect.Height = leftSize.Y; + + //line = (selectionRightChar > 0 ? new string('X', selectionRightChar) : "") + line.Substring(selectionRightChar); + } } + + Render2D.FillRectangle(selectionRect, selectionColor); + + layout.Bounds.Y += lineHeight; } + + Profiler.EndEvent(); } - if (selectedText.Length > 0) - Clipboard.Text = selectedText.ToString(); - } + // render lines + { + Profiler.BeginEvent("ConsoleContentTextBoxDraw_Lines_Render"); + TextLayoutOptions layout = _layout; + layout.Bounds = rect; + layout.Bounds.Y -= accumHeight - Height; + foreach (LineInfo li in wrappedLines) + { + int lineIndex = li.lineIndex; + string line = lines[lineIndex].content.Substring(li.lineOffset, li.lineLength); + Render2D.DrawText(font, line, TextColor, ref layout); + layout.Bounds.Y += lineHeight; + } - private struct LineInfo - { - public int lineIndex; - public int lineOffset; - public int lineLength; + Profiler.EndEvent(); + } + + /*if (CaretPosition > -1) + { + var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2(); + var caretBounds = CaretBounds; + caretBounds.X += prefixSize.X; + + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); + alpha = alpha * alpha * alpha * alpha * alpha * alpha; + Render2D.FillRectangle(caretBounds, CaretColor * alpha); + }*/ + + Render2D.PopClip(); + + Profiler.EndEvent(); } } + + + private void OnSelectingBegin() + { + if (selectionActive) + return; + + selectionActive = true; + StartMouseCapture(); + } + + private void OnSelectingEnd() + { + if (!selectionActive) + return; + + selectionActive = false; + EndMouseCapture(); + } + + public bool HitTestText(Float2 location, out int hitLine, out int hitChar) + { + hitLine = 0; + hitChar = 0; + Font font = Font.GetFont(); + if (font == null) + return false; + + var lines = Console.Lines; + CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + + TextLayoutOptions layout = _layout; + layout.Bounds = new Rectangle(0, 0, Size); + float lineHeightNormalized = font.Height; + float lineHeight = lineHeightNormalized / Platform.DpiScale; + float visibleHeight = wrappedLines.Length * lineHeight; + float top = (layout.Bounds.Bottom - visibleHeight) / + Platform.DpiScale; // UI coordinate space remains normalized + int lineMaxLines = (int)(Height / lineHeight); + + int hiddenLines = 0; + if (wrappedLines.Length > lineMaxLines) + hiddenLines = wrappedLines.Length - lineMaxLines; + //if (top < layout.Bounds.Top) + // hiddenLines = (int)Math.Ceiling((layout.Bounds.Top - top) / (float)lineHeight); + + int hitWrappedLine = (int)((location.Y - top) / lineHeight); //+ hiddenLines; + if (hitWrappedLine < 0 || hitWrappedLine >= wrappedLines.Length) + return false; + + hitLine = wrappedLines[hitWrappedLine].lineIndex; + string line = lines[hitLine].content.Substring(wrappedLines[hitWrappedLine].lineOffset, + wrappedLines[hitWrappedLine].lineLength); + + layout.Bounds.Y = top + hitWrappedLine * lineHeight; + layout.Bounds.Height = top + 9999; //(visibleHeight / Platform.DpiScale); + /*if (layout.Bounds.Y < 0) + { + layout.Bounds.Y = 1; + }*/ + + hitChar = font.HitTestText(line, location, ref layout); + hitChar += wrappedLines[hitWrappedLine].lineOffset; + + //FlaxEngine.Debug.Log(string.Format("hit line {0}/{1}, char {2}", hitWrappedLine, wrappedLines.Length, hitChar)); + return true; + } + + /*public int CharIndexAtPoint(ref Float2 location) + { + return HitTestText(location + _viewOffset); + }*/ + + public override bool OnKeyDown(KeyboardKeys key) + { + if (!SelectionAllowed) + return false; + + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + + if (!CopyOnSelect) + { + // This is not working for some reason, the event is never called while selecting text + if ((shiftDown && key == KeyboardKeys.Delete) || (ctrlDown && key == KeyboardKeys.Insert) || + (ctrlDown && key == KeyboardKeys.C) || (ctrlDown && key == KeyboardKeys.X)) + { + Copy(); + return true; + } + } + + if (key == KeyboardKeys.PageUp) + { + ScrollOffset += GetHeightInLines() / 2; + // should count the wrapped line count here over Console.Lines.Count + //var maxOffset = Console.Lines.Count - GetHeightInLines(); + int maxOffset = Console.Lines.Length - 1; + if (ScrollOffset > maxOffset) + ScrollOffset = maxOffset; + return true; + } + else if (key == KeyboardKeys.PageDown) + { + ScrollOffset -= GetHeightInLines() / 2; + if (ScrollOffset < 0) + ScrollOffset = 0; + return true; + } + + //else if (ctrlDown && key == KeyboardKeys.A) + // SelectAll(); + if (!base.OnKeyDown(key)) + { + inputBox.Focus(); + return inputBox.OnKeyDown(key); + } + return true; + } + + public override bool OnMouseWheel(Float2 location, float delta) + { + if (!SelectionAllowed) + return false; + + if (delta < 0) + { + ScrollOffset -= ScrollMouseLines; + if (ScrollOffset < 0) + ScrollOffset = 0; + } + else if (delta > 0) + { + ScrollOffset += ScrollMouseLines; + int maxOffset = Console.Lines.Length - GetHeightInLines(); + if (ScrollOffset > maxOffset) + ScrollOffset = maxOffset; + } + + return false; + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (!SelectionAllowed) + return false; + + if (!Bounds.Contains(location)) + { + OnSelectingEnd(); + return false; + } + + bool ret = false; + if (/*button == MouseButton.Left &&*/ !IsFocused) + { + //inputBox.Focus(); + //ret = true; + } + + if (button == MouseButton.Left && Console.Lines.Length > 0) + { + bool selectionStarted = !selectionActive; + //Focus(); + OnSelectingBegin(); + + //FlaxEngine.Debug.Log("mousedown, started: " + selectionStarted.ToString()); + + if (HitTestText(location, out int hitLine, out int hitChar)) + { + selectionStartLine = hitLine; + selectionStartChar = hitChar; + selectionEndLine = hitLine; + selectionEndChar = hitChar; + //FlaxEngine.Debug.Log(string.Format("start line {0} char {1}", hitLine, hitChar)); + } + + + // Select range with shift + /*if (_selectionStart != -1 && RootWindow.GetKey(KeyboardKeys.Shift) && SelectionLength == 0) + { + if (hitPos < _selectionStart) + SetSelection(hitPos, _selectionStart); + else + SetSelection(_selectionStart, hitPos); + } + else + { + SetSelection(hitPos); + }*/ + + return true; + } + + return ret; + } + + public override void OnMouseMove(Float2 location) + { + if (!SelectionAllowed) + return; + + if (selectionActive) + if (HitTestText(location, out int hitLine, out int hitChar)) + { + selectionEndLine = hitLine; + selectionEndChar = hitChar; + if (CopyOnSelect) + Copy(); + //FlaxEngine.Debug.Log(string.Format("end line {0} char {1}", hitLine, hitChar)); + } + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (!SelectionAllowed) + return false; + + if (button == MouseButton.Left) + OnSelectingEnd(); + if (Bounds.Contains(location)) + Focus(inputBox); + return true; + } + + public override void OnMouseLeave() + { + base.OnMouseLeave(); + + if (!SelectionAllowed) + return; + + if (selectionActive) + { + OnSelectingEnd(); + Focus(inputBox); + } + } + + protected void Copy() + { + if (!selectionActive) + return; + + // selection in line-space, wrapping ignored + int selectionLeftLine = selectionStartLine; + int selectionLeftChar = selectionStartChar; + int selectionRightLine = selectionEndLine; + int selectionRightChar = selectionEndChar; + + if (selectionLeftLine > selectionRightLine || + (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) + { + selectionLeftLine = selectionEndLine; + selectionLeftChar = selectionEndChar; + selectionRightLine = selectionStartLine; + selectionRightChar = selectionStartChar; + } + + var lines = Console.Lines; + CalculateVisibleLines(lines, out int startLine, out int lastLine, out var wrappedLines); + + StringBuilder selectedText = new StringBuilder(); + int lastLineIndex = -1; + + foreach (LineInfo li in wrappedLines) + { + int lineIndex = li.lineIndex; + if (lineIndex < selectionLeftLine || lineIndex > selectionRightLine) + continue; + + if (lastLineIndex != lineIndex && lastLineIndex != -1) + selectedText.AppendLine(); + lastLineIndex = lineIndex; + + string fullLine = lines[lineIndex]; + string line = fullLine.Substring(li.lineOffset, li.lineLength); + + int leftChar = selectionLeftChar; + int rightChar = selectionRightChar; + + if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) + { + if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) + { + // whole line is selected + selectedText.Append(line); + } + else if (lineIndex == selectionLeftLine) + { + if (lineIndex < selectionRightLine) + // right side of the line is selected + selectedText.Append(fullLine.Substring(leftChar)); + else if (lineIndex == selectionRightLine && leftChar != rightChar) + // selecting middle of the one line + selectedText.Append(fullLine.Substring(leftChar, rightChar - leftChar)); + } + else if (lineIndex == selectionRightLine) + { + // left side of the line is selected + selectedText.Append(fullLine.Substring(0, rightChar)); + } + } + } + + if (selectedText.Length > 0) + Clipboard.Text = selectedText.ToString(); + } + + private struct LineInfo + { + public int lineIndex; + public int lineOffset; + public int lineLength; + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleInputTextBox.cs b/Source/Game/Console/ConsoleInputTextBox.cs index 9ff1920..5339666 100644 --- a/Source/Game/Console/ConsoleInputTextBox.cs +++ b/Source/Game/Console/ConsoleInputTextBox.cs @@ -2,171 +2,170 @@ using System.Linq; using FlaxEngine; -namespace Game +namespace Game; + +public class ConsoleInputTextBox : ConsoleTextBoxBase { - public class ConsoleInputTextBox : ConsoleTextBoxBase + private readonly ConsoleContentTextBox contentBox; + + private int inputHistoryIndex = -1; + + public ConsoleInputTextBox() { - private readonly ConsoleContentTextBox contentBox; + } - private int inputHistoryIndex = -1; + public ConsoleInputTextBox(ConsoleContentTextBox contentBox, float x, float y, float width, float height) : + base(x, y, width, height) + { + this.contentBox = contentBox; + IsMultiline = true; // Not really but behaves better than single-line box + } - public ConsoleInputTextBox() + public override string TextPrefix => Console.LinePrefix; + + protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height); + protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height); + + private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None) + { + // Ignore any characters generated by the key which opens the console + string inputTextLower = Input.InputText.ToLowerInvariant(); + + IEnumerable consoleKeyMappings; + if (key == KeyboardKeys.None) + consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); + else + consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key == key); + foreach (ActionConfig mapping in consoleKeyMappings) { - } - - public ConsoleInputTextBox(ConsoleContentTextBox contentBox, float x, float y, float width, float height) : - base(x, y, width, height) - { - this.contentBox = contentBox; - IsMultiline = true; // Not really but behaves better than single-line box - } - - public override string TextPrefix => Console.LinePrefix; - - protected override Rectangle TextRectangle => new Rectangle(0, 0, Width, Height); - protected override Rectangle TextClipRectangle => new Rectangle(0, 0, Width, Height); - - private bool IsConsoleKeyPressed(KeyboardKeys key = KeyboardKeys.None) - { - // Ignore any characters generated by the key which opens the console - string inputTextLower = Input.InputText.ToLowerInvariant(); - - IEnumerable consoleKeyMappings; - if (key == KeyboardKeys.None) - consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); - else - consoleKeyMappings = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key == key); - foreach (ActionConfig mapping in consoleKeyMappings) + if (inputTextLower.Length > 0) { - if (inputTextLower.Length > 0) - { - if ((mapping.Key == KeyboardKeys.Backslash || mapping.Key == KeyboardKeys.BackQuote) && - (inputTextLower.Contains('ö') || - inputTextLower.Contains('æ') || - inputTextLower.Contains('ø'))) - continue; // Scandinavian keyboard layouts - if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\'')) - continue; - if (mapping.Key == KeyboardKeys.Backslash && - (inputTextLower.Contains('\\') || inputTextLower.Contains('|'))) - continue; - } - - if (Input.GetKey(mapping.Key)) - return true; + if ((mapping.Key == KeyboardKeys.Backslash || mapping.Key == KeyboardKeys.BackQuote) && + (inputTextLower.Contains('ö') || + inputTextLower.Contains('æ') || + inputTextLower.Contains('ø'))) + continue; // Scandinavian keyboard layouts + if (mapping.Key == KeyboardKeys.BackQuote && inputTextLower.Contains('\'')) + continue; + if (mapping.Key == KeyboardKeys.Backslash && + (inputTextLower.Contains('\\') || inputTextLower.Contains('|'))) + continue; } - return false; + if (Input.GetKey(mapping.Key)) + return true; } - public override bool OnCharInput(char c) + return false; + } + + public override bool OnCharInput(char c) + { + if (IsConsoleKeyPressed()) + return true; + + return base.OnCharInput(c); + } + + public override bool OnKeyDown(KeyboardKeys key) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + + if (IsConsoleKeyPressed(key)) { - if (IsConsoleKeyPressed()) - return true; - - return base.OnCharInput(c); - } - - public override bool OnKeyDown(KeyboardKeys key) - { - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - - if (IsConsoleKeyPressed(key)) - { - Clear(); - return true; - } - - if (key == KeyboardKeys.Escape) - { - Console.Close(); - Clear(); - return true; - } - - if (key == KeyboardKeys.Return) - { - try - { - Console.Execute(Text, true); - inputHistoryIndex = -1; - } - finally - { - Clear(); - } - - contentBox.ScrollOffset = 0; - return true; - } - - if (key == KeyboardKeys.ArrowUp) - { - inputHistoryIndex++; - - string line = Console.GetBufferHistory(inputHistoryIndex); - if (line == null) - { - inputHistoryIndex--; - return true; - } - - SetText(line); - SetSelection(TextLength); - return true; - } - - if (key == KeyboardKeys.ArrowDown) - { - if (inputHistoryIndex > 0) - inputHistoryIndex--; - - string line = Console.GetBufferHistory(inputHistoryIndex); - if (line == null) - return true; - - SetText(line); - SetSelection(TextLength); - return true; - } - - if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown) - return contentBox.OnKeyDown(key); - -#if FLAX_EDITOR - if (key == KeyboardKeys.F5) - return false; -#endif - - return base.OnKeyDown(key); - } - - public override void OnLostFocus() - { - // Prevent caret location getting reset back to beginning - bool oldEditing = _isEditing; - _isEditing = false; - base.OnLostFocus(); - _isEditing = oldEditing; - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - base.OnMouseDown(location, button); + Clear(); return true; } - public override bool OnMouseWheel(Float2 location, float delta) + if (key == KeyboardKeys.Escape) { - return contentBox.OnMouseWheel(location, delta); + Console.Close(); + Clear(); + return true; } - public override void Draw() + if (key == KeyboardKeys.Return) { - Profiler.BeginEvent("ConsoleInputTextBoxDraw"); - base.Draw(); - Profiler.EndEvent(); + try + { + Console.Execute(Text, true); + inputHistoryIndex = -1; + } + finally + { + Clear(); + } + + contentBox.ScrollOffset = 0; + return true; } + + if (key == KeyboardKeys.ArrowUp) + { + inputHistoryIndex++; + + string line = Console.GetBufferHistory(inputHistoryIndex); + if (line == null) + { + inputHistoryIndex--; + return true; + } + + SetText(line); + SetSelection(TextLength); + return true; + } + + if (key == KeyboardKeys.ArrowDown) + { + if (inputHistoryIndex > 0) + inputHistoryIndex--; + + string line = Console.GetBufferHistory(inputHistoryIndex); + if (line == null) + return true; + + SetText(line); + SetSelection(TextLength); + return true; + } + + if (key == KeyboardKeys.PageUp || key == KeyboardKeys.PageDown) + return contentBox.OnKeyDown(key); + +#if FLAX_EDITOR + if (key == KeyboardKeys.F5) + return false; +#endif + + return base.OnKeyDown(key); + } + + public override void OnLostFocus() + { + // Prevent caret location getting reset back to beginning + bool oldEditing = _isEditing; + _isEditing = false; + base.OnLostFocus(); + _isEditing = oldEditing; + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + base.OnMouseDown(location, button); + return true; + } + + public override bool OnMouseWheel(Float2 location, float delta) + { + return contentBox.OnMouseWheel(location, delta); + } + + public override void Draw() + { + Profiler.BeginEvent("ConsoleInputTextBoxDraw"); + base.Draw(); + Profiler.EndEvent(); } } \ No newline at end of file diff --git a/Source/Game/Console/ConsolePlugin.cs b/Source/Game/Console/ConsolePlugin.cs index 9b7eb85..d44cb75 100644 --- a/Source/Game/Console/ConsolePlugin.cs +++ b/Source/Game/Console/ConsolePlugin.cs @@ -6,178 +6,177 @@ using System.Linq; using FlaxEditor; #endif -namespace Game +namespace Game; + +public class ConsolePlugin : GamePlugin { - public class ConsolePlugin : GamePlugin + public ConsolePlugin() { - public ConsolePlugin() - { #if FLAX_EDITOR - _description = ConsoleEditorPlugin.DescriptionInternal; + _description = ConsoleEditorPlugin.DescriptionInternal; #endif - } - - private InputEvent onExit; - - public override void Initialize() - { - //FlaxEngine.Debug.Log("ConsolePlugin initialized"); - Console.Init(); - - onExit = new InputEvent("Exit"); - onExit.Pressed += () => - { - if (Console.IsSafeToQuit) - Engine.RequestExit(); - }; - - //AssetManager.Init(); // TODO: move these elsewhere -#if !FLAX_EDITOR - Level.SceneLoading += OnSceneLoading; - Level.SceneLoaded += OnSceneLoaded; -#endif - } - - public override void Deinitialize() - { - onExit.Dispose(); -#if !FLAX_EDITOR - Level.SceneLoading -= OnSceneLoading; - Level.SceneLoaded -= OnSceneLoaded; -#endif - } - - private void OnSceneLoading(Scene scene, Guid guid) - { - Level.SceneLoading -= OnSceneLoading; - LoadConfig(); - } - - private void OnSceneLoaded(Scene scene, Guid guid) - { - Level.SceneLoaded -= OnSceneLoaded; -#if !FLAX_EDITOR - - //GameMode.Connect(); - //GameMode.StartServer(true); - //NetworkManager.StartServer(); - //GameModeManager.Init(); - NetworkManager.ConnectServer(); -#endif - } - - private void LoadConfig() - { - Console.Print("Loading config file (GamePlugin)"); - AssetManager.Globals.ResetValues(); - - foreach (var line in AssetManager.Config.GetLines()) - Console.Execute(line, false, true); - } } -#if FLAX_EDITOR - public class ConsoleEditorPlugin : EditorPlugin + private InputEvent onExit; + + public override void Initialize() { - public static PluginDescription DescriptionInternal = new PluginDescription + //FlaxEngine.Debug.Log("ConsolePlugin initialized"); + Console.Init(); + + onExit = new InputEvent("Exit"); + onExit.Pressed += () => { - Author = "Ari Vuollet", - Name = "Console", - Description = "Quake-like console", - Version = Version.Parse("0.1.0"), - IsAlpha = true, - Category = "Game" + if (Console.IsSafeToQuit) + Engine.RequestExit(); }; - public override Type GamePluginType => typeof(ConsolePlugin); + //AssetManager.Init(); // TODO: move these elsewhere +#if !FLAX_EDITOR + Level.SceneLoading += OnSceneLoading; + Level.SceneLoaded += OnSceneLoaded; +#endif + } - public ConsoleEditorPlugin() + public override void Deinitialize() + { + onExit.Dispose(); +#if !FLAX_EDITOR + Level.SceneLoading -= OnSceneLoading; + Level.SceneLoaded -= OnSceneLoaded; +#endif + } + + private void OnSceneLoading(Scene scene, Guid guid) + { + Level.SceneLoading -= OnSceneLoading; + LoadConfig(); + } + + private void OnSceneLoaded(Scene scene, Guid guid) + { + Level.SceneLoaded -= OnSceneLoaded; +#if !FLAX_EDITOR + + //GameMode.Connect(); + //GameMode.StartServer(true); + //NetworkManager.StartServer(); + //GameModeManager.Init(); + NetworkManager.ConnectServer(); +#endif + } + + private void LoadConfig() + { + Console.Print("Loading config file (GamePlugin)"); + AssetManager.Globals.ResetValues(); + + foreach (var line in AssetManager.Config.GetLines()) + Console.Execute(line, false, true); + } +} + +#if FLAX_EDITOR +public class ConsoleEditorPlugin : EditorPlugin +{ + public static PluginDescription DescriptionInternal = new PluginDescription + { + Author = "Ari Vuollet", + Name = "Console", + Description = "Quake-like console", + Version = Version.Parse("0.1.0"), + IsAlpha = true, + Category = "Game" + }; + + public override Type GamePluginType => typeof(ConsolePlugin); + + public ConsoleEditorPlugin() + { + _description = DescriptionInternal; + } + + //private readonly InputEvent onExit = new InputEvent("Exit"); + + public override void Initialize() + { + //FlaxEngine.Debug.Log("ConsolePlugin initialized"); + Console.Init(); + + /*onExit.Triggered += () => { - _description = DescriptionInternal; - } + if (Console.IsSafeToQuit) + Engine.RequestExit(); + };*/ - //private readonly InputEvent onExit = new InputEvent("Exit"); + //AssetManager.Init(); - public override void Initialize() - { - //FlaxEngine.Debug.Log("ConsolePlugin initialized"); - Console.Init(); + Level.SceneLoading += OnSceneLoading; - /*onExit.Triggered += () => - { - if (Console.IsSafeToQuit) - Engine.RequestExit(); - };*/ + FlaxEditor.Editor.Instance.PlayModeBegin += OnPlayModeBegin; + FlaxEditor.Editor.Instance.PlayModeEnd += OnPlayModeEnd; + //Scripting.Exit += OnScriptingExit; - //AssetManager.Init(); + /*AssetManager.Init(); // TODO: move these elsewhere + AssetManager.Globals.ResetValues(); - Level.SceneLoading += OnSceneLoading; + foreach (var line in AssetManager.Config.GetLines()) + Console.Execute(line, false, true);*/ + } - FlaxEditor.Editor.Instance.PlayModeBegin += OnPlayModeBegin; - FlaxEditor.Editor.Instance.PlayModeEnd += OnPlayModeEnd; - //Scripting.Exit += OnScriptingExit; + /*private void OnScriptingExit() + { + FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin; + FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd; + }*/ - /*AssetManager.Init(); // TODO: move these elsewhere - AssetManager.Globals.ResetValues(); + - foreach (var line in AssetManager.Config.GetLines()) - Console.Execute(line, false, true);*/ - } + private void OnPlayModeBegin() + { + //FlaxEditor.Editor.Instance.PlayModeBegin -= Instance_PlayModeBegin; + LoadConfig(); + //GameMode.Connect(); + //WorldStateManager.Init(); + NetworkManager.StartServer(); - /*private void OnScriptingExit() + //GameMode.StartServer(true); + } + + private void OnPlayModeEnd() + { + //onExit.Dispose(); + //GameMode.StopServer(); + NetworkManager.Cleanup(); + } + + public override void Deinitialize() + { + //onExit.Dispose(); + //Level.SceneLoaded -= OnSceneLoaded; + Level.SceneLoading -= OnSceneLoading; + if (FlaxEditor.Editor.Instance != null) { FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin; FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd; - }*/ - - - - private void OnPlayModeBegin() - { - //FlaxEditor.Editor.Instance.PlayModeBegin -= Instance_PlayModeBegin; - LoadConfig(); - //GameMode.Connect(); - //WorldStateManager.Init(); - NetworkManager.StartServer(); - - //GameMode.StartServer(true); } - - private void OnPlayModeEnd() - { - //onExit.Dispose(); - //GameMode.StopServer(); - NetworkManager.Cleanup(); - } - - public override void Deinitialize() - { - //onExit.Dispose(); - //Level.SceneLoaded -= OnSceneLoaded; - Level.SceneLoading -= OnSceneLoading; - if (FlaxEditor.Editor.Instance != null) - { - FlaxEditor.Editor.Instance.PlayModeBegin -= OnPlayModeBegin; - FlaxEditor.Editor.Instance.PlayModeEnd -= OnPlayModeEnd; - } - } - - private void OnSceneLoading(Scene scene, Guid guid) - { - //Level.SceneLoaded -= OnSceneLoaded; - Level.SceneLoading -= OnSceneLoading; - LoadConfig(); - } - - private void LoadConfig() - { - Console.Print("Loading config file (EditorPlugin)"); - - AssetManager.Globals.ResetValues(); - - foreach (var line in AssetManager.Config.GetLines()) - Console.Execute(line, false, true); - } } + + private void OnSceneLoading(Scene scene, Guid guid) + { + //Level.SceneLoaded -= OnSceneLoaded; + Level.SceneLoading -= OnSceneLoading; + LoadConfig(); + } + + private void LoadConfig() + { + Console.Print("Loading config file (EditorPlugin)"); + + AssetManager.Globals.ResetValues(); + + foreach (var line in AssetManager.Config.GetLines()) + Console.Execute(line, false, true); + } +} #endif -} \ No newline at end of file diff --git a/Source/Game/Console/ConsoleScript.cs b/Source/Game/Console/ConsoleScript.cs index 87b1385..fb0978d 100644 --- a/Source/Game/Console/ConsoleScript.cs +++ b/Source/Game/Console/ConsoleScript.cs @@ -6,393 +6,392 @@ using FlaxEngine.Assertions; using FlaxEngine.GUI; using Object = FlaxEngine.Object; -namespace Game +namespace Game; + +public class ConsoleScript : Script { - public class ConsoleScript : Script + public Color BackgroundColor; + + public Texture BackgroundTexture; + private ConsoleContentTextBox consoleBox; + + public FontAsset ConsoleFont; + [Limit(5, 720)] public int ConsoleFontSize = 16; + + [Limit(0.05f, 1.0f)] public float ConsoleHeight = 0.65f; + private ConsoleInputTextBox consoleInputBox; + + internal InputEvent consoleInputEvent; + private ConsoleContentTextBox consoleNotifyBox; + + [Limit(0)] public int ConsoleNotifyLines = 15; + + [Limit(0f)] public float ConsoleSpeed = 3500f; + + private UIControl rootControl; + + private int fontHeight; + + public override void OnStart() { - public Color BackgroundColor; + consoleInputEvent = new InputEvent("Console"); + consoleInputEvent.Pressed += OnConsoleInputEvent; - public Texture BackgroundTexture; - private ConsoleContentTextBox consoleBox; + FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize); + Font fontRaw = fontReference.GetFont(); + fontHeight = (int)(fontRaw.Height / Platform.DpiScale); + // root actor which holds all the elements + //var rootContainerControl = new ContainerControl(new Rectangle(0, 0, screenSize.X, screenSize.Y)); + ContainerControl rootContainerControl = new ContainerControl(new Rectangle()); + rootContainerControl.SetAnchorPreset(AnchorPresets.StretchAll, false); - public FontAsset ConsoleFont; - [Limit(5, 720)] public int ConsoleFontSize = 16; + rootControl = Actor.AddChild(); + rootControl.Name = "ConsoleRoot"; + rootControl.Control = rootContainerControl; - [Limit(0.05f, 1.0f)] public float ConsoleHeight = 0.65f; - private ConsoleInputTextBox consoleInputBox; - - internal InputEvent consoleInputEvent; - private ConsoleContentTextBox consoleNotifyBox; - - [Limit(0)] public int ConsoleNotifyLines = 15; - - [Limit(0f)] public float ConsoleSpeed = 3500f; - - private UIControl rootControl; - - private int fontHeight; - - public override void OnStart() + VerticalPanel contentContainer = new VerticalPanel { - consoleInputEvent = new InputEvent("Console"); - consoleInputEvent.Pressed += OnConsoleInputEvent; + AutoSize = true, + Margin = Margin.Zero, + Spacing = 0, + Bounds = new Rectangle(), + BackgroundColor = BackgroundColor + }; + contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true); - FontReference fontReference = new FontReference(ConsoleFont, ConsoleFontSize); - Font fontRaw = fontReference.GetFont(); - fontHeight = (int)(fontRaw.Height / Platform.DpiScale); - // root actor which holds all the elements - //var rootContainerControl = new ContainerControl(new Rectangle(0, 0, screenSize.X, screenSize.Y)); - ContainerControl rootContainerControl = new ContainerControl(new Rectangle()); - rootContainerControl.SetAnchorPreset(AnchorPresets.StretchAll, false); + UIControl contentContainerControl = rootControl.AddChild(); + contentContainerControl.Name = "ContentContainer"; + contentContainerControl.Control = contentContainer; - rootControl = Actor.AddChild(); - rootControl.Name = "ConsoleRoot"; - rootControl.Control = rootContainerControl; - - VerticalPanel contentContainer = new VerticalPanel + { + if (consoleBox == null) { - AutoSize = true, - Margin = Margin.Zero, - Spacing = 0, - Bounds = new Rectangle(), - BackgroundColor = BackgroundColor - }; - contentContainer.SetAnchorPreset(AnchorPresets.StretchAll, true); - - UIControl contentContainerControl = rootControl.AddChild(); - contentContainerControl.Name = "ContentContainer"; - contentContainerControl.Control = contentContainer; - - { - if (consoleBox == null) - { - //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); - consoleBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); + //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); + consoleBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); - consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); - //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight); - //consoleBox.Height = consoleSize.Y - fontHeight; + consoleBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); + //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight); + //consoleBox.Height = consoleSize.Y - fontHeight; - //consoleBox.HorizontalAlignment = TextAlignment.Near; - //consoleBox.VerticalAlignment = TextAlignment.Near; - consoleBox.HeightMultiplier = ConsoleHeight; - consoleBox.Wrapping = TextWrapping.WrapWords; - consoleBox.BackgroundColor = Color.Transparent; - consoleBox.BackgroundSelectedColor = Color.Transparent; - consoleBox.BackgroundSelectedFlashSpeed = 0; - consoleBox.BorderSelectedColor = Color.Transparent; - consoleBox.CaretFlashSpeed = 0; - } - - Float2 locationFix = consoleBox.Location; - UIControl parentControl = contentContainerControl.AddChild(); - parentControl.Name = "ConsoleContent"; - parentControl.Control = consoleBox; - consoleBox.Location = locationFix; // workaround to UIControl.Control overriding the old position - - if (consoleNotifyBox == null) - { - //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); - consoleNotifyBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); - consoleNotifyBox.HeightMultiplier = 0; - consoleNotifyBox.Height = ConsoleNotifyLines * fontHeight; - consoleNotifyBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); - //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight); - - //consoleBox.HorizontalAlignment = TextAlignment.Near; - //consoleBox.VerticalAlignment = TextAlignment.Near; - //consoleNotifyBox.HeightMultiplier = ConsoleHeight; - consoleNotifyBox.Wrapping = TextWrapping.WrapWords; - consoleNotifyBox.BackgroundColor = Color.Transparent; - consoleNotifyBox.BackgroundSelectedColor = Color.Transparent; - consoleNotifyBox.BackgroundSelectedFlashSpeed = 0; - consoleNotifyBox.BorderSelectedColor = Color.Transparent; - consoleNotifyBox.CaretFlashSpeed = 0; - consoleNotifyBox.SelectionAllowed = false; - } - - Float2 locationFix2 = consoleNotifyBox.Location; - UIControl parentControl2 = Actor.AddChild(); - parentControl2.Name = "ConsoleNotifyContent"; - parentControl2.Control = consoleNotifyBox; - consoleNotifyBox.Location = locationFix2; // workaround to UIControl.Control overriding the old position + //consoleBox.HorizontalAlignment = TextAlignment.Near; + //consoleBox.VerticalAlignment = TextAlignment.Near; + consoleBox.HeightMultiplier = ConsoleHeight; + consoleBox.Wrapping = TextWrapping.WrapWords; + consoleBox.BackgroundColor = Color.Transparent; + consoleBox.BackgroundSelectedColor = Color.Transparent; + consoleBox.BackgroundSelectedFlashSpeed = 0; + consoleBox.BorderSelectedColor = Color.Transparent; + consoleBox.CaretFlashSpeed = 0; } + + Float2 locationFix = consoleBox.Location; + UIControl parentControl = contentContainerControl.AddChild(); + parentControl.Name = "ConsoleContent"; + parentControl.Control = consoleBox; + consoleBox.Location = locationFix; // workaround to UIControl.Control overriding the old position + + if (consoleNotifyBox == null) { - if (consoleInputBox == null) - { - //consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, consoleSize.Y - fontHeight, consoleSize.X, fontHeight); - consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, 0, 0, 0); - consoleInputBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); - //consoleInputBox.Location = new Float2(0, consoleSize.Y - fontHeight); - consoleInputBox.Height = fontHeight; + //consoleBox = new ConsoleContentTextBox(null, 0, 0, consoleSize.X, consoleSize.Y - fontHeight); + consoleNotifyBox = new ConsoleContentTextBox(fontReference, null, 0, 0, 0, 0); + consoleNotifyBox.HeightMultiplier = 0; + consoleNotifyBox.Height = ConsoleNotifyLines * fontHeight; + consoleNotifyBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, true); + //consoleBox.AnchorMax = new Float2(1.0f, ConsoleHeight); - consoleInputBox.Font = fontReference; - consoleBox.inputBox = consoleInputBox; - - consoleInputBox.Wrapping = TextWrapping.WrapWords; - consoleInputBox.BackgroundColor = Color.Transparent; - consoleInputBox.BackgroundSelectedColor = Color.Transparent; - consoleInputBox.BackgroundSelectedFlashSpeed = 0; - consoleInputBox.BorderSelectedColor = Color.Transparent; - consoleInputBox.CaretFlashSpeed = 0; - } - - - Float2 locationFix = consoleInputBox.Location; - UIControl parentControl = contentContainerControl.AddChild(); - parentControl.Name = "ConsoleInput"; - parentControl.Control = consoleInputBox; - - consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position + //consoleBox.HorizontalAlignment = TextAlignment.Near; + //consoleBox.VerticalAlignment = TextAlignment.Near; + //consoleNotifyBox.HeightMultiplier = ConsoleHeight; + consoleNotifyBox.Wrapping = TextWrapping.WrapWords; + consoleNotifyBox.BackgroundColor = Color.Transparent; + consoleNotifyBox.BackgroundSelectedColor = Color.Transparent; + consoleNotifyBox.BackgroundSelectedFlashSpeed = 0; + consoleNotifyBox.BorderSelectedColor = Color.Transparent; + consoleNotifyBox.CaretFlashSpeed = 0; + consoleNotifyBox.SelectionAllowed = false; } + Float2 locationFix2 = consoleNotifyBox.Location; + UIControl parentControl2 = Actor.AddChild(); + parentControl2.Name = "ConsoleNotifyContent"; + parentControl2.Control = consoleNotifyBox; + consoleNotifyBox.Location = locationFix2; // workaround to UIControl.Control overriding the old position + } + { + if (consoleInputBox == null) + { + //consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, consoleSize.Y - fontHeight, consoleSize.X, fontHeight); + consoleInputBox = new ConsoleInputTextBox(consoleBox, 0, 0, 0, 0); + consoleInputBox.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); + //consoleInputBox.Location = new Float2(0, consoleSize.Y - fontHeight); + consoleInputBox.Height = fontHeight; + + consoleInputBox.Font = fontReference; + consoleBox.inputBox = consoleInputBox; + + consoleInputBox.Wrapping = TextWrapping.WrapWords; + consoleInputBox.BackgroundColor = Color.Transparent; + consoleInputBox.BackgroundSelectedColor = Color.Transparent; + consoleInputBox.BackgroundSelectedFlashSpeed = 0; + consoleInputBox.BorderSelectedColor = Color.Transparent; + consoleInputBox.CaretFlashSpeed = 0; + } + + + Float2 locationFix = consoleInputBox.Location; + UIControl parentControl = contentContainerControl.AddChild(); + parentControl.Name = "ConsoleInput"; + parentControl.Control = consoleInputBox; + + consoleInputBox.Location = locationFix; // workaround to UIControl.Control overriding the old position + } + #if false - //for (int i = 0; i < 10; i++) - { - string[] teststr = { - /* - "...loading 'scripts/devilpunch.shader'", - "...loading 'scripts/mkoxide.shader'", - "...loading 'scripts/cpm22.shader'", - "...loading 'scripts/cpm27.shader'", - "...loading 'scripts/island.shader'", - "...loading 'scripts/noodtex3.shader'", - "...loading 'scripts/nood_cosdglass.shader'", - "...loading 'scripts/nood_fog_1000.shader'", - "...loading 'scripts/nood_lightbeams.shader'", - "...loading 'scripts/nood_nightsky_nolight.shader'", - "Rescanning shaders", - @"Raw input type 0: [0] \\?\HID#VID_046D&PID_C231#2&229a2ea&0&0000#", - @"Raw input type 0: [18] \\?\HID#VID_046D&PID_C539&MI_01&Col01#8&24523410&0&0000#", - @"Raw input type 0: [19] \\?\HID#VID_28DE&PID_1102&MI_01#8&2fb9bb60&0&0000#", - @"Raw input type 0: [20] \\?\HID#VID_04D9&PID_A131&MI_02&Col01#8&197f95af&0&0000#", - "Raw input: initialized with 5 mice and 0 keyboards", - "WASAPI: overriding channels", - "WASAPI: 2 channel 32bit 48000khz non-exclusive", - "WASAPI: requested periodicity: 128, default: 480, min: 128, max: 480, step: 16", - "WASAPI: Low latency mode enabled", - "WASAPI: buffer size: 280", - "OpenGL renderer initialized", - "video restart took 0.291480 seconds", - "main thread video restart took 0.291798 secs", - "[------ Goake Initialized ------]", - "Initializing menu.dat", - "Couldn't load sound/ambience/water1.wav", - "Couldn't load sound/ambience/wind2.wav", - "Couldn't load sound/misc/menu2.wav", - "Couldn't load sound/misc/menu3.wav", - "]cl_maxfps 120", - "a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat" - */ - "Warning: Unsupported entity field 'light'", - "Warning: Unsupported entity field '_keeplights'", - "Warning: Unsupported entity field 'light'", - "Warning: Unsupported entity field '_keeplights'", - "Warning: Unsupported entity field 'light'", - "Warning: Unsupported entity field '_keeplights'", - "Warning: Unsupported entity field 'light'", - "Warning: Unsupported entity field '_keeplights'", - "maps/aerowalk.bsp: Using lightmap format E5BGR9_UF", - "maps/devbox.bsp: Using lightmap format E5BGR9_UF", - "what", - "Game mode changed to: Free For All", - "what", - "641 additional FS searches", - "fixangle frame: 1427", - "Couldn't load sound/ambience/wind2.wav", - "Couldn't load sound/ambience/water1.wav" - }; + //for (int i = 0; i < 10; i++) + { + string[] teststr = { + /* + "...loading 'scripts/devilpunch.shader'", + "...loading 'scripts/mkoxide.shader'", + "...loading 'scripts/cpm22.shader'", + "...loading 'scripts/cpm27.shader'", + "...loading 'scripts/island.shader'", + "...loading 'scripts/noodtex3.shader'", + "...loading 'scripts/nood_cosdglass.shader'", + "...loading 'scripts/nood_fog_1000.shader'", + "...loading 'scripts/nood_lightbeams.shader'", + "...loading 'scripts/nood_nightsky_nolight.shader'", + "Rescanning shaders", + @"Raw input type 0: [0] \\?\HID#VID_046D&PID_C231#2&229a2ea&0&0000#", + @"Raw input type 0: [18] \\?\HID#VID_046D&PID_C539&MI_01&Col01#8&24523410&0&0000#", + @"Raw input type 0: [19] \\?\HID#VID_28DE&PID_1102&MI_01#8&2fb9bb60&0&0000#", + @"Raw input type 0: [20] \\?\HID#VID_04D9&PID_A131&MI_02&Col01#8&197f95af&0&0000#", + "Raw input: initialized with 5 mice and 0 keyboards", + "WASAPI: overriding channels", + "WASAPI: 2 channel 32bit 48000khz non-exclusive", + "WASAPI: requested periodicity: 128, default: 480, min: 128, max: 480, step: 16", + "WASAPI: Low latency mode enabled", + "WASAPI: buffer size: 280", + "OpenGL renderer initialized", + "video restart took 0.291480 seconds", + "main thread video restart took 0.291798 secs", + "[------ Goake Initialized ------]", + "Initializing menu.dat", + "Couldn't load sound/ambience/water1.wav", + "Couldn't load sound/ambience/wind2.wav", + "Couldn't load sound/misc/menu2.wav", + "Couldn't load sound/misc/menu3.wav", + "]cl_maxfps 120", + "a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat a very very very long long long line in repeat" + */ + "Warning: Unsupported entity field 'light'", + "Warning: Unsupported entity field '_keeplights'", + "Warning: Unsupported entity field 'light'", + "Warning: Unsupported entity field '_keeplights'", + "Warning: Unsupported entity field 'light'", + "Warning: Unsupported entity field '_keeplights'", + "Warning: Unsupported entity field 'light'", + "Warning: Unsupported entity field '_keeplights'", + "maps/aerowalk.bsp: Using lightmap format E5BGR9_UF", + "maps/devbox.bsp: Using lightmap format E5BGR9_UF", + "what", + "Game mode changed to: Free For All", + "what", + "641 additional FS searches", + "fixangle frame: 1427", + "Couldn't load sound/ambience/wind2.wav", + "Couldn't load sound/ambience/water1.wav" + }; - foreach (var l in teststr) - Console.Print(l); - } + foreach (var l in teststr) + Console.Print(l); + } #endif - /*FlaxEditor.Editor.Options.OptionsChanged += (FlaxEditor.Options.EditorOptions options) => - { - - };*/ - - /*Console.Print("normal line"); - Console.Print( - "a very very very long long long line in repeat a very very very long long long line in repeat 1 a very very ver" - + "y long long long line in repeat a very very very 2 long long long line in repeat a very very very 3 long long" - + " long line in repeat a very very very long long long 4 line in repeat"); - Console.Print("another normal line");*/ - - Debug.Logger.LogHandler.SendLog += OnSendLog; - Debug.Logger.LogHandler.SendExceptionLog += OnSendExceptionLog; - - - Console.OnOpen += OnConsoleOpen; - Console.OnClose += OnConsoleClose; - Console.OnPrint += OnPrint; - - // hide console by default, and close it instantly - Console.Close(); - OnConsoleClose(); - Float2 rootlocation = rootControl.Control.Location; - rootlocation.Y = -rootControl.Control.Height; - rootControl.Control.Location = rootlocation; - - Console.Print("Renderer: " + GPUDevice.Instance.RendererType); - } - - private void OnSendLog(LogType level, string msg, Object obj, string stackTrace) + /*FlaxEditor.Editor.Options.OptionsChanged += (FlaxEditor.Options.EditorOptions options) => { - Console.Print("[DEBUG] " + msg); - } - private void OnSendExceptionLog(Exception exception, Object obj) + };*/ + + /*Console.Print("normal line"); + Console.Print( + "a very very very long long long line in repeat a very very very long long long line in repeat 1 a very very ver" + + "y long long long line in repeat a very very very 2 long long long line in repeat a very very very 3 long long" + + " long line in repeat a very very very long long long 4 line in repeat"); + Console.Print("another normal line");*/ + + Debug.Logger.LogHandler.SendLog += OnSendLog; + Debug.Logger.LogHandler.SendExceptionLog += OnSendExceptionLog; + + + Console.OnOpen += OnConsoleOpen; + Console.OnClose += OnConsoleClose; + Console.OnPrint += OnPrint; + + // hide console by default, and close it instantly + Console.Close(); + OnConsoleClose(); + Float2 rootlocation = rootControl.Control.Location; + rootlocation.Y = -rootControl.Control.Height; + rootControl.Control.Location = rootlocation; + + Console.Print("Renderer: " + GPUDevice.Instance.RendererType); + } + + private void OnSendLog(LogType level, string msg, Object obj, string stackTrace) + { + Console.Print("[DEBUG] " + msg); + } + + private void OnSendExceptionLog(Exception exception, Object obj) + { + AssertionException assert = exception as AssertionException; + if (assert != null) { - AssertionException assert = exception as AssertionException; - if (assert != null) - { - string[] assertLines = assert.Message.Split('\n'); - if (assertLines.Length > 2) - Console.Print("Assert Failure: " + assertLines[2]); - else - Console.Print("Assert Failure: " + assert.Message); - } + string[] assertLines = assert.Message.Split('\n'); + if (assertLines.Length > 2) + Console.Print("Assert Failure: " + assertLines[2]); else - { - Console.Print("[EXCEP] " + exception.ToString()); - } + Console.Print("Assert Failure: " + assert.Message); } - - public override void OnDestroy() + else { - base.OnDestroy(); - - //consoleInputEvent.Triggered -= OnConsoleInputEvent; - consoleInputEvent?.Dispose(); - consoleBox?.Dispose(); - consoleNotifyBox?.Dispose(); - - Console.OnOpen -= OnConsoleOpen; - Console.OnClose -= OnConsoleClose; - Console.OnPrint -= OnPrint; - - Debug.Logger.LogHandler.SendLog -= OnSendLog; - Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog; - } - - private void OnConsoleInputEvent() - { - string currentInput = Input.InputText; - - if (Input.InputText.Length > 0) - { - // Really need rawinput support with separate ActionConfig.RawKey values, bound to physical keys/scancode instead of virtual ones - var consoleKeys = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); - bool backslash = consoleKeys.Any(x => x.Key == KeyboardKeys.Backslash); - bool backquote = consoleKeys.Any(x => x.Key == KeyboardKeys.BackQuote); - - // Workaround to only trigger Console key from key bound to left side of 1 (tilde/backquote/backslash key) - if ((backslash || backquote) && - (Input.InputText.ToLowerInvariant().Contains('ö') || - Input.InputText.ToLowerInvariant().Contains('æ') || - Input.InputText.ToLowerInvariant().Contains('ø'))) // Scandinavian keyboard layouts - return; - if (backquote && Input.InputText.ToLowerInvariant().Contains('\'')) // UK keyboard layouts - return; - if (backslash && (Input.InputText.ToLowerInvariant().Contains('\\') || - Input.InputText.ToLowerInvariant() - .Contains('|'))) // US/International keyboard layouts - return; - } - - if (!consoleInputBox.IsFocused) - Console.Open(); - else - Console.Close(); - } - - public void OnConsoleOpen() - { - Screen.CursorVisible = true; - Screen.CursorLock = CursorLockMode.None; - - consoleInputBox.Focus(); - Parent.As().ReceivesEvents = true; - } - - public void OnConsoleClose() - { - Screen.CursorVisible = false; - Screen.CursorLock = CursorLockMode.Locked; - - consoleInputBox.Defocus(); -#if FLAX_EDITOR - Editor.Instance.Windows.GameWin.Focus(); -#endif - Parent.As().ReceivesEvents = false; - } - - public override void OnUpdate() - { - base.OnUpdate(); - - if (!Console.IsOpen && Input.GetAction("ClearConsole")) - Console.Clear(); - - float targetY; - float conHeight = rootControl.Control.Height /*/ Platform.DpiScale*/; - if (!Console.IsOpen) - targetY = -conHeight; - else - targetY = 0.0f; - - Float2 location = rootControl.Control.Location; - if (location.Y != targetY) - { - if (location.Y > targetY) - { - // closing - location.Y -= Time.UnscaledDeltaTime * ConsoleSpeed; - if (location.Y < targetY) - location.Y = targetY; - - if (location.Y < targetY * ConsoleHeight) - location.Y = targetY; - } - else if (location.Y < targetY) - { - // opening - if (location.Y < -conHeight * ConsoleHeight) - location.Y = -conHeight * ConsoleHeight; - - location.Y += Time.UnscaledDeltaTime * ConsoleSpeed; - if (location.Y > targetY) - location.Y = targetY; - } - - rootControl.Control.Location = location; - - if (Console.IsOpen) - { - consoleNotifyBox.Visible = false; - consoleInputBox.Visible = true; - } - else if (!Console.IsOpen) - { - if (location.Y < -conHeight * ConsoleHeight + fontHeight) - { - consoleNotifyBox.Visible = true; - consoleInputBox.Visible = false; - } - } - } - } - - public void OnPrint(string text) - { - consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Length) * fontHeight; - } - - public void SetInput(string text) - { - consoleInputBox.Text = text; + Console.Print("[EXCEP] " + exception.ToString()); } } + + public override void OnDestroy() + { + base.OnDestroy(); + + //consoleInputEvent.Triggered -= OnConsoleInputEvent; + consoleInputEvent?.Dispose(); + consoleBox?.Dispose(); + consoleNotifyBox?.Dispose(); + + Console.OnOpen -= OnConsoleOpen; + Console.OnClose -= OnConsoleClose; + Console.OnPrint -= OnPrint; + + Debug.Logger.LogHandler.SendLog -= OnSendLog; + Debug.Logger.LogHandler.SendExceptionLog -= OnSendExceptionLog; + } + + private void OnConsoleInputEvent() + { + string currentInput = Input.InputText; + + if (Input.InputText.Length > 0) + { + // Really need rawinput support with separate ActionConfig.RawKey values, bound to physical keys/scancode instead of virtual ones + var consoleKeys = Input.ActionMappings.Where(x => x.Name == "Console" && x.Key != KeyboardKeys.None); + bool backslash = consoleKeys.Any(x => x.Key == KeyboardKeys.Backslash); + bool backquote = consoleKeys.Any(x => x.Key == KeyboardKeys.BackQuote); + + // Workaround to only trigger Console key from key bound to left side of 1 (tilde/backquote/backslash key) + if ((backslash || backquote) && + (Input.InputText.ToLowerInvariant().Contains('ö') || + Input.InputText.ToLowerInvariant().Contains('æ') || + Input.InputText.ToLowerInvariant().Contains('ø'))) // Scandinavian keyboard layouts + return; + if (backquote && Input.InputText.ToLowerInvariant().Contains('\'')) // UK keyboard layouts + return; + if (backslash && (Input.InputText.ToLowerInvariant().Contains('\\') || + Input.InputText.ToLowerInvariant() + .Contains('|'))) // US/International keyboard layouts + return; + } + + if (!consoleInputBox.IsFocused) + Console.Open(); + else + Console.Close(); + } + + public void OnConsoleOpen() + { + Screen.CursorVisible = true; + Screen.CursorLock = CursorLockMode.None; + + consoleInputBox.Focus(); + Parent.As().ReceivesEvents = true; + } + + public void OnConsoleClose() + { + Screen.CursorVisible = false; + Screen.CursorLock = CursorLockMode.Locked; + + consoleInputBox.Defocus(); +#if FLAX_EDITOR + Editor.Instance.Windows.GameWin.Focus(); +#endif + Parent.As().ReceivesEvents = false; + } + + public override void OnUpdate() + { + base.OnUpdate(); + + if (!Console.IsOpen && Input.GetAction("ClearConsole")) + Console.Clear(); + + float targetY; + float conHeight = rootControl.Control.Height /*/ Platform.DpiScale*/; + if (!Console.IsOpen) + targetY = -conHeight; + else + targetY = 0.0f; + + Float2 location = rootControl.Control.Location; + if (location.Y != targetY) + { + if (location.Y > targetY) + { + // closing + location.Y -= Time.UnscaledDeltaTime * ConsoleSpeed; + if (location.Y < targetY) + location.Y = targetY; + + if (location.Y < targetY * ConsoleHeight) + location.Y = targetY; + } + else if (location.Y < targetY) + { + // opening + if (location.Y < -conHeight * ConsoleHeight) + location.Y = -conHeight * ConsoleHeight; + + location.Y += Time.UnscaledDeltaTime * ConsoleSpeed; + if (location.Y > targetY) + location.Y = targetY; + } + + rootControl.Control.Location = location; + + if (Console.IsOpen) + { + consoleNotifyBox.Visible = false; + consoleInputBox.Visible = true; + } + else if (!Console.IsOpen) + { + if (location.Y < -conHeight * ConsoleHeight + fontHeight) + { + consoleNotifyBox.Visible = true; + consoleInputBox.Visible = false; + } + } + } + } + + public void OnPrint(string text) + { + consoleNotifyBox.Height = Math.Min(ConsoleNotifyLines, Console.Lines.Length) * fontHeight; + } + + public void SetInput(string text) + { + consoleInputBox.Text = text; + } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleTextBoxBase.cs b/Source/Game/Console/ConsoleTextBoxBase.cs index ae5233d..1fd3d2a 100644 --- a/Source/Game/Console/ConsoleTextBoxBase.cs +++ b/Source/Game/Console/ConsoleTextBoxBase.cs @@ -2,389 +2,388 @@ using FlaxEngine; using FlaxEngine.GUI; -namespace Game +namespace Game; + +// Mostly based on TextBox +public class ConsoleTextBoxBase : TextBoxBase { - // Mostly based on TextBox - public class ConsoleTextBoxBase : TextBoxBase + private readonly Stopwatch lastDoubleClick = new Stopwatch(); + protected TextLayoutOptions _layout; + + private bool doubleClicked; + private Float2 lastDoubleClickLocation = new Float2(0, 0); + + /// + /// Gets or sets the color of the selection (Transparent if not used). + /// + [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the selection (Transparent if not used).")] + public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); + + /// + /// Gets or sets the color of the text. + /// + [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the text.")] + public Color TextColor = Color.White; + + //[HideInEditor] + //public override string Text => _text; + + public ConsoleTextBoxBase() { - private readonly Stopwatch lastDoubleClick = new Stopwatch(); - protected TextLayoutOptions _layout; + } - private bool doubleClicked; - private Float2 lastDoubleClickLocation = new Float2(0, 0); + public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width) + { + Height = height; - /// - /// Gets or sets the color of the selection (Transparent if not used). - /// - [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the selection (Transparent if not used).")] - public Color SelectionColor = new Color(0x00, 0x7A, 0xCC); + IsReadOnly = false; + CaretColor = new Color(1f, 1f, 1f, 1f); + AutoFocus = true; + EndEditOnClick = false; - /// - /// Gets or sets the color of the text. - /// - [EditorDisplay("Style")] [EditorOrder(2000)] [Tooltip("The color of the text.")] - public Color TextColor = Color.White; + _layout = TextLayoutOptions.Default; + _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; + _layout.TextWrapping = TextWrapping.NoWrap; + _layout.Bounds = + new Rectangle(0, 0, Width, + Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); + } - //[HideInEditor] - //public override string Text => _text; + /// + /// Gets or sets the text wrapping within the control bounds. + /// + [EditorDisplay("Style")] + [EditorOrder(2000)] + [Tooltip("The text wrapping within the control bounds.")] + public TextWrapping Wrapping + { + get => _layout.TextWrapping; + set => _layout.TextWrapping = value; + } - public ConsoleTextBoxBase() + /// + /// Gets or sets the font. + /// + [EditorDisplay("Style")] + [EditorOrder(2000)] + public FontReference Font { get; set; } + + [HideInEditor] public virtual string TextPrefix { get; set; } = ""; + + /*protected override void SetText(string value) + { + // Prevent from null problems + if (value == null) + value = string.Empty; + + // Filter text + if (value.IndexOf('\r') != -1) + value = value.Replace("\r", ""); + + // Clamp length + if (value.Length > MaxLength) + value = value.Substring(0, MaxLength); + + // Ensure to use only single line + if (_isMultiline == false && value.Length > 0) { + // Extract only the first line + value = value.GetLines()[0]; } - public ConsoleTextBoxBase(float x, float y, float width, float height) : base(false, x, y, width) + if (Text != value) { - Height = height; + Deselect(); + ResetViewOffset(); - IsReadOnly = false; - CaretColor = new Color(1f, 1f, 1f, 1f); - AutoFocus = true; - EndEditOnClick = false; + Text = value; - _layout = TextLayoutOptions.Default; - _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; - _layout.TextWrapping = TextWrapping.NoWrap; - _layout.Bounds = - new Rectangle(0, 0, Width, - Height); //new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); + OnTextChanged(); + } + }*/ + + public int GetFontHeight() + { + Font font = Font.GetFont(); + if (font == null) + return (int)Height; + + return (int)Mathf.Round(font.Height * Scale.Y); + } + + public override void Clear() + { + // Can't clear the text while user is editing it... + bool oldEditing = _isEditing; + _isEditing = false; + base.Clear(); + _isEditing = oldEditing; + } + + public override void ResetViewOffset() + { + TargetViewOffset = new Float2(0, 0); + } + + /*public void ScrollToEnd() + { + float maxY = TextSize.Y - Height; + float spacing = GetRealLineSpacing(); + maxY += spacing; + + TargetViewOffset = new Float2(0, Math.Max(0, maxY)); + }*/ + + public override void ScrollToCaret() + { + if (Text.Length == 0) + return; + + Rectangle caretBounds = CaretBounds; + + float maxY = TextSize.Y - Height; + + Float2 newLocation = CaretBounds.Location; + TargetViewOffset = Float2.Clamp(newLocation, new Float2(0, 0), new Float2(_targetViewOffset.X, maxY)); + } + + /*const bool smoothScrolling = false; + + public override bool OnMouseWheel(Float2 location, float delta) + { + if (!IsMultiline || Text.Length == 0) + return false; + + if (!smoothScrolling) + delta = GetFontHeight() * Math.Sign(delta) * 3; + else + delta *= 30; + + float maxY = TextSize.Y - Height; + float offset = GetRealLineSpacing(); + maxY += offset; + TargetViewOffset = Float2.Clamp(_targetViewOffset - new Float2(0, delta), new Float2(0, offset), new Float2(_targetViewOffset.X, maxY)); + return true; + }*/ + + public override Float2 GetTextSize() + { + Font font = Font.GetFont(); + if (font == null) + return Float2.Zero; + + return font.MeasureText(Text, ref _layout); + } + + public override Float2 GetCharPosition(int index, out float height) + { + Font font = Font.GetFont(); + if (font == null) + { + height = Height; + return Float2.Zero; } - /// - /// Gets or sets the text wrapping within the control bounds. - /// - [EditorDisplay("Style")] - [EditorOrder(2000)] - [Tooltip("The text wrapping within the control bounds.")] - public TextWrapping Wrapping + height = GetFontHeight(); + return font.GetCharPosition(Text, index, ref _layout); + } + + public override int HitTestText(Float2 location) + { + Font font = Font.GetFont(); + if (font == null) + return 0; + + if (TextPrefix != "") { - get => _layout.TextWrapping; - set => _layout.TextWrapping = value; + Float2 prefixSize = font.MeasureText(TextPrefix); + location.X -= prefixSize.X; } - /// - /// Gets or sets the font. - /// - [EditorDisplay("Style")] - [EditorOrder(2000)] - public FontReference Font { get; set; } + return font.HitTestText(Text, location, ref _layout); + } - [HideInEditor] public virtual string TextPrefix { get; set; } = ""; + protected override void OnIsMultilineChanged() + { + base.OnIsMultilineChanged(); - /*protected override void SetText(string value) + _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; + } + + public override bool OnKeyDown(KeyboardKeys key) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + bool ctrlDown = Root.GetKey(KeyboardKeys.Control); + + if (shiftDown && key == KeyboardKeys.Delete) + Cut(); + else if (ctrlDown && key == KeyboardKeys.Insert) + Copy(); + else if (shiftDown && key == KeyboardKeys.Insert) + Paste(); + if (shiftDown && key == KeyboardKeys.Home) { - // Prevent from null problems - if (value == null) - value = string.Empty; + if (!IsReadOnly) + SetSelection(_selectionStart, 0); + return true; + } - // Filter text - if (value.IndexOf('\r') != -1) - value = value.Replace("\r", ""); + if (shiftDown && key == KeyboardKeys.End) + { + if (!IsReadOnly) + SetSelection(_selectionStart, TextLength); + return true; + } - // Clamp length - if (value.Length > MaxLength) - value = value.Substring(0, MaxLength); + return base.OnKeyDown(key); + } - // Ensure to use only single line - if (_isMultiline == false && value.Length > 0) + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (doubleClicked && lastDoubleClick.Elapsed.TotalSeconds < 0.5 && + location == lastDoubleClickLocation) // Windows defaults to 500ms window + { + doubleClicked = false; + if (OnMouseTripleClick(location, button)) + return true; + } + + return base.OnMouseDown(location, button); + } + + + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + doubleClicked = true; + lastDoubleClick.Restart(); + lastDoubleClickLocation = location; + + return base.OnMouseDoubleClick(location, button); + } + + public bool OnMouseTripleClick(Float2 location, MouseButton button) + { + if (!IsMultiline) + SelectAll(); + else + // TODO: select the line + SelectAll(); + + return true; + } + + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + _layout.Bounds = TextRectangle; + } + + public override void Draw() + { + // Cache data + Rectangle rect = new Rectangle(Float2.Zero, Size); + Font font = Font.GetFont(); + if (!font) + return; + + // Background + Color backColor = BackgroundColor; + if (IsMouseOver) + backColor = BackgroundSelectedColor; + if (backColor.A > 0.0f) + Render2D.FillRectangle(rect, backColor); + + Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; + if (borderColor.A > 0.0f) + Render2D.DrawRectangle(rect, borderColor); + + //string text = TextPrefix + Text; + string text = TextPrefix + Text; + + if (text.Length == 0) + return; + + // Apply view offset and clip mask + Render2D.PushClip(TextClipRectangle); + bool useViewOffset = !_viewOffset.IsZero; + if (useViewOffset) + Render2D.PushTransform(Matrix3x3.Translation2D(-_viewOffset)); + + // Check if any text is selected to draw selection + if (HasSelection) + { + Float2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout); + Float2 rightEdge = font.GetCharPosition(text, SelectionRight + TextPrefix.Length, ref _layout); + float fontHeight = GetFontHeight(); + + // Draw selection background + float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); + alpha = alpha * alpha; + Color selectionColor = SelectionColor * alpha; + + int selectedLinesCount = 1 + Mathf.FloorToInt((rightEdge.Y - leftEdge.Y) / fontHeight); + if (selectedLinesCount == 1) // Selected is part of single line { - // Extract only the first line - value = value.GetLines()[0]; + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); + Render2D.FillRectangle(r1, selectionColor); } - - if (Text != value) + else // Selected is more than one line { - Deselect(); - ResetViewOffset(); + float leftMargin = _layout.Bounds.Location.X; + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); + Render2D.FillRectangle(r1, selectionColor); - Text = value; + for (int i = 3; i <= selectedLinesCount; i++) + { + leftEdge.Y += fontHeight; + Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); + Render2D.FillRectangle(r, selectionColor); + } - OnTextChanged(); + Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); + Render2D.FillRectangle(r2, selectionColor); } - }*/ - - public int GetFontHeight() - { - Font font = Font.GetFont(); - if (font == null) - return (int)Height; - - return (int)Mathf.Round(font.Height * Scale.Y); } - public override void Clear() + Render2D.DrawText(font, text, TextColor, ref _layout); + + if (CaretPosition > -1) { - // Can't clear the text while user is editing it... - bool oldEditing = _isEditing; - _isEditing = false; - base.Clear(); - _isEditing = oldEditing; - } - - public override void ResetViewOffset() - { - TargetViewOffset = new Float2(0, 0); - } - - /*public void ScrollToEnd() - { - float maxY = TextSize.Y - Height; - float spacing = GetRealLineSpacing(); - maxY += spacing; - - TargetViewOffset = new Float2(0, Math.Max(0, maxY)); - }*/ - - public override void ScrollToCaret() - { - if (Text.Length == 0) - return; - + Float2 prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2(); Rectangle caretBounds = CaretBounds; + caretBounds.X += prefixSize.X; - float maxY = TextSize.Y - Height; - - Float2 newLocation = CaretBounds.Location; - TargetViewOffset = Float2.Clamp(newLocation, new Float2(0, 0), new Float2(_targetViewOffset.X, maxY)); + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); + alpha = alpha * alpha * alpha * alpha * alpha * alpha; + Render2D.FillRectangle(caretBounds, CaretColor * alpha); } - /*const bool smoothScrolling = false; + // Restore rendering state + if (useViewOffset) + Render2D.PopTransform(); + Render2D.PopClip(); + } - public override bool OnMouseWheel(Float2 location, float delta) + public override void Paste() + { + if (IsReadOnly) + return; + + string clipboardText = Clipboard.Text; + // Handle newlines in clipboard text + if (!string.IsNullOrEmpty(clipboardText)) { - if (!IsMultiline || Text.Length == 0) - return false; - - if (!smoothScrolling) - delta = GetFontHeight() * Math.Sign(delta) * 3; - else - delta *= 30; - - float maxY = TextSize.Y - Height; - float offset = GetRealLineSpacing(); - maxY += offset; - TargetViewOffset = Float2.Clamp(_targetViewOffset - new Float2(0, delta), new Float2(0, offset), new Float2(_targetViewOffset.X, maxY)); - return true; - }*/ - - public override Float2 GetTextSize() - { - Font font = Font.GetFont(); - if (font == null) - return Float2.Zero; - - return font.MeasureText(Text, ref _layout); + // TODO: probably better to just split these lines and parse each line separately + clipboardText = clipboardText.Replace("\r\n", ""); + clipboardText = clipboardText.Replace("\n", ""); } - public override Float2 GetCharPosition(int index, out float height) + if (!string.IsNullOrEmpty(clipboardText)) { - Font font = Font.GetFont(); - if (font == null) - { - height = Height; - return Float2.Zero; - } - - height = GetFontHeight(); - return font.GetCharPosition(Text, index, ref _layout); - } - - public override int HitTestText(Float2 location) - { - Font font = Font.GetFont(); - if (font == null) - return 0; - - if (TextPrefix != "") - { - Float2 prefixSize = font.MeasureText(TextPrefix); - location.X -= prefixSize.X; - } - - return font.HitTestText(Text, location, ref _layout); - } - - protected override void OnIsMultilineChanged() - { - base.OnIsMultilineChanged(); - - _layout.VerticalAlignment = IsMultiline ? TextAlignment.Near : TextAlignment.Center; - } - - public override bool OnKeyDown(KeyboardKeys key) - { - bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - bool ctrlDown = Root.GetKey(KeyboardKeys.Control); - - if (shiftDown && key == KeyboardKeys.Delete) - Cut(); - else if (ctrlDown && key == KeyboardKeys.Insert) - Copy(); - else if (shiftDown && key == KeyboardKeys.Insert) - Paste(); - if (shiftDown && key == KeyboardKeys.Home) - { - if (!IsReadOnly) - SetSelection(_selectionStart, 0); - return true; - } - - if (shiftDown && key == KeyboardKeys.End) - { - if (!IsReadOnly) - SetSelection(_selectionStart, TextLength); - return true; - } - - return base.OnKeyDown(key); - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (doubleClicked && lastDoubleClick.Elapsed.TotalSeconds < 0.5 && - location == lastDoubleClickLocation) // Windows defaults to 500ms window - { - doubleClicked = false; - if (OnMouseTripleClick(location, button)) - return true; - } - - return base.OnMouseDown(location, button); - } - - - public override bool OnMouseDoubleClick(Float2 location, MouseButton button) - { - doubleClicked = true; - lastDoubleClick.Restart(); - lastDoubleClickLocation = location; - - return base.OnMouseDoubleClick(location, button); - } - - public bool OnMouseTripleClick(Float2 location, MouseButton button) - { - if (!IsMultiline) - SelectAll(); - else - // TODO: select the line - SelectAll(); - - return true; - } - - protected override void OnSizeChanged() - { - base.OnSizeChanged(); - - _layout.Bounds = TextRectangle; - } - - public override void Draw() - { - // Cache data - Rectangle rect = new Rectangle(Float2.Zero, Size); - Font font = Font.GetFont(); - if (!font) - return; - - // Background - Color backColor = BackgroundColor; - if (IsMouseOver) - backColor = BackgroundSelectedColor; - if (backColor.A > 0.0f) - Render2D.FillRectangle(rect, backColor); - - Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; - if (borderColor.A > 0.0f) - Render2D.DrawRectangle(rect, borderColor); - - //string text = TextPrefix + Text; - string text = TextPrefix + Text; - - if (text.Length == 0) - return; - - // Apply view offset and clip mask - Render2D.PushClip(TextClipRectangle); - bool useViewOffset = !_viewOffset.IsZero; - if (useViewOffset) - Render2D.PushTransform(Matrix3x3.Translation2D(-_viewOffset)); - - // Check if any text is selected to draw selection - if (HasSelection) - { - Float2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout); - Float2 rightEdge = font.GetCharPosition(text, SelectionRight + TextPrefix.Length, ref _layout); - float fontHeight = GetFontHeight(); - - // Draw selection background - float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); - alpha = alpha * alpha; - Color selectionColor = SelectionColor * alpha; - - int selectedLinesCount = 1 + Mathf.FloorToInt((rightEdge.Y - leftEdge.Y) / fontHeight); - if (selectedLinesCount == 1) // Selected is part of single line - { - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); - Render2D.FillRectangle(r1, selectionColor); - } - else // Selected is more than one line - { - float leftMargin = _layout.Bounds.Location.X; - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); - Render2D.FillRectangle(r1, selectionColor); - - for (int i = 3; i <= selectedLinesCount; i++) - { - leftEdge.Y += fontHeight; - Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); - Render2D.FillRectangle(r, selectionColor); - } - - Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); - Render2D.FillRectangle(r2, selectionColor); - } - } - - Render2D.DrawText(font, text, TextColor, ref _layout); - - if (CaretPosition > -1) - { - Float2 prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Float2(); - Rectangle caretBounds = CaretBounds; - caretBounds.X += prefixSize.X; - - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; - Render2D.FillRectangle(caretBounds, CaretColor * alpha); - } - - // Restore rendering state - if (useViewOffset) - Render2D.PopTransform(); - Render2D.PopClip(); - } - - public override void Paste() - { - if (IsReadOnly) - return; - - string clipboardText = Clipboard.Text; - // Handle newlines in clipboard text - if (!string.IsNullOrEmpty(clipboardText)) - { - // TODO: probably better to just split these lines and parse each line separately - clipboardText = clipboardText.Replace("\r\n", ""); - clipboardText = clipboardText.Replace("\n", ""); - } - - if (!string.IsNullOrEmpty(clipboardText)) - { - int right = SelectionRight; - Insert(clipboardText); - SetSelection(Mathf.Max(right, 0) + clipboardText.Length); - } + int right = SelectionRight; + Insert(clipboardText); + SetSelection(Mathf.Max(right, 0) + clipboardText.Length); } } } \ No newline at end of file diff --git a/Source/Game/Console/ConsoleVariable.cs b/Source/Game/Console/ConsoleVariable.cs index cc0d80e..e71ccbb 100644 --- a/Source/Game/Console/ConsoleVariable.cs +++ b/Source/Game/Console/ConsoleVariable.cs @@ -1,75 +1,74 @@ using System; using System.Reflection; -namespace Game -{ - [Flags] - public enum ConsoleFlags - { - NoSerialize = 1, // Value does not persist +namespace Game; - Alias = NoSerialize +[Flags] +public enum ConsoleFlags +{ + NoSerialize = 1, // Value does not persist + + Alias = NoSerialize +} + +internal struct ConsoleVariable +{ + public string name { get; } + public ConsoleFlags flags { get; } + + private readonly FieldInfo field; + private readonly MethodInfo getter; + private readonly MethodInfo setter; + + public ConsoleVariable(string name, ConsoleFlags flags, FieldInfo field) + { + this.name = name; + this.flags = flags; + this.field = field; + getter = null; + setter = null; } - internal struct ConsoleVariable + public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter) { - public string name { get; } - public ConsoleFlags flags { get; } + this.name = name; + this.flags = flags; + field = null; + this.getter = getter; + this.setter = setter; + } - private readonly FieldInfo field; - private readonly MethodInfo getter; - private readonly MethodInfo setter; - - public ConsoleVariable(string name, ConsoleFlags flags, FieldInfo field) + public string GetValueString() + { + Type type = field != null ? field.FieldType : getter.ReturnType; + if (type == typeof(string)) { - this.name = name; - this.flags = flags; - this.field = field; - getter = null; - setter = null; + if (field != null) + return (string)field.GetValue(null); + if (setter != null) + return (string)getter.Invoke(null, null); + } + else + { + throw new Exception("cvar is not type of string"); } - public ConsoleVariable(string name, ConsoleFlags flags, MethodInfo getter, MethodInfo setter) + throw new Exception("GetValueString no field or getter specified"); + } + + public void SetValue(string value) + { + Type type = field != null ? field.FieldType : getter.ReturnType; + if (type == typeof(string)) { - this.name = name; - this.flags = flags; - field = null; - this.getter = getter; - this.setter = setter; + if (field != null) + field.SetValue(null, value); + else if (setter != null) + setter.Invoke(null, new object[] { value }); } - - public string GetValueString() + else { - Type type = field != null ? field.FieldType : getter.ReturnType; - if (type == typeof(string)) - { - if (field != null) - return (string)field.GetValue(null); - if (setter != null) - return (string)getter.Invoke(null, null); - } - else - { - throw new Exception("cvar is not type of string"); - } - - throw new Exception("GetValueString no field or getter specified"); - } - - public void SetValue(string value) - { - Type type = field != null ? field.FieldType : getter.ReturnType; - if (type == typeof(string)) - { - if (field != null) - field.SetValue(null, value); - else if (setter != null) - setter.Invoke(null, new object[] { value }); - } - else - { - throw new Exception("Unsupported type for SetValue: " + type.Name); - } + throw new Exception("Unsupported type for SetValue: " + type.Name); } } } \ No newline at end of file diff --git a/Source/Game/Console/EngineSubsystem.cs b/Source/Game/Console/EngineSubsystem.cs index 919f870..369a006 100644 --- a/Source/Game/Console/EngineSubsystem.cs +++ b/Source/Game/Console/EngineSubsystem.cs @@ -9,725 +9,724 @@ using FlaxEditor.Content.Settings; using FlaxEngine; using FlaxEngine.Networking; -namespace Game +namespace Game; + +public enum UpscalingMode { - public enum UpscalingMode + None, + FSR2, + DLSS, + FSR1, +} + +// Holds Console variables and commands to control engine behaviour +public static class EngineSubsystem +{ + private static FSR _fsrPlugin; + public static FSR FsrPlugin { - None, - FSR2, - DLSS, - FSR1, + get + { + if (_fsrPlugin == null) + _fsrPlugin = PluginManager.GetPlugin(); + return _fsrPlugin; + } } - // Holds Console variables and commands to control engine behaviour - public static class EngineSubsystem +#if COMPILE_WITH_DLSS + private static DLSS _dlssPlugin; + public static DLSS DlssPlugin { - private static FSR _fsrPlugin; - public static FSR FsrPlugin + get { - get - { - if (_fsrPlugin == null) - _fsrPlugin = PluginManager.GetPlugin(); - return _fsrPlugin; - } - } - -#if COMPILE_WITH_DLSS - private static DLSS _dlssPlugin; - public static DLSS DlssPlugin - { - get - { - if (_dlssPlugin == null) - _dlssPlugin = PluginManager.GetPlugin(); - return _dlssPlugin; - } + if (_dlssPlugin == null) + _dlssPlugin = PluginManager.GetPlugin(); + return _dlssPlugin; } + } #endif - // TODO: this should manually set all postprocessing values to 0 or disabled - /*[ConsoleVariable("r_postprocessing")] - public static string PostProcessing + // TODO: this should manually set all postprocessing values to 0 or disabled + /*[ConsoleVariable("r_postprocessing")] + public static string PostProcessing + { + get { - get - { - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - return postFx.CameraArtifacts.OverrideFlags.ToString(); - return ""; - } - set - { - bool valueBoolean = false; - if (int.TryParse(value, out int valueInt)) - valueBoolean = valueInt != 0; - else - return; - - PostFxVolume postFx = Level.FindActor(); - if (postFx != null) - { - var cameraArtifacts = postFx.CameraArtifacts; - cameraArtifacts.OverrideFlags = valueBoolean ? CameraArtifactsSettingsOverride.None : CameraArtifactsSettingsOverride.All; - postFx.CameraArtifacts = cameraArtifacts; - } - } - }*/ - - [ConsoleVariable("r_vignette")] - public static string Vignette + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + return postFx.CameraArtifacts.OverrideFlags.ToString(); + return ""; + } + set { - get - { - return Graphics.PostProcessSettings.CameraArtifacts.VignetteIntensity.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 2.0f); + bool valueBoolean = false; + if (int.TryParse(value, out int valueInt)) + valueBoolean = valueInt != 0; + else + return; - PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; - CameraArtifactsSettings cameraArtifacts = postProcessSettings.CameraArtifacts; - cameraArtifacts.VignetteIntensity = valueFloat; - postProcessSettings.CameraArtifacts = cameraArtifacts; - Graphics.PostProcessSettings = postProcessSettings; - } + PostFxVolume postFx = Level.FindActor(); + if (postFx != null) + { + var cameraArtifacts = postFx.CameraArtifacts; + cameraArtifacts.OverrideFlags = valueBoolean ? CameraArtifactsSettingsOverride.None : CameraArtifactsSettingsOverride.All; + postFx.CameraArtifacts = cameraArtifacts; } } + }*/ - [ConsoleVariable("cl_maxfps")] - public static string MaxFps + [ConsoleVariable("r_vignette")] + public static string Vignette + { + get { - get => Time.UpdateFPS.ToString(); - set - { - if (float.TryParse(value, out float valueFloat)) - { - if (valueFloat <= 0.0f) - valueFloat = 0.0f; - else - valueFloat = Mathf.Clamp(valueFloat, 10f, 99999999999.0f); - - if (Time.UpdateFPS != valueFloat) - Time.UpdateFPS = valueFloat; - if (Time.DrawFPS != valueFloat) - Time.DrawFPS = valueFloat; - } - } + return Graphics.PostProcessSettings.CameraArtifacts.VignetteIntensity.ToString(); } - - [ConsoleVariable("r_renderscale")] - public static string RenderScale + set { - get => MainRenderTask.Instance.RenderingPercentage.ToString(); - set + if (float.TryParse(value, out float valueFloat)) { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.00001f, 4.0f); - MainRenderTask.Instance.RenderingPercentage = valueFloat; - } - } - } - - private static UpscalingMode GetUpscalingMode() - { - return (UpscalingMode)int.Parse(Upscaling); - } - - [ConsoleVariable("r_upscaling")] - public static string Upscaling - { - get - { -#if COMPILE_WITH_DLSS - if (DlssPlugin.PostFx?.Enabled ?? false) - return ((int)UpscalingMode.DLSS).ToString(); -#else - if (false) { } -#endif - - else if (FsrPlugin.PostFx?.Enabled ?? false) - return ((int)UpscalingMode.FSR1).ToString(); - else - return ((int)UpscalingMode.None).ToString(); - } - set - { - if (int.TryParse(value, out int intValue)) - { - UpscalingMode upscaling = (UpscalingMode)intValue; - -#if COMPILE_WITH_DLSS - if (DlssPlugin.PostFx != null) - { - if (upscaling == UpscalingMode.DLSS) - { - if (DlssPlugin.Support == DLSSSupport.Supported) - DlssPlugin.PostFx.Enabled = true; - else - { - DlssPlugin.PostFx.Enabled = false; - Console.Print("DLSS not supported"); - } - } - else - DlssPlugin.PostFx.Enabled = false; - } -#else - if (upscaling == UpscalingMode.DLSS) - { - Console.Print("DLSS not supported: compiled without COMPILE_WITH_DLSS"); - upscaling = UpscalingMode.None; - } -#endif - - if (FsrPlugin.PostFx != null) - FsrPlugin.PostFx.Enabled = upscaling == UpscalingMode.FSR1; - } - } - } - -#if !COMPILE_WITH_DLSS - private static float dummy_DlssSharpness; - private static int dummy_DlssQuality; -#endif - - [ConsoleVariable("r_dlss_sharpness")] - public static string DlssSharpness - { - get - { -#if COMPILE_WITH_DLSS - return DlssPlugin.Sharpness.ToString(); -#else - return dummy_DlssSharpness.ToString(); -#endif - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, -1f, 1f); -#if COMPILE_WITH_DLSS - DlssPlugin.Sharpness = valueFloat; -#else - dummy_DlssSharpness = valueFloat; -#endif - } - } - } - - [ConsoleVariable("r_dlss_quality")] - public static string DlssQuality - { - get - { -#if COMPILE_WITH_DLSS - return ((int)DlssPlugin.Quality).ToString(); -#else - return dummy_DlssQuality.ToString(); -#endif - } - set - { - if (int.TryParse(value, out int intValue)) - { -#if COMPILE_WITH_DLSS -#if USE_NETCORE - intValue = Math.Clamp(intValue, 0, (int)DLSSQuality.MAX-1); -#else - intValue = (intValue > (int)DLSSQuality.MAX-1) ? ((int)DLSSQuality.MAX-1) : (intValue < 0 ? 0 : intValue); -#endif - DlssPlugin.Quality = (DLSSQuality)intValue; -#else - //intValue = Math.Clamp(intValue, 0, 4); - intValue = (intValue > 4) ? 4 : (intValue < 0 ? 0 : intValue); - dummy_DlssQuality = intValue; -#endif - } - } - } - - [ConsoleVariable("r_fsr_sharpness")] - public static string FsrSharpness - { - get - { - // In shader, the value of 0 is the max sharpness... - float sharpness = 2.0f - FsrPlugin.PostFx.Sharpness; - return sharpness.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0f, 2.0f); - FsrPlugin.PostFx.Sharpness = 2.0f - valueFloat; - } - } - } - - [ConsoleVariable("cl_showweapon")] - public static string ShowWeapon - { - get => Level.FindActor("ViewModelCamera").IsActive ? "1" : "0"; - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - Level.FindActor("ViewModelCamera").IsActive = boolValue; - } - } - - - // Horizontal field of view of the Camera - [ConsoleVariable("fov")] - public static string CameraFov - { - get - { - float valueFloat = Level.FindActor("PlayerCamera").As().FieldOfView; - float horizontalFov = (float)(180.0f / Math.PI * - (2 * Math.Atan(4f / 3f * - Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); - return horizontalFov.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.01f, 360.0f); - - float verticalFov = (float)(180.0f / Math.PI * - (2 * Math.Atan(3f / 4f * - Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); - Level.FindActor("PlayerCamera").As().FieldOfView = verticalFov; - } - } - } - - [ConsoleVariable("r_lighting")] - public static string SceneLighting - { - get - { - return ((bool)AssetManager.Globals.GetValue("Scene Lighting") ? "1" : "0"); - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - AssetManager.Globals.SetValue("Scene Lighting", boolValue); - - AmbientOcclusion = AmbientOcclusion; - GlobalIllumination = GlobalIllumination; - //GlobalIllumination = value; - } - } - - private static bool _AmbientOcclusion; - [ConsoleVariable("r_ambientocclusion")] - public static string AmbientOcclusion - { - get - { - return _AmbientOcclusion ? "1" : "0";//Graphics.PostProcessSettings.AmbientOcclusion.Enabled ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - _AmbientOcclusion = boolValue; + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 2.0f); PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; - AmbientOcclusionSettings aoSettings = postProcessSettings.AmbientOcclusion; - /*aoSettings.OverrideFlags = (aoSettings.OverrideFlags & ~AmbientOcclusionSettingsOverride.Enabled) | - (boolValue - ? AmbientOcclusionSettingsOverride.Enabled - : 0 & AmbientOcclusionSettingsOverride.Enabled);*/ - - - aoSettings.Enabled = boolValue && SceneLighting == "1"; - postProcessSettings.AmbientOcclusion = aoSettings; - + CameraArtifactsSettings cameraArtifacts = postProcessSettings.CameraArtifacts; + cameraArtifacts.VignetteIntensity = valueFloat; + postProcessSettings.CameraArtifacts = cameraArtifacts; Graphics.PostProcessSettings = postProcessSettings; } } + } - [ConsoleVariable("r_vsync")] - public static string Vsync + [ConsoleVariable("cl_maxfps")] + public static string MaxFps + { + get => Time.UpdateFPS.ToString(); + set { - get + if (float.TryParse(value, out float valueFloat)) { - return Graphics.UseVSync ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; + if (valueFloat <= 0.0f) + valueFloat = 0.0f; + else + valueFloat = Mathf.Clamp(valueFloat, 10f, 99999999999.0f); - Graphics.UseVSync = boolValue; + if (Time.UpdateFPS != valueFloat) + Time.UpdateFPS = valueFloat; + if (Time.DrawFPS != valueFloat) + Time.DrawFPS = valueFloat; } } + } - private static bool _GlobalIllumination; - - [ConsoleVariable("r_gi")] - public static string GlobalIllumination + [ConsoleVariable("r_renderscale")] + public static string RenderScale + { + get => MainRenderTask.Instance.RenderingPercentage.ToString(); + set { - get + if (float.TryParse(value, out float valueFloat)) { - return _GlobalIllumination ? "1" : "0";//Graphics.PostProcessSettings.GlobalIllumination.Mode == GlobalIlluminationMode.DDGI ? "1" : "0"; + valueFloat = Mathf.Clamp(valueFloat, 0.00001f, 4.0f); + MainRenderTask.Instance.RenderingPercentage = valueFloat; } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; + } + } - _GlobalIllumination = boolValue; + private static UpscalingMode GetUpscalingMode() + { + return (UpscalingMode)int.Parse(Upscaling); + } + + [ConsoleVariable("r_upscaling")] + public static string Upscaling + { + get + { +#if COMPILE_WITH_DLSS + if (DlssPlugin.PostFx?.Enabled ?? false) + return ((int)UpscalingMode.DLSS).ToString(); +#else + if (false) { } +#endif + + else if (FsrPlugin.PostFx?.Enabled ?? false) + return ((int)UpscalingMode.FSR1).ToString(); + else + return ((int)UpscalingMode.None).ToString(); + } + set + { + if (int.TryParse(value, out int intValue)) + { + UpscalingMode upscaling = (UpscalingMode)intValue; + +#if COMPILE_WITH_DLSS + if (DlssPlugin.PostFx != null) + { + if (upscaling == UpscalingMode.DLSS) + { + if (DlssPlugin.Support == DLSSSupport.Supported) + DlssPlugin.PostFx.Enabled = true; + else + { + DlssPlugin.PostFx.Enabled = false; + Console.Print("DLSS not supported"); + } + } + else + DlssPlugin.PostFx.Enabled = false; + } +#else + if (upscaling == UpscalingMode.DLSS) + { + Console.Print("DLSS not supported: compiled without COMPILE_WITH_DLSS"); + upscaling = UpscalingMode.None; + } +#endif + + if (FsrPlugin.PostFx != null) + FsrPlugin.PostFx.Enabled = upscaling == UpscalingMode.FSR1; + } + } + } + +#if !COMPILE_WITH_DLSS + private static float dummy_DlssSharpness; + private static int dummy_DlssQuality; +#endif + + [ConsoleVariable("r_dlss_sharpness")] + public static string DlssSharpness + { + get + { +#if COMPILE_WITH_DLSS + return DlssPlugin.Sharpness.ToString(); +#else + return dummy_DlssSharpness.ToString(); +#endif + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, -1f, 1f); +#if COMPILE_WITH_DLSS + DlssPlugin.Sharpness = valueFloat; +#else + dummy_DlssSharpness = valueFloat; +#endif + } + } + } + + [ConsoleVariable("r_dlss_quality")] + public static string DlssQuality + { + get + { +#if COMPILE_WITH_DLSS + return ((int)DlssPlugin.Quality).ToString(); +#else + return dummy_DlssQuality.ToString(); +#endif + } + set + { + if (int.TryParse(value, out int intValue)) + { +#if COMPILE_WITH_DLSS +#if USE_NETCORE + intValue = Math.Clamp(intValue, 0, (int)DLSSQuality.MAX-1); +#else + intValue = (intValue > (int)DLSSQuality.MAX-1) ? ((int)DLSSQuality.MAX-1) : (intValue < 0 ? 0 : intValue); +#endif + DlssPlugin.Quality = (DLSSQuality)intValue; +#else + //intValue = Math.Clamp(intValue, 0, 4); + intValue = (intValue > 4) ? 4 : (intValue < 0 ? 0 : intValue); + dummy_DlssQuality = intValue; +#endif + } + } + } + + [ConsoleVariable("r_fsr_sharpness")] + public static string FsrSharpness + { + get + { + // In shader, the value of 0 is the max sharpness... + float sharpness = 2.0f - FsrPlugin.PostFx.Sharpness; + return sharpness.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0f, 2.0f); + FsrPlugin.PostFx.Sharpness = 2.0f - valueFloat; + } + } + } + + [ConsoleVariable("cl_showweapon")] + public static string ShowWeapon + { + get => Level.FindActor("ViewModelCamera").IsActive ? "1" : "0"; + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + Level.FindActor("ViewModelCamera").IsActive = boolValue; + } + } + + + // Horizontal field of view of the Camera + [ConsoleVariable("fov")] + public static string CameraFov + { + get + { + float valueFloat = Level.FindActor("PlayerCamera").As().FieldOfView; + float horizontalFov = (float)(180.0f / Math.PI * + (2 * Math.Atan(4f / 3f * + Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); + return horizontalFov.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.01f, 360.0f); + + float verticalFov = (float)(180.0f / Math.PI * + (2 * Math.Atan(3f / 4f * + Math.Tan(Math.PI / 180.0f * valueFloat / 2.0f)))); + Level.FindActor("PlayerCamera").As().FieldOfView = verticalFov; + } + } + } + + [ConsoleVariable("r_lighting")] + public static string SceneLighting + { + get + { + return ((bool)AssetManager.Globals.GetValue("Scene Lighting") ? "1" : "0"); + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + AssetManager.Globals.SetValue("Scene Lighting", boolValue); + + AmbientOcclusion = AmbientOcclusion; + GlobalIllumination = GlobalIllumination; + //GlobalIllumination = value; + } + } + + private static bool _AmbientOcclusion; + [ConsoleVariable("r_ambientocclusion")] + public static string AmbientOcclusion + { + get + { + return _AmbientOcclusion ? "1" : "0";//Graphics.PostProcessSettings.AmbientOcclusion.Enabled ? "1" : "0"; + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + _AmbientOcclusion = boolValue; + + PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; + AmbientOcclusionSettings aoSettings = postProcessSettings.AmbientOcclusion; + /*aoSettings.OverrideFlags = (aoSettings.OverrideFlags & ~AmbientOcclusionSettingsOverride.Enabled) | + (boolValue + ? AmbientOcclusionSettingsOverride.Enabled + : 0 & AmbientOcclusionSettingsOverride.Enabled);*/ + + + aoSettings.Enabled = boolValue && SceneLighting == "1"; + postProcessSettings.AmbientOcclusion = aoSettings; + + Graphics.PostProcessSettings = postProcessSettings; + } + } + + [ConsoleVariable("r_vsync")] + public static string Vsync + { + get + { + return Graphics.UseVSync ? "1" : "0"; + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + Graphics.UseVSync = boolValue; + } + } + + private static bool _GlobalIllumination; + + [ConsoleVariable("r_gi")] + public static string GlobalIllumination + { + get + { + return _GlobalIllumination ? "1" : "0";//Graphics.PostProcessSettings.GlobalIllumination.Mode == GlobalIlluminationMode.DDGI ? "1" : "0"; + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + _GlobalIllumination = boolValue; + + PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; + + GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; + giSettings.Mode = (boolValue && SceneLighting == "1") ? GlobalIlluminationMode.DDGI : GlobalIlluminationMode.None; + postProcessSettings.GlobalIllumination = giSettings; + + Graphics.PostProcessSettings = postProcessSettings; + + //Graphics.EnableGlobalSDF = boolValue; + } + } + + [ConsoleVariable("r_gi_bounce")] + public static string GlobalIlluminationBounce + { + get + { + return Graphics.PostProcessSettings.GlobalIllumination.BounceIntensity.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 999.0f); PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; - giSettings.Mode = (boolValue && SceneLighting == "1") ? GlobalIlluminationMode.DDGI : GlobalIlluminationMode.None; + giSettings.BounceIntensity = valueFloat; postProcessSettings.GlobalIllumination = giSettings; Graphics.PostProcessSettings = postProcessSettings; - - //Graphics.EnableGlobalSDF = boolValue; } } - - [ConsoleVariable("r_gi_bounce")] - public static string GlobalIlluminationBounce - { - get - { - return Graphics.PostProcessSettings.GlobalIllumination.BounceIntensity.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 999.0f); - - PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; - - GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; - giSettings.BounceIntensity = valueFloat; - postProcessSettings.GlobalIllumination = giSettings; - - Graphics.PostProcessSettings = postProcessSettings; - } - } - } - - /*[ConsoleVariable("r_gi_spacing")] - public static string GlobalIlluminationProbeSpacing - { - get - { - return Graphics.GIProbesSpacing.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 999.0f); - - Graphics.GIProbesSpacing = valueFloat; - } - } - }*/ - - [ConsoleVariable("r_gi_time")] - public static string GlobalIlluminationTime - { - get - { - return Graphics.PostProcessSettings.GlobalIllumination.TemporalResponse.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 10.0f); - - PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; - - GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; - giSettings.TemporalResponse = valueFloat; - postProcessSettings.GlobalIllumination = giSettings; - - Graphics.PostProcessSettings = postProcessSettings; - } - } - } - - [ConsoleVariable("r_gi_distance")] - public static string GlobalIlluminationDistance - { - get - { - return Graphics.PostProcessSettings.GlobalIllumination.Distance.ToString(); - } - set - { - if (float.TryParse(value, out float valueFloat)) - { - valueFloat = Mathf.Clamp(valueFloat, 0.0f, 10000000.0f); - - PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; - - GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; - giSettings.Distance = valueFloat; - postProcessSettings.GlobalIllumination = giSettings; - - Graphics.PostProcessSettings = postProcessSettings; - } - } - } - - [ConsoleVariable("r_shadows")] - public static string SceneShadows - { - get - { - return (bool)AssetManager.Globals.GetValue("Scene Shadows") ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - AssetManager.Globals.SetValue("Scene Shadows", boolValue); - } - } - - [ConsoleVariable("r_staticbatch")] - public static string StaticBatch - { - get - { - return (bool)AssetManager.Globals.GetValue("Static Batching") ? "1" : "0"; - } - set - { - bool boolValue = false; - if (int.TryParse(value, out int intValue)) - boolValue = intValue != 0; - else if (float.TryParse(value, out float valueFloat)) - boolValue = valueFloat != 0f; - - AssetManager.Globals.SetValue("Static Batching", boolValue); - } - } - - [ConsoleCommand("playdemo")] - public static void PlayDemoCommand(string[] text) - { - string demoName; - if (text.Length < 1) - demoName = "638201307621505588_v2";//return; - else - demoName = text[0]; - - if (!demoName.EndsWith(".gdem")) - demoName += ".gdem"; - - string demoPath = Path.Combine(AssetManager.DemoPath, demoName); - NetworkManager.PlayDemo(demoPath); -#if false - PlayerActor playerActor = Level.GetActors().First(/*x => - x.GetScript().PlayerId == NetworkManager.LocalPlayerClientId*/); - - string demoPath = Path.Combine(AssetManager.DemoPath, demoName); - if (File.Exists(demoPath)) - playerActor.GetScript().SetInput(demoPath); -#endif - } - - private static Action timeDemoUpdate = null; - private static void TimeDemoOnUpdate() - { - if (timeDemoUpdate != null) - timeDemoUpdate(); - } - - [ConsoleCommand("timedemo")] - public static void TimeDemoCommand(string[] text) - { - string demoName; - if (text.Length < 1) - demoName = "638201307621505588_v2";//return; - else - demoName = text[0]; - - if (!demoName.EndsWith(".gdem")) - demoName += ".gdem"; - - string demoPath = Path.Combine(AssetManager.DemoPath, demoName); - NetworkManager.PlayDemo(demoPath); - - float oldPhysicsFps = Time.PhysicsFPS; - float oldFps = Time.UpdateFPS; - Time.PhysicsFPS = 0f; - Time.UpdateFPS = 0f; - Time.DrawFPS = 0f; - - float accumTime = 0f; - int accumTimes = 0; - timeDemoUpdate = () => - { - if (!NetworkManager.IsDemoPlaying) - { - Console.Print($"timedemo ended, time: {accumTimes} frames, avg: {(accumTime/(float)accumTimes)*1000.0f}"); - timeDemoUpdate = null; - Time.PhysicsFPS = oldPhysicsFps; - Time.UpdateFPS = oldFps; - Time.DrawFPS = oldFps; - return; - } - - if (accumTimes == 0) - Console.Print($"timedemo started"); - - accumTime += Time.DeltaTime; - accumTimes++; - - }; - - Scripting.Update -= TimeDemoOnUpdate; - Scripting.Update += TimeDemoOnUpdate; - } - - [ConsoleCommand("timedemo2")] - public static void TimeDemo2Command(string[] text) - { - string demoName; - if (text.Length < 1) - demoName = "638201307621505588_v2";//return; - else - demoName = text[0]; - - if (!demoName.EndsWith(".gdem")) - demoName += ".gdem"; - - string demoPath = Path.Combine(AssetManager.DemoPath, demoName); - NetworkManager.PlayDemo(demoPath); - - float oldPhysicsFps = Time.PhysicsFPS; - float oldFps = Time.UpdateFPS; - Time.PhysicsFPS = 0f; - Time.UpdateFPS = 5f; - Time.DrawFPS = 5f; - - float accumTime = 0f; - int accumTimes = 0; - timeDemoUpdate = () => - { - if (!NetworkManager.IsDemoPlaying) - { - Console.Print($"timedemo ended, time: {accumTimes} frames, avg: {(accumTime/(float)accumTimes)*1000.0f}"); - timeDemoUpdate = null; - Time.PhysicsFPS = oldPhysicsFps; - Time.UpdateFPS = oldFps; - Time.DrawFPS = oldFps; - return; - } - - if (accumTimes == 0) - Console.Print($"timedemo started"); - - accumTime += Time.DeltaTime; - accumTimes++; - - }; - - Scripting.FixedUpdate -= TimeDemoOnUpdate; - Scripting.FixedUpdate += TimeDemoOnUpdate; - } - - [ConsoleVariable("net_fakelag")] - public static string NetFakeLag - { - get - { - var driver = NetworkManager.server != null ? (NetworkManager.ServerNetworkDriver as NetworkLagDriver) : (NetworkManager.ClientNetworkDriver as NetworkLagDriver); - if (driver == null) - return 0.ToString(); - return ((int)driver.Lag).ToString(); - } - set - { - var driver = NetworkManager.server != null ? (NetworkManager.ServerNetworkDriver as NetworkLagDriver) : (NetworkManager.ClientNetworkDriver as NetworkLagDriver); - if (driver == null) - return; - - int intValue = 0; - if (int.TryParse(value, out intValue)) - { } - else if (float.TryParse(value, out float valueFloat)) - intValue = (int)valueFloat; - intValue = Math.Clamp(intValue, 0, 2000); - - driver.Lag = intValue; - } - } - - [ConsoleCommand("map")] - public static void MapCommand() - { - //NetworkManager.StartServer(true); - NetworkManager.StartServer(); - } - - [ConsoleCommand("connect")] - public static void ConnectCommand() - { - //GameMode.Connect(); - NetworkManager.ConnectServer(); - } - - [ConsoleSubsystemInitializer] - public static void Initialize() - { - } - - [ConsoleCommand("quit", "exit")] - public static void ExitCommand() - { - Engine.RequestExit(); - } - - [ConsoleCommand("debuglog")] - public static void DebugLogCommand(string[] text) - { - Debug.Log(string.Join(" ", text)); - } + } + + /*[ConsoleVariable("r_gi_spacing")] + public static string GlobalIlluminationProbeSpacing + { + get + { + return Graphics.GIProbesSpacing.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 999.0f); + + Graphics.GIProbesSpacing = valueFloat; + } + } + }*/ + + [ConsoleVariable("r_gi_time")] + public static string GlobalIlluminationTime + { + get + { + return Graphics.PostProcessSettings.GlobalIllumination.TemporalResponse.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 10.0f); + + PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; + + GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; + giSettings.TemporalResponse = valueFloat; + postProcessSettings.GlobalIllumination = giSettings; + + Graphics.PostProcessSettings = postProcessSettings; + } + } + } + + [ConsoleVariable("r_gi_distance")] + public static string GlobalIlluminationDistance + { + get + { + return Graphics.PostProcessSettings.GlobalIllumination.Distance.ToString(); + } + set + { + if (float.TryParse(value, out float valueFloat)) + { + valueFloat = Mathf.Clamp(valueFloat, 0.0f, 10000000.0f); + + PostProcessSettings postProcessSettings = Graphics.PostProcessSettings; + + GlobalIlluminationSettings giSettings = postProcessSettings.GlobalIllumination; + giSettings.Distance = valueFloat; + postProcessSettings.GlobalIllumination = giSettings; + + Graphics.PostProcessSettings = postProcessSettings; + } + } + } + + [ConsoleVariable("r_shadows")] + public static string SceneShadows + { + get + { + return (bool)AssetManager.Globals.GetValue("Scene Shadows") ? "1" : "0"; + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + AssetManager.Globals.SetValue("Scene Shadows", boolValue); + } + } + + [ConsoleVariable("r_staticbatch")] + public static string StaticBatch + { + get + { + return (bool)AssetManager.Globals.GetValue("Static Batching") ? "1" : "0"; + } + set + { + bool boolValue = false; + if (int.TryParse(value, out int intValue)) + boolValue = intValue != 0; + else if (float.TryParse(value, out float valueFloat)) + boolValue = valueFloat != 0f; + + AssetManager.Globals.SetValue("Static Batching", boolValue); + } + } + + [ConsoleCommand("playdemo")] + public static void PlayDemoCommand(string[] text) + { + string demoName; + if (text.Length < 1) + demoName = "638201307621505588_v2";//return; + else + demoName = text[0]; + + if (!demoName.EndsWith(".gdem")) + demoName += ".gdem"; + + string demoPath = Path.Combine(AssetManager.DemoPath, demoName); + NetworkManager.PlayDemo(demoPath); +#if false + PlayerActor playerActor = Level.GetActors().First(/*x => + x.GetScript().PlayerId == NetworkManager.LocalPlayerClientId*/); + + string demoPath = Path.Combine(AssetManager.DemoPath, demoName); + if (File.Exists(demoPath)) + playerActor.GetScript().SetInput(demoPath); +#endif + } + + private static Action timeDemoUpdate = null; + private static void TimeDemoOnUpdate() + { + if (timeDemoUpdate != null) + timeDemoUpdate(); + } + + [ConsoleCommand("timedemo")] + public static void TimeDemoCommand(string[] text) + { + string demoName; + if (text.Length < 1) + demoName = "638201307621505588_v2";//return; + else + demoName = text[0]; + + if (!demoName.EndsWith(".gdem")) + demoName += ".gdem"; + + string demoPath = Path.Combine(AssetManager.DemoPath, demoName); + NetworkManager.PlayDemo(demoPath); + + float oldPhysicsFps = Time.PhysicsFPS; + float oldFps = Time.UpdateFPS; + Time.PhysicsFPS = 0f; + Time.UpdateFPS = 0f; + Time.DrawFPS = 0f; + + float accumTime = 0f; + int accumTimes = 0; + timeDemoUpdate = () => + { + if (!NetworkManager.IsDemoPlaying) + { + Console.Print($"timedemo ended, time: {accumTimes} frames, avg: {(accumTime/(float)accumTimes)*1000.0f}"); + timeDemoUpdate = null; + Time.PhysicsFPS = oldPhysicsFps; + Time.UpdateFPS = oldFps; + Time.DrawFPS = oldFps; + return; + } + + if (accumTimes == 0) + Console.Print($"timedemo started"); + + accumTime += Time.DeltaTime; + accumTimes++; + + }; + + Scripting.Update -= TimeDemoOnUpdate; + Scripting.Update += TimeDemoOnUpdate; + } + + [ConsoleCommand("timedemo2")] + public static void TimeDemo2Command(string[] text) + { + string demoName; + if (text.Length < 1) + demoName = "638201307621505588_v2";//return; + else + demoName = text[0]; + + if (!demoName.EndsWith(".gdem")) + demoName += ".gdem"; + + string demoPath = Path.Combine(AssetManager.DemoPath, demoName); + NetworkManager.PlayDemo(demoPath); + + float oldPhysicsFps = Time.PhysicsFPS; + float oldFps = Time.UpdateFPS; + Time.PhysicsFPS = 0f; + Time.UpdateFPS = 5f; + Time.DrawFPS = 5f; + + float accumTime = 0f; + int accumTimes = 0; + timeDemoUpdate = () => + { + if (!NetworkManager.IsDemoPlaying) + { + Console.Print($"timedemo ended, time: {accumTimes} frames, avg: {(accumTime/(float)accumTimes)*1000.0f}"); + timeDemoUpdate = null; + Time.PhysicsFPS = oldPhysicsFps; + Time.UpdateFPS = oldFps; + Time.DrawFPS = oldFps; + return; + } + + if (accumTimes == 0) + Console.Print($"timedemo started"); + + accumTime += Time.DeltaTime; + accumTimes++; + + }; + + Scripting.FixedUpdate -= TimeDemoOnUpdate; + Scripting.FixedUpdate += TimeDemoOnUpdate; + } + + [ConsoleVariable("net_fakelag")] + public static string NetFakeLag + { + get + { + var driver = NetworkManager.server != null ? (NetworkManager.ServerNetworkDriver as NetworkLagDriver) : (NetworkManager.ClientNetworkDriver as NetworkLagDriver); + if (driver == null) + return 0.ToString(); + return ((int)driver.Lag).ToString(); + } + set + { + var driver = NetworkManager.server != null ? (NetworkManager.ServerNetworkDriver as NetworkLagDriver) : (NetworkManager.ClientNetworkDriver as NetworkLagDriver); + if (driver == null) + return; + + int intValue = 0; + if (int.TryParse(value, out intValue)) + { } + else if (float.TryParse(value, out float valueFloat)) + intValue = (int)valueFloat; + intValue = Math.Clamp(intValue, 0, 2000); + + driver.Lag = intValue; + } + } + + [ConsoleCommand("map")] + public static void MapCommand() + { + //NetworkManager.StartServer(true); + NetworkManager.StartServer(); + } + + [ConsoleCommand("connect")] + public static void ConnectCommand() + { + //GameMode.Connect(); + NetworkManager.ConnectServer(); + } + + [ConsoleSubsystemInitializer] + public static void Initialize() + { + } + + [ConsoleCommand("quit", "exit")] + public static void ExitCommand() + { + Engine.RequestExit(); + } + + [ConsoleCommand("debuglog")] + public static void DebugLogCommand(string[] text) + { + Debug.Log(string.Join(" ", text)); } } \ No newline at end of file diff --git a/Source/Game/EngineUtilities.cs b/Source/Game/EngineUtilities.cs index e1bfda4..ea0afe8 100644 --- a/Source/Game/EngineUtilities.cs +++ b/Source/Game/EngineUtilities.cs @@ -1,26 +1,25 @@ using System; using FlaxEngine; -namespace Game +namespace Game; + +public static class Utilities { - public static class Utilities + public static ScopeProfiler ProfileScope(string eventName) { - public static ScopeProfiler ProfileScope(string eventName) + return new ScopeProfiler(eventName); + } + + public class ScopeProfiler : IDisposable + { + public ScopeProfiler(string eventName) { - return new ScopeProfiler(eventName); + Profiler.BeginEvent(eventName); } - public class ScopeProfiler : IDisposable + public void Dispose() { - public ScopeProfiler(string eventName) - { - Profiler.BeginEvent(eventName); - } - - public void Dispose() - { - Profiler.EndEvent(); - } + Profiler.EndEvent(); } } } \ No newline at end of file diff --git a/Source/Game/GameMode/NetworkManager.cs b/Source/Game/GameMode/NetworkManager.cs index e4f8693..9c63617 100644 --- a/Source/Game/GameMode/NetworkManager.cs +++ b/Source/Game/GameMode/NetworkManager.cs @@ -7,199 +7,198 @@ using FlaxEngine.Assertions; using FlaxEngine.Networking; using Console = Game.Console; -namespace Game +namespace Game; + +[AttributeUsage(AttributeTargets.Class)] +public class NetworkPredictedAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public class NetworkPredictedAttribute : Attribute +} + +// TODO: insert code to update variables with this attribute? +// rename to NetworkReplicatedAttribute? +[AttributeUsage(AttributeTargets.Class)] +public class NetworkedAttribute : Attribute +{ +} + +// NetworkMulticastAttribute: calls methods marked with this in all clients + +public enum NetworkMessageType : byte +{ + Handshake = 1, + Message +} + + +public static partial class NetworkManager +{ + public delegate bool OnMessageDecl(ref NetworkEvent networkEvent); + + private static bool initialized; + + public static NetworkPeer server; + public static NetworkPeer client; + + private static readonly ushort ServerPort = 59183; + private static string ServerAddress; + private static readonly ushort MTU = 1500; + private static readonly ushort MaximumClients = 32; + + private static List OnClientMessageDelegates = new(3); + private static List OnServerMessageDelegates = new(3); + + public static void RegisterClientMessageCallback(OnMessageDecl deleg) { + Assert.IsTrue(!OnClientMessageDelegates.Contains(deleg)); + OnClientMessageDelegates.Add(deleg); + } + public static void UnregisterClientMessageCallback(OnMessageDecl deleg) + { + Assert.IsTrue(OnClientMessageDelegates.Contains(deleg)); + OnClientMessageDelegates.Remove(deleg); } - // TODO: insert code to update variables with this attribute? - // rename to NetworkReplicatedAttribute? - [AttributeUsage(AttributeTargets.Class)] - public class NetworkedAttribute : Attribute + public static void RegisterServerMessageCallback(OnMessageDecl deleg) { + Assert.IsTrue(!OnServerMessageDelegates.Contains(deleg)); + OnServerMessageDelegates.Add(deleg); + } + public static void UnregisterServerMessageCallback(OnMessageDecl deleg) + { + Assert.IsTrue(OnServerMessageDelegates.Contains(deleg)); + OnServerMessageDelegates.Remove(deleg); } - // NetworkMulticastAttribute: calls methods marked with this in all clients + public static string DebugLastHandledMessage = ""; - public enum NetworkMessageType : byte + //public static bool IsServer = false; + //public static bool IsClient = false; + // static bool IsLocalClient = false; // Context dependant, true when message is handled by local client + + public static void Init() { - Handshake = 1, - Message - } + if (initialized) + return; - - public static partial class NetworkManager - { - public delegate bool OnMessageDecl(ref NetworkEvent networkEvent); - - private static bool initialized; - - public static NetworkPeer server; - public static NetworkPeer client; - - private static readonly ushort ServerPort = 59183; - private static string ServerAddress; - private static readonly ushort MTU = 1500; - private static readonly ushort MaximumClients = 32; - - private static List OnClientMessageDelegates = new(3); - private static List OnServerMessageDelegates = new(3); - - public static void RegisterClientMessageCallback(OnMessageDecl deleg) + /*if (Engine.CommandLine.Contains("-server")) { - Assert.IsTrue(!OnClientMessageDelegates.Contains(deleg)); - OnClientMessageDelegates.Add(deleg); + StartServer(); + ServerAddress = "localhost"; + ConnectServer(); } - public static void UnregisterClientMessageCallback(OnMessageDecl deleg) + else if (Engine.CommandLine.Contains("-client")) { - Assert.IsTrue(OnClientMessageDelegates.Contains(deleg)); - OnClientMessageDelegates.Remove(deleg); + ServerAddress = "localhost"; + ConnectServer(); } - - public static void RegisterServerMessageCallback(OnMessageDecl deleg) - { - Assert.IsTrue(!OnServerMessageDelegates.Contains(deleg)); - OnServerMessageDelegates.Add(deleg); - } - public static void UnregisterServerMessageCallback(OnMessageDecl deleg) - { - Assert.IsTrue(OnServerMessageDelegates.Contains(deleg)); - OnServerMessageDelegates.Remove(deleg); - } - - public static string DebugLastHandledMessage = ""; - - //public static bool IsServer = false; - //public static bool IsClient = false; - // static bool IsLocalClient = false; // Context dependant, true when message is handled by local client - - public static void Init() - { - if (initialized) - return; - - /*if (Engine.CommandLine.Contains("-server")) - { - StartServer(); - ServerAddress = "localhost"; - ConnectServer(); - } - else if (Engine.CommandLine.Contains("-client")) - { - ServerAddress = "localhost"; - ConnectServer(); - } //#if FLAX_EDITOR - else - { - StartServer(); - ServerAddress = "localhost"; - ConnectServer(); - }*/ + else + { + StartServer(); + ServerAddress = "localhost"; + ConnectServer(); + }*/ //#endif - initialized = true; + initialized = true; #if FLAX_EDITOR - Editor.Instance.PlayModeEnd += Cleanup; + Editor.Instance.PlayModeEnd += Cleanup; #endif - //WorldStateManager.Init(); // FIXME + //WorldStateManager.Init(); // FIXME + } + + public static void Cleanup() + { + Scripting.Exit -= Cleanup; + Scripting.FixedUpdate -= OnDemoUpdate; + Scripting.FixedUpdate -= OnServerNetworkUpdate; + Level.ActorSpawned -= OnServerActorSpawned; + Scripting.FixedUpdate -= OnClientNetworkUpdate; + Level.ActorSpawned -= OnClientActorSpawned; + + if (server != null) + { + NetworkPeer.ShutdownPeer(server); + server = null; } - public static void Cleanup() + if (client != null) { - Scripting.Exit -= Cleanup; - Scripting.FixedUpdate -= OnDemoUpdate; - Scripting.FixedUpdate -= OnServerNetworkUpdate; - Level.ActorSpawned -= OnServerActorSpawned; - Scripting.FixedUpdate -= OnClientNetworkUpdate; - Level.ActorSpawned -= OnClientActorSpawned; + NetworkPeer.ShutdownPeer(client); + client = null; + } - if (server != null) - { - NetworkPeer.ShutdownPeer(server); - server = null; - } - - if (client != null) - { - NetworkPeer.ShutdownPeer(client); - client = null; - } - - LocalPlayerClientId = 0; + LocalPlayerClientId = 0; #if FLAX_EDITOR - Editor.Instance.PlayModeEnd -= Cleanup; - //GameModeManager.Cleanup(); // FIXME + Editor.Instance.PlayModeEnd -= Cleanup; + //GameModeManager.Cleanup(); // FIXME #endif - if (clientWorldStateManager != null) - { - clientWorldStateManager.Cleanup(); - clientWorldStateManager = null; - } - if (serverWorldStateManager != null) - { - serverWorldStateManager.Cleanup(); - serverWorldStateManager = null; - } - - StopRecording(); - - initialized = false; + if (clientWorldStateManager != null) + { + clientWorldStateManager.Cleanup(); + clientWorldStateManager = null; + } + if (serverWorldStateManager != null) + { + serverWorldStateManager.Cleanup(); + serverWorldStateManager = null; } - private static void OnNetworkMessage(ref NetworkEvent networkEvent, Span funcs) + StopRecording(); + + initialized = false; + } + + private static void OnNetworkMessage(ref NetworkEvent networkEvent, Span funcs) + { + byte messageTypeByte = networkEvent.Message.ReadByte(); + if (!Enum.IsDefined(typeof(NetworkMessageType), messageTypeByte)) { - byte messageTypeByte = networkEvent.Message.ReadByte(); - if (!Enum.IsDefined(typeof(NetworkMessageType), messageTypeByte)) + Console.PrintError($"Unsupported message type received from client: {messageTypeByte}"); + return; + } + + NetworkMessageType messageType = (NetworkMessageType)messageTypeByte; + + switch (messageType) + { + case NetworkMessageType.Handshake: { - Console.PrintError($"Unsupported message type received from client: {messageTypeByte}"); - return; + string message = networkEvent.Message.ReadString(); + Console.Print($"Received handshake from {networkEvent.Sender.ConnectionId}, msg: " + message); + break; } - - NetworkMessageType messageType = (NetworkMessageType)messageTypeByte; - - switch (messageType) + case NetworkMessageType.Message: { - case NetworkMessageType.Handshake: + var messageStartPosition = networkEvent.Message.Position; + foreach (var func in funcs) { - string message = networkEvent.Message.ReadString(); - Console.Print($"Received handshake from {networkEvent.Sender.ConnectionId}, msg: " + message); - break; + networkEvent.Message.Position = messageStartPosition; + bool handled = func(ref networkEvent); + if (handled) + break; } - case NetworkMessageType.Message: - { - var messageStartPosition = networkEvent.Message.Position; - foreach (var func in funcs) + + //if (OnMessage != null) + /*{ + // GetInvocationList() allocates, use allocation-free but unsafe way to iterate + // + //foreach (OnMessageDecl func in OnMessage.GetInvocationList() + // .Cast().ToArray()) + foreach (OnMessageDecl func in OnMessageDelegates) { - networkEvent.Message.Position = messageStartPosition; - bool handled = func(ref networkEvent); - if (handled) + bool ret = func.Invoke(ref networkEvent); + if (ret) break; } - - //if (OnMessage != null) - /*{ - // GetInvocationList() allocates, use allocation-free but unsafe way to iterate - // - //foreach (OnMessageDecl func in OnMessage.GetInvocationList() - // .Cast().ToArray()) - foreach (OnMessageDecl func in OnMessageDelegates) - { - bool ret = func.Invoke(ref networkEvent); - if (ret) - break; - } - }*/ - break; - } - default: - Console.PrintError($"Unsupported message type received from client: {messageTypeByte}"); - break; + }*/ + break; } + default: + Console.PrintError($"Unsupported message type received from client: {messageTypeByte}"); + break; } } } \ No newline at end of file diff --git a/Source/Game/GameMode/NetworkManager_Client.cs b/Source/Game/GameMode/NetworkManager_Client.cs index 2938874..bc5c32e 100644 --- a/Source/Game/GameMode/NetworkManager_Client.cs +++ b/Source/Game/GameMode/NetworkManager_Client.cs @@ -6,153 +6,152 @@ using FlaxEngine.Networking; using Console = Game.Console; using Object = FlaxEngine.Object; -namespace Game +namespace Game; + +public static partial class NetworkManager { - public static partial class NetworkManager + public static uint LocalPlayerClientId { get; /*private*/ set; } + + public static INetworkDriver ClientNetworkDriver { get; set; } + + public static WorldStateManager clientWorldStateManager = null; + + public static bool ConnectServer(string serverAddress = "localhost", bool listenServer = false) { - public static uint LocalPlayerClientId { get; /*private*/ set; } - - public static INetworkDriver ClientNetworkDriver { get; set; } - - public static WorldStateManager clientWorldStateManager = null; - - public static bool ConnectServer(string serverAddress = "localhost", bool listenServer = false) + if (!listenServer) { - if (!listenServer) - { - Cleanup(); - //WorldStateManager.Init(); - clientWorldStateManager = new WorldStateManager(isClient: true); - } - else - { - clientWorldStateManager = new WorldStateManager(isLocalClient: true); - } - ServerAddress = serverAddress; + Cleanup(); + //WorldStateManager.Init(); + clientWorldStateManager = new WorldStateManager(isClient: true); + } + else + { + clientWorldStateManager = new WorldStateManager(isLocalClient: true); + } + ServerAddress = serverAddress; - //var driver = Object.New(typeof(ENetDriver)); - //ClientNetworkDriver = null; - NetworkLagDriver driver = Object.New(); - driver.Lag = 0f; - ClientNetworkDriver = driver; + //var driver = Object.New(typeof(ENetDriver)); + //ClientNetworkDriver = null; + NetworkLagDriver driver = Object.New(); + driver.Lag = 0f; + ClientNetworkDriver = driver; + + + client = NetworkPeer.CreatePeer(new NetworkConfig + { + NetworkDriver = driver, + ConnectionsLimit = MaximumClients, + MessagePoolSize = 2048, + MessageSize = MTU, + Address = ServerAddress == "localhost" ? "127.0.0.1" : ServerAddress, + Port = ServerPort + }); + if (client == null) + { + Console.Print("Failed to create NetworkPeer."); + return false; + } + if (!client.Connect()) + { + Console.Print("Failed to connect to the server."); + return false; + } + + Scripting.FixedUpdate += OnClientNetworkUpdate; + if (!listenServer) + { + Scripting.Exit += Cleanup; + //Level.ActorSpawned += OnClientActorSpawned; + } + + string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}_v2.gdem"); + RecordDemo(demoPath); + + return true; + } + + private static void OnClientNetworkUpdate() + { + using var _ = Utilities.ProfileScope("NetworkManager_OnClientNetworkUpdate"); + + while (client.PopEvent(out NetworkEvent networkEvent)) + { + RecordMessage(ref networkEvent); + + try + { + //IsLocalClient = server != null; + //IsClient = true; + OnClientReadMessage(ref networkEvent); + } + finally + { + //IsLocalClient = false; + //IsClient = false; + if (networkEvent.EventType == NetworkEventType.Message) + client.RecycleMessage(networkEvent.Message); + } - - client = NetworkPeer.CreatePeer(new NetworkConfig - { - NetworkDriver = driver, - ConnectionsLimit = MaximumClients, - MessagePoolSize = 2048, - MessageSize = MTU, - Address = ServerAddress == "localhost" ? "127.0.0.1" : ServerAddress, - Port = ServerPort - }); - if (client == null) - { - Console.Print("Failed to create NetworkPeer."); - return false; - } - if (!client.Connect()) - { - Console.Print("Failed to connect to the server."); - return false; - } - - Scripting.FixedUpdate += OnClientNetworkUpdate; - if (!listenServer) - { - Scripting.Exit += Cleanup; - //Level.ActorSpawned += OnClientActorSpawned; - } - - string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}_v2.gdem"); - RecordDemo(demoPath); - - return true; - } - - private static void OnClientNetworkUpdate() - { - using var _ = Utilities.ProfileScope("NetworkManager_OnClientNetworkUpdate"); - - while (client.PopEvent(out NetworkEvent networkEvent)) - { - RecordMessage(ref networkEvent); - - try - { - //IsLocalClient = server != null; - //IsClient = true; - OnClientReadMessage(ref networkEvent); - } - finally - { - //IsLocalClient = false; - //IsClient = false; - if (networkEvent.EventType == NetworkEventType.Message) - client.RecycleMessage(networkEvent.Message); - } - - } - } - - private static void OnClientReadMessage(ref NetworkEvent networkEvent) - { - using var _ = Utilities.ProfileScope("NetworkManager_OnClientReadMessage"); - - switch (networkEvent.EventType) - { - case NetworkEventType.Connected: - { - LocalPlayerClientId = networkEvent.Sender.ConnectionId; - Console.Print("Connected to server, ConnectionId: " + networkEvent.Sender.ConnectionId); - break; - } - case NetworkEventType.Disconnected: - { - Console.Print("Disconnected from server, timeout."); - LocalPlayerClientId = 0; - break; - } - case NetworkEventType.Timeout: - { - Console.Print("Disconnected from server, connection closed."); - LocalPlayerClientId = 0; - break; - } - case NetworkEventType.Message: - { - OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnClientMessageDelegates)); - - if (networkEvent.Message.Position > 0 && - networkEvent.Message.Position < networkEvent.Message.Length) - { - string err = - $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.\n"; - - networkEvent.Message.Position = 0; - byte[] messageBytes = new byte[networkEvent.Message.Length]; - unsafe - { - fixed (byte* messageBytePtr = &messageBytes[0]) - networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); - } - - //string messageBytesStr = string.Join(", ", messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); - string messageBytesStr = string.Join("", messageBytes.Select(x => ((int)x).ToString("X2"))); - - Console.PrintError(err + $"Message dump: {messageBytesStr}"); - } - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - DebugLastHandledMessage = ""; - } - - private static void OnClientActorSpawned(Actor actor) - { } } + + private static void OnClientReadMessage(ref NetworkEvent networkEvent) + { + using var _ = Utilities.ProfileScope("NetworkManager_OnClientReadMessage"); + + switch (networkEvent.EventType) + { + case NetworkEventType.Connected: + { + LocalPlayerClientId = networkEvent.Sender.ConnectionId; + Console.Print("Connected to server, ConnectionId: " + networkEvent.Sender.ConnectionId); + break; + } + case NetworkEventType.Disconnected: + { + Console.Print("Disconnected from server, timeout."); + LocalPlayerClientId = 0; + break; + } + case NetworkEventType.Timeout: + { + Console.Print("Disconnected from server, connection closed."); + LocalPlayerClientId = 0; + break; + } + case NetworkEventType.Message: + { + OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnClientMessageDelegates)); + + if (networkEvent.Message.Position > 0 && + networkEvent.Message.Position < networkEvent.Message.Length) + { + string err = + $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}.\n"; + + networkEvent.Message.Position = 0; + byte[] messageBytes = new byte[networkEvent.Message.Length]; + unsafe + { + fixed (byte* messageBytePtr = &messageBytes[0]) + networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); + } + + //string messageBytesStr = string.Join(", ", messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); + string messageBytesStr = string.Join("", messageBytes.Select(x => ((int)x).ToString("X2"))); + + Console.PrintError(err + $"Message dump: {messageBytesStr}"); + } + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + DebugLastHandledMessage = ""; + } + + private static void OnClientActorSpawned(Actor actor) + { + } } \ No newline at end of file diff --git a/Source/Game/GameMode/NetworkManager_Demo.cs b/Source/Game/GameMode/NetworkManager_Demo.cs index 25f6ebc..642e55e 100644 --- a/Source/Game/GameMode/NetworkManager_Demo.cs +++ b/Source/Game/GameMode/NetworkManager_Demo.cs @@ -11,165 +11,195 @@ using FlaxEngine.Networking; using Console = Game.Console; using Object = FlaxEngine.Object; -namespace Game +namespace Game; + +public static unsafe partial class NetworkManager { - public static unsafe partial class NetworkManager + public const byte DemoVer = 2; + private struct DemoNetworkEventHeader + { + public NetworkEventType EventType; + public uint SenderId; + public uint MessageId; + public uint Length; + } + + private static List buffer; + private static IEnumerator bufferEnumerable; + private static GZipStream demoStream; // record + private static FileStream demoFileStream; + + public static bool IsRecording => demoStream != null; + private static long flushedFrames = 0; + + public static bool IsDemoPlaying => bufferEnumerable != null; + + public static bool PlayDemo(string demoName) { - public const byte DemoVer = 2; - private struct DemoNetworkEventHeader { - public NetworkEventType EventType; - public uint SenderId; - public uint MessageId; - public uint Length; + Cleanup(); + clientWorldStateManager = new WorldStateManager(isServer: true); } - private static List buffer; - private static IEnumerator bufferEnumerable; - private static GZipStream demoStream; // record - private static FileStream demoFileStream; - - public static bool IsRecording => demoStream != null; - private static long flushedFrames = 0; - - public static bool IsDemoPlaying => bufferEnumerable != null; - - public static bool PlayDemo(string demoName) + if (!ReadDemo(demoName)) { - { - Cleanup(); - clientWorldStateManager = new WorldStateManager(isServer: true); - } + Console.Print("Failed to read demo."); + return false; + } + + /*if (client == null) + { + Console.Print("Failed to create NetworkPeer."); + return false; + } + if (!client.Connect()) + { + Console.Print("Failed to connect to the server."); + return false; + }*/ - if (!ReadDemo(demoName)) + Scripting.FixedUpdate += OnDemoUpdate; + //if (!listenServer) + { + Scripting.Exit += Cleanup; + //Level.ActorSpawned += OnClientActorSpawned; + } + return true; + } + + public static void RecordDemo(string demoPath) + { + if (IsRecording) + StopRecording(); + + buffer = new List(); + var demoFolder = Directory.GetParent(demoPath); + if (!demoFolder.Exists) + Directory.CreateDirectory(demoFolder.FullName); + + demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); + demoStream = new GZipStream(demoFileStream, CompressionMode.Compress); + demoStream.WriteByte(DemoVer); + } + + private static void RecordMessage(ref NetworkEvent networkEvent) + { + if (!IsRecording) + return; + + NetworkEvent recordedEvent = networkEvent; + recordedEvent.Message.Buffer = (byte*)NativeMemory.Alloc(recordedEvent.Message.Length); + recordedEvent.Sender.ConnectionId = networkEvent.Sender.ConnectionId; + + NativeMemory.Copy(networkEvent.Message.Buffer, recordedEvent.Message.Buffer, recordedEvent.Message.Length); + buffer.Add(recordedEvent); + } + + public static void FlushDemo() + { + if (!IsRecording) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + Span bytes = stackalloc byte[Unsafe.SizeOf()]; + foreach (ref NetworkEvent networkEvent in CollectionsMarshal.AsSpan(buffer)) + { + DemoNetworkEventHeader header = new DemoNetworkEventHeader() { - Console.Print("Failed to read demo."); - return false; - } + EventType = networkEvent.EventType, + SenderId = networkEvent.Sender.ConnectionId, + Length = networkEvent.Message.Length, + MessageId = networkEvent.Message.MessageId, + }; + MemoryMarshal.Write(bytes, header); + demoStream.Write(bytes); + + Span messageBytes = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.Length); + demoStream.Write(messageBytes); - /*if (client == null) - { - Console.Print("Failed to create NetworkPeer."); - return false; - } - if (!client.Connect()) - { - Console.Print("Failed to connect to the server."); - return false; - }*/ - - Scripting.FixedUpdate += OnDemoUpdate; - //if (!listenServer) - { - Scripting.Exit += Cleanup; - //Level.ActorSpawned += OnClientActorSpawned; - } - return true; + // TODO: free networkEvent buffer } - public static void RecordDemo(string demoPath) + sw.Stop(); + + flushedFrames += buffer.Count; + buffer.Clear(); + + FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); + } + + public static void StopRecording() + { + if (!IsRecording) + return; + + FlushDemo(); + demoStream.Close(); + demoStream = null; + demoFileStream.Close(); + demoFileStream = null; + } + + private static unsafe bool ReadDemo(string demoPath) + { + if (!File.Exists(demoPath)) + return false; + + buffer = new List(); + + Stopwatch sw = Stopwatch.StartNew(); + + using FileStream fileStream = File.OpenRead(demoPath); + using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress); + + int ver = stream.ReadByte(); + if (ver != DemoVer) { - if (IsRecording) - StopRecording(); - - buffer = new List(); - var demoFolder = Directory.GetParent(demoPath); - if (!demoFolder.Exists) - Directory.CreateDirectory(demoFolder.FullName); - - demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); - demoStream = new GZipStream(demoFileStream, CompressionMode.Compress); - demoStream.WriteByte(DemoVer); + Console.Print($"Demo version mismatch, expected {DemoVer}, got {ver}"); + stream.Close(); + return false; } - private static void RecordMessage(ref NetworkEvent networkEvent) + int headerSize = Unsafe.SizeOf(); + Span headerBuffer = stackalloc byte[headerSize]; + while (true) { - if (!IsRecording) - return; - - NetworkEvent recordedEvent = networkEvent; - recordedEvent.Message.Buffer = (byte*)NativeMemory.Alloc(recordedEvent.Message.Length); - recordedEvent.Sender.ConnectionId = networkEvent.Sender.ConnectionId; - - NativeMemory.Copy(networkEvent.Message.Buffer, recordedEvent.Message.Buffer, recordedEvent.Message.Length); - buffer.Add(recordedEvent); - } - - public static void FlushDemo() - { - if (!IsRecording) - return; - - Stopwatch sw = Stopwatch.StartNew(); - - Span bytes = stackalloc byte[Unsafe.SizeOf()]; - foreach (ref NetworkEvent networkEvent in CollectionsMarshal.AsSpan(buffer)) { - DemoNetworkEventHeader header = new DemoNetworkEventHeader() + int bytesLeftInBuffer = headerSize; + do { - EventType = networkEvent.EventType, - SenderId = networkEvent.Sender.ConnectionId, - Length = networkEvent.Message.Length, - MessageId = networkEvent.Message.MessageId, - }; - MemoryMarshal.Write(bytes, header); - demoStream.Write(bytes); + int readBytes = stream.Read(headerBuffer.Slice(headerSize - bytesLeftInBuffer, bytesLeftInBuffer)); + if (readBytes == 0) + break; + bytesLeftInBuffer -= readBytes; + } while (bytesLeftInBuffer > 0); - Span messageBytes = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.Length); - demoStream.Write(messageBytes); - - // TODO: free networkEvent buffer + if (bytesLeftInBuffer > 0) + break; // EOF; } - sw.Stop(); - - flushedFrames += buffer.Count; - buffer.Clear(); + DemoNetworkEventHeader header = MemoryMarshal.Read(headerBuffer); - FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); - } + //buffer.Add(new NetworkEvent()); + //ref NetworkEvent networkEvent = ref buffer[buffer.Length]; // collectionmarshal + NetworkEvent networkEvent = new NetworkEvent(); + networkEvent.EventType = header.EventType; + networkEvent.Message.Position = 0; + networkEvent.Message.MessageId = header.MessageId; + networkEvent.Message.Length = networkEvent.Message.BufferSize = header.Length; + networkEvent.Sender.ConnectionId = header.SenderId; - public static void StopRecording() - { - if (!IsRecording) - return; - - FlushDemo(); - demoStream.Close(); - demoStream = null; - demoFileStream.Close(); - demoFileStream = null; - } - - private static unsafe bool ReadDemo(string demoPath) - { - if (!File.Exists(demoPath)) - return false; - - buffer = new List(); - - Stopwatch sw = Stopwatch.StartNew(); - - using FileStream fileStream = File.OpenRead(demoPath); - using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress); - - int ver = stream.ReadByte(); - if (ver != DemoVer) + if (header.Length > 0) { - Console.Print($"Demo version mismatch, expected {DemoVer}, got {ver}"); - stream.Close(); - return false; - } + networkEvent.Message.Buffer = (byte*)NativeMemory.Alloc(header.Length); - int headerSize = Unsafe.SizeOf(); - Span headerBuffer = stackalloc byte[headerSize]; - while (true) - { + Span messageBufferSpan = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.BufferSize); { - int bytesLeftInBuffer = headerSize; + int bytesLeftInBuffer = (int)header.Length; do { - int readBytes = stream.Read(headerBuffer.Slice(headerSize - bytesLeftInBuffer, bytesLeftInBuffer)); + int readBytes = stream.Read(messageBufferSpan.Slice((int)header.Length - bytesLeftInBuffer, bytesLeftInBuffer)); if (readBytes == 0) break; bytesLeftInBuffer -= readBytes; @@ -178,101 +208,70 @@ namespace Game if (bytesLeftInBuffer > 0) break; // EOF; } - - DemoNetworkEventHeader header = MemoryMarshal.Read(headerBuffer); - - //buffer.Add(new NetworkEvent()); - //ref NetworkEvent networkEvent = ref buffer[buffer.Length]; // collectionmarshal - NetworkEvent networkEvent = new NetworkEvent(); - networkEvent.EventType = header.EventType; - networkEvent.Message.Position = 0; - networkEvent.Message.MessageId = header.MessageId; - networkEvent.Message.Length = networkEvent.Message.BufferSize = header.Length; - networkEvent.Sender.ConnectionId = header.SenderId; - - if (header.Length > 0) - { - networkEvent.Message.Buffer = (byte*)NativeMemory.Alloc(header.Length); - - Span messageBufferSpan = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.BufferSize); - { - int bytesLeftInBuffer = (int)header.Length; - do - { - int readBytes = stream.Read(messageBufferSpan.Slice((int)header.Length - bytesLeftInBuffer, bytesLeftInBuffer)); - if (readBytes == 0) - break; - bytesLeftInBuffer -= readBytes; - } while (bytesLeftInBuffer > 0); - - if (bytesLeftInBuffer > 0) - break; // EOF; - } - } - buffer.Add(networkEvent); } - - sw.Stop(); - - bufferEnumerable = buffer.GetEnumerator(); - - Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} "); - - DemoEndFrame(); // advances to first frame - return true; + buffer.Add(networkEvent); } - private static void DemoEndFrame() + sw.Stop(); + + bufferEnumerable = buffer.GetEnumerator(); + + Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} "); + + DemoEndFrame(); // advances to first frame + return true; + } + + private static void DemoEndFrame() + { + // TODO: check if the current state frame matches the current frame number before advancing + + /*asdf++; + if (asdf < 8) + return;*/ + + if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) { - // TODO: check if the current state frame matches the current frame number before advancing - - /*asdf++; - if (asdf < 8) - return;*/ - - if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) + if (buffer.Any()) { - if (buffer.Any()) - { - bufferEnumerable.Dispose(); - bufferEnumerable = null; - buffer.Clear(); - Console.Print("Demo ended"); - } - - return; + bufferEnumerable.Dispose(); + bufferEnumerable = null; + buffer.Clear(); + Console.Print("Demo ended"); } - //var actorState = currentState.actor; - //currentState.input = bufferEnumerable.Current; - //frame++; - //currentState.actor = actorState; + return; } - private static void OnDemoUpdate() + //var actorState = currentState.actor; + //currentState.input = bufferEnumerable.Current; + //frame++; + //currentState.actor = actorState; + } + + private static void OnDemoUpdate() + { + if (!IsDemoPlaying) + return; + + using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnDemoUpdate"); + + NetworkEvent demoEvent = bufferEnumerable.Current; // ref? + + // TODO: change/randomize Sender.ConnectionId? + try { - if (!IsDemoPlaying) - return; - - using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnDemoUpdate"); - - NetworkEvent demoEvent = bufferEnumerable.Current; // ref? - - // TODO: change/randomize Sender.ConnectionId? - try - { - //IsLocalClient = server != null; - //IsClient = true; - OnClientReadMessage(ref demoEvent); - } - finally - { - //IsLocalClient = false; - //IsClient = false; - //TODO: recycle event? - } - - DemoEndFrame(); + //IsLocalClient = server != null; + //IsClient = true; + OnClientReadMessage(ref demoEvent); } + finally + { + //IsLocalClient = false; + //IsClient = false; + //TODO: recycle event? + } + + DemoEndFrame(); } } \ No newline at end of file diff --git a/Source/Game/GameMode/NetworkManager_Server.cs b/Source/Game/GameMode/NetworkManager_Server.cs index 4bc34c7..adc01f4 100644 --- a/Source/Game/GameMode/NetworkManager_Server.cs +++ b/Source/Game/GameMode/NetworkManager_Server.cs @@ -8,206 +8,205 @@ using FlaxEngine.Networking; using Console = Game.Console; using Object = FlaxEngine.Object; -namespace Game +namespace Game; + +public static partial class NetworkManager { - public static partial class NetworkManager + private static List ConnectedClients; + + private static List NetworkedTypes; + + public static INetworkDriver ServerNetworkDriver { get; set; } + + public static WorldStateManager serverWorldStateManager = null; + + public static bool StartServer(bool listenServer = true) { - private static List ConnectedClients; + Cleanup(); + ConnectedClients = new List(MaximumClients); - private static List NetworkedTypes; - public static INetworkDriver ServerNetworkDriver { get; set; } + //var driver = Object.New(typeof(ENetDriver)); + //ServerNetworkDriver = null; + NetworkLagDriver driver = Object.New(); + driver.Lag = 200f; + ServerNetworkDriver = driver; - public static WorldStateManager serverWorldStateManager = null; - - public static bool StartServer(bool listenServer = true) + server = NetworkPeer.CreatePeer(new NetworkConfig { - Cleanup(); - ConnectedClients = new List(MaximumClients); + NetworkDriver = driver, + ConnectionsLimit = MaximumClients, + MessagePoolSize = 2048, + MessageSize = MTU, + Address = "any", + Port = ServerPort + }); + if (!server.Listen()) + { + Console.PrintError("Failed to start the server."); + return false; + } + Scripting.FixedUpdate += OnServerNetworkUpdate; + Scripting.Exit += Cleanup; + Level.ActorSpawned += OnServerActorSpawned; - //var driver = Object.New(typeof(ENetDriver)); - //ServerNetworkDriver = null; - NetworkLagDriver driver = Object.New(); - driver.Lag = 200f; - ServerNetworkDriver = driver; - - server = NetworkPeer.CreatePeer(new NetworkConfig - { - NetworkDriver = driver, - ConnectionsLimit = MaximumClients, - MessagePoolSize = 2048, - MessageSize = MTU, - Address = "any", - Port = ServerPort - }); - if (!server.Listen()) - { - Console.PrintError("Failed to start the server."); - return false; - } - - Scripting.FixedUpdate += OnServerNetworkUpdate; - Scripting.Exit += Cleanup; - Level.ActorSpawned += OnServerActorSpawned; - - NetworkedTypes = new List(); + NetworkedTypes = new List(); #if false - var assemblies = Utils.GetAssemblies(); + var assemblies = Utils.GetAssemblies(); - foreach (Assembly assembly in assemblies) - { - // Skip common assemblies - string assemblyName = assembly.GetName().Name; - if (assemblyName == "System" || - assemblyName.StartsWith("System.") || - assemblyName.StartsWith("Mono.") || - assemblyName == "mscorlib" || - assemblyName == "Newtonsoft.Json" || - assemblyName == "Snippets" || - assemblyName == "netstandard" || - assemblyName == "Anonymously Hosted DynamicMethods Assembly" || - assemblyName.StartsWith("FlaxEngine.") || - assemblyName.StartsWith("JetBrains.") || - assemblyName.StartsWith("Microsoft.") || - assemblyName.StartsWith("nunit.")) - continue; + foreach (Assembly assembly in assemblies) + { + // Skip common assemblies + string assemblyName = assembly.GetName().Name; + if (assemblyName == "System" || + assemblyName.StartsWith("System.") || + assemblyName.StartsWith("Mono.") || + assemblyName == "mscorlib" || + assemblyName == "Newtonsoft.Json" || + assemblyName == "Snippets" || + assemblyName == "netstandard" || + assemblyName == "Anonymously Hosted DynamicMethods Assembly" || + assemblyName.StartsWith("FlaxEngine.") || + assemblyName.StartsWith("JetBrains.") || + assemblyName.StartsWith("Microsoft.") || + assemblyName.StartsWith("nunit.")) + continue; - foreach (Type type in assembly.GetTypes()) - if (type.GetCustomAttributes().Any(x => x is NetworkedAttribute)) - NetworkedTypes.Add(type); - } + foreach (Type type in assembly.GetTypes()) + if (type.GetCustomAttributes().Any(x => x is NetworkedAttribute)) + NetworkedTypes.Add(type); + } - foreach (Type type in NetworkedTypes) - Console.Print("tracking networked type: " + type.Name); + foreach (Type type in NetworkedTypes) + Console.Print("tracking networked type: " + type.Name); #endif - serverWorldStateManager = new WorldStateManager(isServer: true); - //WorldStateManager.Init(); + serverWorldStateManager = new WorldStateManager(isServer: true); + //WorldStateManager.Init(); - if (listenServer) - return ConnectServer(listenServer: true); - return true; - } + if (listenServer) + return ConnectServer(listenServer: true); + return true; + } - public static NetworkMessage ServerBeginSendMessage() + public static NetworkMessage ServerBeginSendMessage() + { + NetworkMessage message = server.BeginSendMessage(); + message.WriteByte((byte)NetworkMessageType.Message); + return message; + } + + public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection connection, NetworkChannelType channelType = NetworkChannelType.Reliable) + { + server.EndSendMessage(channelType, message, connection); + } + + public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection[] connections, NetworkChannelType channelType = NetworkChannelType.Reliable) + { + server.EndSendMessage(channelType, message, connections); + } + + public static NetworkMessage ClientBeginSendMessage() + { + NetworkMessage message = client.BeginSendMessage(); + message.WriteByte((byte)NetworkMessageType.Message); + return message; + } + + public static void ClientEndSendMessage(ref NetworkMessage message, NetworkChannelType channelType = NetworkChannelType.Reliable) + { + client.EndSendMessage(channelType, message); + } + + private static void OnServerNetworkUpdate() + { + using var _ = Utilities.ProfileScope("NetworkManager_OnServerNetworkUpdate"); + + while (server.PopEvent(out NetworkEvent networkEvent)) + OnServerReadMessage(ref networkEvent); + } + + private static void OnServerReadMessage(ref NetworkEvent networkEvent) + { + using var _ = Utilities.ProfileScope("NetworkManager_OnServerReadMessage"); + + switch (networkEvent.EventType) { - NetworkMessage message = server.BeginSendMessage(); - message.WriteByte((byte)NetworkMessageType.Message); - return message; - } - - public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection connection, NetworkChannelType channelType = NetworkChannelType.Reliable) - { - server.EndSendMessage(channelType, message, connection); - } - - public static void ServerEndSendMessage(ref NetworkMessage message, NetworkConnection[] connections, NetworkChannelType channelType = NetworkChannelType.Reliable) - { - server.EndSendMessage(channelType, message, connections); - } - - public static NetworkMessage ClientBeginSendMessage() - { - NetworkMessage message = client.BeginSendMessage(); - message.WriteByte((byte)NetworkMessageType.Message); - return message; - } - - public static void ClientEndSendMessage(ref NetworkMessage message, NetworkChannelType channelType = NetworkChannelType.Reliable) - { - client.EndSendMessage(channelType, message); - } - - private static void OnServerNetworkUpdate() - { - using var _ = Utilities.ProfileScope("NetworkManager_OnServerNetworkUpdate"); - - while (server.PopEvent(out NetworkEvent networkEvent)) - OnServerReadMessage(ref networkEvent); - } - - private static void OnServerReadMessage(ref NetworkEvent networkEvent) - { - using var _ = Utilities.ProfileScope("NetworkManager_OnServerReadMessage"); - - switch (networkEvent.EventType) + case NetworkEventType.Connected: { - case NetworkEventType.Connected: + Console.Print($"Client({networkEvent.Sender.ConnectionId}) is trying to connect"); + try { - Console.Print($"Client({networkEvent.Sender.ConnectionId}) is trying to connect"); - try + //IsServer = true; + if (serverWorldStateManager.OnClientConnecting(networkEvent.Sender)) { - //IsServer = true; - if (serverWorldStateManager.OnClientConnecting(networkEvent.Sender)) - { - ConnectedClients.Add(networkEvent.Sender); - Console.Print( - $"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}"); + ConnectedClients.Add(networkEvent.Sender); + Console.Print( + $"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}"); - serverWorldStateManager.OnClientConnected(networkEvent.Sender); - } - else - Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused"); + serverWorldStateManager.OnClientConnected(networkEvent.Sender); } - finally - { - //IsServer = false; - } - - break; + else + Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused"); } - case NetworkEventType.Disconnected: - case NetworkEventType.Timeout: + finally { - Console.Print($"Client({networkEvent.Sender.ConnectionId}) disconnected!"); - - ConnectedClients.Remove(networkEvent.Sender); - Console.Print("Connected clients: " + ConnectedClients.Count); - break; + //IsServer = false; } - case NetworkEventType.Message: - { - try - { - //IsServer = true; - OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnServerMessageDelegates)); - if (networkEvent.Message.Position > 0 && - networkEvent.Message.Position < networkEvent.Message.Length) - { - string err = - $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}."; - - networkEvent.Message.Position = 0; - byte[] messageBytes = new byte[networkEvent.Message.Length]; - unsafe - { - fixed (byte* messageBytePtr = &messageBytes[0]) - networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); - } - - string messageBytesStr = string.Join(", ", - messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); - - Console.PrintError(err + $"Message dump: {messageBytesStr}"); - } - } - finally - { - //IsServer = false; - server.RecycleMessage(networkEvent.Message); - } - break; - } - default: - throw new ArgumentOutOfRangeException(); + break; } - } + case NetworkEventType.Disconnected: + case NetworkEventType.Timeout: + { + Console.Print($"Client({networkEvent.Sender.ConnectionId}) disconnected!"); - private static void OnServerActorSpawned(Actor actor) - { - //Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})"); + ConnectedClients.Remove(networkEvent.Sender); + Console.Print("Connected clients: " + ConnectedClients.Count); + break; + } + case NetworkEventType.Message: + { + try + { + //IsServer = true; + OnNetworkMessage(ref networkEvent, CollectionsMarshal.AsSpan(OnServerMessageDelegates)); + + if (networkEvent.Message.Position > 0 && + networkEvent.Message.Position < networkEvent.Message.Length) + { + string err = + $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}."; + + networkEvent.Message.Position = 0; + byte[] messageBytes = new byte[networkEvent.Message.Length]; + unsafe + { + fixed (byte* messageBytePtr = &messageBytes[0]) + networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); + } + + string messageBytesStr = string.Join(", ", + messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); + + Console.PrintError(err + $"Message dump: {messageBytesStr}"); + } + } + finally + { + //IsServer = false; + server.RecycleMessage(networkEvent.Message); + } + break; + } + default: + throw new ArgumentOutOfRangeException(); } } + + private static void OnServerActorSpawned(Actor actor) + { + //Console.Print($"actor spawned: {actor.Name} ({actor.TypeName})"); + } } \ No newline at end of file diff --git a/Source/Game/GameMode/WorldStateManager.cs b/Source/Game/GameMode/WorldStateManager.cs index 97b0e06..1480128 100644 --- a/Source/Game/GameMode/WorldStateManager.cs +++ b/Source/Game/GameMode/WorldStateManager.cs @@ -10,100 +10,100 @@ using FlaxEngine.Json; using FlaxEngine.Networking; using Console = Game.Console; -namespace Game -{ - public enum GameModeMessageType : byte - { - WelcomePlayer, // WelcomePlayerMessage - SpawnPlayer, - PlayerInput, - PlayerPosition, // world update - PlayerSnapshot, // send world state delta to client since last client's acknowledged frame +namespace Game; - LastMessageType = 128, +public enum GameModeMessageType : byte +{ + WelcomePlayer, // WelcomePlayerMessage + SpawnPlayer, + PlayerInput, + PlayerPosition, // world update + PlayerSnapshot, // send world state delta to client since last client's acknowledged frame + + LastMessageType = 128, +} + + +// Manages world simulation and handles network messages +public class WorldStateManager +{ + private class WorldState + { + public ulong frame; + public List actors; + + public Dictionary playerFrameHistory; + + public WorldState() + { + actors = new List(); + playerFrameHistory = new Dictionary(); + } } - - // Manages world simulation and handles network messages - public class WorldStateManager + private class PlayerFrame { - private class WorldState - { - public ulong frame; - public List actors; + public ulong frame; + public Float3 position; + } - public Dictionary playerFrameHistory; + private Dictionary players; + private Dictionary playerConnections; + public Dictionary playerLastReceivedFrames; + private Dictionary playerLastFrame; + private bool welcomed = false; - public WorldState() - { - actors = new List(); - playerFrameHistory = new Dictionary(); - } - } + private WorldState serverWorldState; + private WorldState clientWorldState; + private GameMode gameMode; + private Scene scene; + private Actor worldSpawn; - private class PlayerFrame - { - public ulong frame; - public Float3 position; - } + private ulong lastReceivedServerFrame = 0; + public ulong ServerFrame => /*NetworkManager.server != null ? serverWorldState.frame :*/ lastReceivedServerFrame; + public ulong ClientFrame => clientWorldState.frame; + public float ServerTime = 0f; + public float ClientTime = 0f; - private Dictionary players; - private Dictionary playerConnections; - public Dictionary playerLastReceivedFrames; - private Dictionary playerLastFrame; - private bool welcomed = false; + public bool IsServer = false; + public bool IsClient = false; + public bool IsLocalClient = false; // Context dependant, true when message is handled by local client - private WorldState serverWorldState; - private WorldState clientWorldState; - private GameMode gameMode; - private Scene scene; - private Actor worldSpawn; + public WorldStateManager(bool isServer = false, bool isClient = false, bool isLocalClient = false) + { + IsServer = isServer; + IsClient = isClient; + IsLocalClient = isLocalClient; + Assert.IsTrue(isServer || isClient || isLocalClient); - private ulong lastReceivedServerFrame = 0; - public ulong ServerFrame => /*NetworkManager.server != null ? serverWorldState.frame :*/ lastReceivedServerFrame; - public ulong ClientFrame => clientWorldState.frame; - public float ServerTime = 0f; - public float ClientTime = 0f; + Init(); + } - public bool IsServer = false; - public bool IsClient = false; - public bool IsLocalClient = false; // Context dependant, true when message is handled by local client + public void Init() + { + //welcomed = false; + //lastReceivedServerFrame = 0; + ServerTime = Time.GameTime; + //ClientTime = 0f; - public WorldStateManager(bool isServer = false, bool isClient = false, bool isLocalClient = false) - { - IsServer = isServer; - IsClient = isClient; - IsLocalClient = isLocalClient; - Assert.IsTrue(isServer || isClient || isLocalClient); + players = new Dictionary(); + playerConnections = new Dictionary(); + playerLastReceivedFrames = new Dictionary(); + playerLastFrame = new Dictionary(); + serverWorldState = new WorldState(); + clientWorldState = new WorldState(); + gameMode = new GameMode(); - Init(); - } - - public void Init() - { - //welcomed = false; - //lastReceivedServerFrame = 0; - ServerTime = Time.GameTime; - //ClientTime = 0f; - - players = new Dictionary(); - playerConnections = new Dictionary(); - playerLastReceivedFrames = new Dictionary(); - playerLastFrame = new Dictionary(); - serverWorldState = new WorldState(); - clientWorldState = new WorldState(); - gameMode = new GameMode(); - - if (IsServer) - NetworkManager.RegisterServerMessageCallback(OnMessage); - else - NetworkManager.RegisterClientMessageCallback(OnMessage); + if (IsServer) + NetworkManager.RegisterServerMessageCallback(OnMessage); + else + NetworkManager.RegisterClientMessageCallback(OnMessage); - PhysicsScene localPhysicsScene = Physics.FindOrCreateScene(IsLocalClient ? "ClientPhysicsScene" : "ServerPhysicsScene"); + PhysicsScene localPhysicsScene = Physics.FindOrCreateScene(IsLocalClient ? "ClientPhysicsScene" : "ServerPhysicsScene"); - Guid sceneGuid = JsonSerializer.ParseID(IsLocalClient ? "c095f9ac4989a46afd7fe3821f086e2e" : "59dd37cc444d5d7015759389c6153c4c"); - string sceneData = $@" + Guid sceneGuid = JsonSerializer.ParseID(IsLocalClient ? "c095f9ac4989a46afd7fe3821f086e2e" : "59dd37cc444d5d7015759389c6153c4c"); + string sceneData = $@" {{ ""ID"": ""{JsonSerializer.GetStringID(sceneGuid)}"", ""TypeName"": ""FlaxEngine.SceneAsset"", @@ -126,604 +126,603 @@ namespace Game ] }}"; + { + var onSceneLoaded = (Scene loadedScene, Guid id) => { - var onSceneLoaded = (Scene loadedScene, Guid id) => + if (sceneGuid == id) { - if (sceneGuid == id) - { - loadedScene.PhysicsScene = localPhysicsScene; - //scene = loadedScene; - } - }; - - try - { - Level.SceneLoaded += onSceneLoaded; - //Level.LoadScene(new SceneReference(sceneGuid)); - scene = Level.LoadSceneFromBytes(Encoding.ASCII.GetBytes(sceneData)); + loadedScene.PhysicsScene = localPhysicsScene; + //scene = loadedScene; } - finally - { - Level.SceneLoaded -= onSceneLoaded; - } - } - Assert.IsTrue(scene); + }; - scene.Name = IsLocalClient ? "ClientScene" : "ServerScene"; - - Level.SceneLoaded += OnLevelLoaded; - //Scripting.LateUpdate += OnLateUpdatePre; - Scripting.LateFixedUpdate += OnLateUpdatePre; - - - var importer = FlaxEngine.Object.New(); - importer.mapPath = @"C:\dev\GoakeFlax\Assets\Maps\aerowalk.map"; - importer.LoadCollidersOnly = false;//IsServer; - importer.Parent = scene; - - //importer.Enabled = true; - worldSpawn = scene.FindActor("WorldSpawn");// ?? Level.FindActor("WorldSpawn"); - Assert.IsTrue(worldSpawn); - } - - public void Cleanup() - { - if (IsServer) - NetworkManager.UnregisterServerMessageCallback(OnMessage); - else - NetworkManager.UnregisterClientMessageCallback(OnMessage); - - Level.SceneLoaded -= OnLevelLoaded; - //Scripting.LateUpdate -= OnLateUpdatePre; - Scripting.LateFixedUpdate -= OnLateUpdatePre; - - if (scene) - foreach (var player in scene.GetChildren()) - { - FlaxEngine.Object.Destroy(player); - } - } - - private PlayerFrame GetPlayerFrame(uint playerIndex, ulong playerFrameIndex) - { - WorldState worldState = NetworkManager.server != null ? serverWorldState : clientWorldState; - PlayerFrame[] playerFrames = worldState.playerFrameHistory[playerIndex]; - PlayerFrame playerFrame = playerFrames[playerFrameIndex % 120]; - - if (playerFrame.frame != playerFrameIndex) - return null; - - return playerFrame; - } - - public void OnLevelLoaded(Scene scene, Guid assetGuid) - { - serverWorldState.frame = 0; - Console.Print("level loaded"); - } - - public void OnLateUpdatePre() - { try { - //NetworkManager.IsServer = NetworkManager.server != null; - //NetworkManager.IsClient = (NetworkManager.client != null || NetworkManager.IsDemoPlaying) && NetworkManager.server == null; - //NetworkManager.IsLocalClient = (NetworkManager.client != null || NetworkManager.IsDemoPlaying) && NetworkManager.server != null; - OnLateUpdate(); + Level.SceneLoaded += onSceneLoaded; + //Level.LoadScene(new SceneReference(sceneGuid)); + scene = Level.LoadSceneFromBytes(Encoding.ASCII.GetBytes(sceneData)); } finally { - //NetworkManager.IsServer = false; - //NetworkManager.IsClient = false; - //NetworkManager.IsLocalClient = false; + Level.SceneLoaded -= onSceneLoaded; } } + Assert.IsTrue(scene); - public void OnLateUpdate() + scene.Name = IsLocalClient ? "ClientScene" : "ServerScene"; + + Level.SceneLoaded += OnLevelLoaded; + //Scripting.LateUpdate += OnLateUpdatePre; + Scripting.LateFixedUpdate += OnLateUpdatePre; + + + var importer = FlaxEngine.Object.New(); + importer.mapPath = @"C:\dev\GoakeFlax\Assets\Maps\aerowalk.map"; + importer.LoadCollidersOnly = false;//IsServer; + importer.Parent = scene; + + //importer.Enabled = true; + worldSpawn = scene.FindActor("WorldSpawn");// ?? Level.FindActor("WorldSpawn"); + Assert.IsTrue(worldSpawn); + } + + public void Cleanup() + { + if (IsServer) + NetworkManager.UnregisterServerMessageCallback(OnMessage); + else + NetworkManager.UnregisterClientMessageCallback(OnMessage); + + Level.SceneLoaded -= OnLevelLoaded; + //Scripting.LateUpdate -= OnLateUpdatePre; + Scripting.LateFixedUpdate -= OnLateUpdatePre; + + if (scene) + foreach (var player in scene.GetChildren()) { - if (IsServer) - { - foreach (KeyValuePair kv in players) - { - var playerId = kv.Key; - var playerActor = kv.Value; + FlaxEngine.Object.Destroy(player); + } + } - var playerFrames = serverWorldState.playerFrameHistory[playerId]; - var playerFrame = playerFrames[serverWorldState.frame % 120]; + private PlayerFrame GetPlayerFrame(uint playerIndex, ulong playerFrameIndex) + { + WorldState worldState = NetworkManager.server != null ? serverWorldState : clientWorldState; + PlayerFrame[] playerFrames = worldState.playerFrameHistory[playerIndex]; + PlayerFrame playerFrame = playerFrames[playerFrameIndex % 120]; - playerFrame.frame = serverWorldState.frame; - //playerFrame.position = playerActor.Position; - PlayerMovement playerMovement = playerActor.GetScript(); - playerMovement.input.GetState(serverWorldState.frame, out var inputState, out var actorState); - playerFrame.position = actorState.position; - } + if (playerFrame.frame != playerFrameIndex) + return null; - foreach (KeyValuePair kv in players) - { - var otherPlayerId = kv.Key; - foreach (KeyValuePair kv2 in players) - { - //if (kv2.Key == playerId) - // continue; + return playerFrame; + } - var playerActor = kv2.Value; - var playerId = kv2.Key; + public void OnLevelLoaded(Scene scene, Guid assetGuid) + { + serverWorldState.frame = 0; + Console.Print("level loaded"); + } - // TODO: relevancy checks here etc. + public void OnLateUpdatePre() + { + try + { + //NetworkManager.IsServer = NetworkManager.server != null; + //NetworkManager.IsClient = (NetworkManager.client != null || NetworkManager.IsDemoPlaying) && NetworkManager.server == null; + //NetworkManager.IsLocalClient = (NetworkManager.client != null || NetworkManager.IsDemoPlaying) && NetworkManager.server != null; + OnLateUpdate(); + } + finally + { + //NetworkManager.IsServer = false; + //NetworkManager.IsClient = false; + //NetworkManager.IsLocalClient = false; + } + } - PlayerMovement playerMovement = playerActor.GetScript(); - //PlayerActorState actorState = playerMovement.input.GetCurrentActorState(); - //PlayerInputState inputState = playerMovement.input.GetCurrentInputState(); - var playerFrame = playerLastFrame[playerId]; - if (!playerMovement.input.GetState(playerFrame, out var inputState, out var actorState)) - Console.Print("send input failure to client"); - - { - NetworkMessage message = NetworkManager.ServerBeginSendMessage(); - message.WriteByte((byte)GameModeMessageType.PlayerPosition); - message.WriteUInt64(playerFrame); - message.WriteUInt32(kv2.Key); - message.WriteSingle(actorState.position.X); - message.WriteSingle(actorState.position.Y); - message.WriteSingle(actorState.position.Z); - message.WriteSingle(actorState.velocity.X); - message.WriteSingle(actorState.velocity.Y); - message.WriteSingle(actorState.velocity.Z); - message.WriteSingle(actorState.orientation.X); - message.WriteSingle(actorState.orientation.Y); - message.WriteSingle(actorState.orientation.Z); - message.WriteSingle(actorState.orientation.W); - message.WriteSingle(actorState.viewAngles.X); - message.WriteSingle(actorState.viewAngles.Y); - message.WriteSingle(actorState.viewAngles.Z); - message.WriteSingle(actorState.lastJumpTime); - message.WriteInt32(actorState.numJumps); - message.WriteBoolean(actorState.jumped); - - //inputState.frame - message.WriteSingle(inputState.viewDeltaX); - message.WriteSingle(inputState.viewDeltaY); - message.WriteSingle(inputState.moveForward); - message.WriteSingle(inputState.moveRight); - message.WriteBoolean(inputState.attacking); - message.WriteBoolean(inputState.jumping); - - NetworkManager.ServerEndSendMessage(ref message, playerConnections[otherPlayerId]); - } - } - } - } - - if (IsClient || IsLocalClient) - { - //if (!welcomed) - // return; - - if (welcomed) - foreach (PlayerActor playerActor in scene.GetChildren()) - { - var playerId = playerActor.PlayerId; - if (!clientWorldState.playerFrameHistory.ContainsKey(playerId)) - { - var playerFrames = new PlayerFrame[120]; - for (int j = 0; j < playerFrames.Length; j++) - playerFrames[j] = new PlayerFrame(); - clientWorldState.playerFrameHistory.Add(playerId, playerFrames); - } - var playerFrameHistory = clientWorldState.playerFrameHistory[playerId]; - var playerFrame = playerFrameHistory[ClientFrame % 120]; - - playerFrame.frame = ClientFrame; - playerFrame.position = playerActor.Position; - } - - clientWorldState.frame++; - - /*PlayerActor playerActor = scene.GetChildren().FirstOrDefault(x => - x.GetScript().PlayerId == NetworkManager.LocalPlayerClientId); - - if (playerActor == null) - return;*/ - - //var playerFrame = localPlayerFrameHistory[serverWorldState.frame % 120]; - - //playerFrame.frame = serverWorldState.frame; - //playerFrame.position = playerActor.Position; - - /*{ - NetworkMessage message = NetworkManager.ClientBeginSendMessage(); - message.WriteByte((byte)GameModeMessageType.PlayerPosition); - message.WriteUInt64(worldState.frame); - message.WriteUInt32(NetworkManager.LocalPlayerClientId); - message.WriteSingle(playerActor.Position.X); - message.WriteSingle(playerActor.Position.Y); - message.WriteSingle(playerActor.Position.Z); - NetworkManager.ClientEndSendMessage(ref message); - }*/ - } - else if (IsLocalClient) - { - - } - - serverWorldState.frame++; - - /*foreach (KeyValuePair kv in players) + public void OnLateUpdate() + { + if (IsServer) + { + foreach (KeyValuePair kv in players) { var playerId = kv.Key; var playerActor = kv.Value; - var playerConnection = playerConnections[playerId]; - if (NetworkManager.IsLocalClient && playerId == NetworkManager.LocalPlayerClientId) - continue; + var playerFrames = serverWorldState.playerFrameHistory[playerId]; + var playerFrame = playerFrames[serverWorldState.frame % 120]; - { - NetworkMessage message = NetworkManager.ServerBeginSendMessage(); - message.WriteByte((byte)GameModeMessageType.PlayerPosition); - message.WriteUInt64(0); - message.WriteSingle(playerActor.Position.X); - message.WriteSingle(playerActor.Position.Y); - message.WriteSingle(playerActor.Position.Z); - NetworkManager.ServerEndSendMessage(ref message, playerConnections[playerId]); - } - }*/ - } - - public bool OnMessageServer(ref NetworkEvent networkEvent) - { - return OnMessage(ref networkEvent); - } - - public bool OnMessageClient(ref NetworkEvent networkEvent) - { - return OnMessage(ref networkEvent /*NetworkMessage message, NetworkConnection sender*/); - } - - public bool OnMessage(ref NetworkEvent networkEvent) - { - byte messageTypeByte = networkEvent.Message.ReadByte(); - if (!Enum.IsDefined(typeof(GameModeMessageType), messageTypeByte)) - { - //Console.PrintError($"GameModeManager: Unsupported message type received from client: {messageTypeByte}"); - return false; + playerFrame.frame = serverWorldState.frame; + //playerFrame.position = playerActor.Position; + PlayerMovement playerMovement = playerActor.GetScript(); + playerMovement.input.GetState(serverWorldState.frame, out var inputState, out var actorState); + playerFrame.position = actorState.position; } - GameModeMessageType messageType = (GameModeMessageType)messageTypeByte; - NetworkManager.DebugLastHandledMessage = messageType.ToString(); - - //Console.Print($"GameModeManager: {messageType}"); - switch (messageType) + foreach (KeyValuePair kv in players) { - case GameModeMessageType.WelcomePlayer: + var otherPlayerId = kv.Key; + foreach (KeyValuePair kv2 in players) { - welcomed = true; - if (IsClient || IsLocalClient) + //if (kv2.Key == playerId) + // continue; + + var playerActor = kv2.Value; + var playerId = kv2.Key; + + // TODO: relevancy checks here etc. + + PlayerMovement playerMovement = playerActor.GetScript(); + //PlayerActorState actorState = playerMovement.input.GetCurrentActorState(); + //PlayerInputState inputState = playerMovement.input.GetCurrentInputState(); + var playerFrame = playerLastFrame[playerId]; + if (!playerMovement.input.GetState(playerFrame, out var inputState, out var actorState)) + Console.Print("send input failure to client"); + { - WelcomePlayerMessage welcomePlayer = WelcomePlayerMessage.Read(ref networkEvent.Message); - if (!IsLocalClient) - serverWorldState.frame = welcomePlayer.frame; - //lastReceivedServerFrame = welcomePlayer.frame; - clientWorldState.frame += welcomePlayer.frame; + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(playerFrame); + message.WriteUInt32(kv2.Key); + message.WriteSingle(actorState.position.X); + message.WriteSingle(actorState.position.Y); + message.WriteSingle(actorState.position.Z); + message.WriteSingle(actorState.velocity.X); + message.WriteSingle(actorState.velocity.Y); + message.WriteSingle(actorState.velocity.Z); + message.WriteSingle(actorState.orientation.X); + message.WriteSingle(actorState.orientation.Y); + message.WriteSingle(actorState.orientation.Z); + message.WriteSingle(actorState.orientation.W); + message.WriteSingle(actorState.viewAngles.X); + message.WriteSingle(actorState.viewAngles.Y); + message.WriteSingle(actorState.viewAngles.Z); + message.WriteSingle(actorState.lastJumpTime); + message.WriteInt32(actorState.numJumps); + message.WriteBoolean(actorState.jumped); - ClientTime = welcomePlayer.time; - foreach (var playerInfo in welcomePlayer.players) - { - SpawnPlayer(playerInfo.playerId, playerInfo.playerPosition, new Float3(0)); + //inputState.frame + message.WriteSingle(inputState.viewDeltaX); + message.WriteSingle(inputState.viewDeltaY); + message.WriteSingle(inputState.moveForward); + message.WriteSingle(inputState.moveRight); + message.WriteBoolean(inputState.attacking); + message.WriteBoolean(inputState.jumping); - var playerFrames = new PlayerFrame[120]; - for (int j = 0; j < playerFrames.Length; j++) - playerFrames[j] = new PlayerFrame(); - clientWorldState.playerFrameHistory.Add(playerInfo.playerId, playerFrames); - } - - Console.Print("received welcome: frame " + serverWorldState.frame); - - if (!players.ContainsKey(NetworkManager.LocalPlayerClientId)) // listen server - players.Add(NetworkManager.LocalPlayerClientId, null); - //playerConnections.Add(NetworkManager.LocalPlayerClientId, connection); + NetworkManager.ServerEndSendMessage(ref message, playerConnections[otherPlayerId]); } - break; } - case GameModeMessageType.SpawnPlayer: - { - uint playerId = networkEvent.Message.ReadUInt32(); - ulong playerFrameIndex = networkEvent.Message.ReadUInt64(); + } + } - if (!clientWorldState.playerFrameHistory.ContainsKey(playerId)) + if (IsClient || IsLocalClient) + { + //if (!welcomed) + // return; + + if (welcomed) + foreach (PlayerActor playerActor in scene.GetChildren()) + { + var playerId = playerActor.PlayerId; + if (!clientWorldState.playerFrameHistory.ContainsKey(playerId)) + { + var playerFrames = new PlayerFrame[120]; + for (int j = 0; j < playerFrames.Length; j++) + playerFrames[j] = new PlayerFrame(); + clientWorldState.playerFrameHistory.Add(playerId, playerFrames); + } + var playerFrameHistory = clientWorldState.playerFrameHistory[playerId]; + var playerFrame = playerFrameHistory[ClientFrame % 120]; + + playerFrame.frame = ClientFrame; + playerFrame.position = playerActor.Position; + } + + clientWorldState.frame++; + + /*PlayerActor playerActor = scene.GetChildren().FirstOrDefault(x => + x.GetScript().PlayerId == NetworkManager.LocalPlayerClientId); + + if (playerActor == null) + return;*/ + + //var playerFrame = localPlayerFrameHistory[serverWorldState.frame % 120]; + + //playerFrame.frame = serverWorldState.frame; + //playerFrame.position = playerActor.Position; + + /*{ + NetworkMessage message = NetworkManager.ClientBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(worldState.frame); + message.WriteUInt32(NetworkManager.LocalPlayerClientId); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + NetworkManager.ClientEndSendMessage(ref message); + }*/ + } + else if (IsLocalClient) + { + + } + + serverWorldState.frame++; + + /*foreach (KeyValuePair kv in players) + { + var playerId = kv.Key; + var playerActor = kv.Value; + var playerConnection = playerConnections[playerId]; + + if (NetworkManager.IsLocalClient && playerId == NetworkManager.LocalPlayerClientId) + continue; + + { + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(0); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + NetworkManager.ServerEndSendMessage(ref message, playerConnections[playerId]); + } + }*/ + } + + public bool OnMessageServer(ref NetworkEvent networkEvent) + { + return OnMessage(ref networkEvent); + } + + public bool OnMessageClient(ref NetworkEvent networkEvent) + { + return OnMessage(ref networkEvent /*NetworkMessage message, NetworkConnection sender*/); + } + + public bool OnMessage(ref NetworkEvent networkEvent) + { + byte messageTypeByte = networkEvent.Message.ReadByte(); + if (!Enum.IsDefined(typeof(GameModeMessageType), messageTypeByte)) + { + //Console.PrintError($"GameModeManager: Unsupported message type received from client: {messageTypeByte}"); + return false; + } + + GameModeMessageType messageType = (GameModeMessageType)messageTypeByte; + NetworkManager.DebugLastHandledMessage = messageType.ToString(); + + //Console.Print($"GameModeManager: {messageType}"); + switch (messageType) + { + case GameModeMessageType.WelcomePlayer: + { + welcomed = true; + if (IsClient || IsLocalClient) + { + WelcomePlayerMessage welcomePlayer = WelcomePlayerMessage.Read(ref networkEvent.Message); + if (!IsLocalClient) + serverWorldState.frame = welcomePlayer.frame; + //lastReceivedServerFrame = welcomePlayer.frame; + clientWorldState.frame += welcomePlayer.frame; + + ClientTime = welcomePlayer.time; + foreach (var playerInfo in welcomePlayer.players) { + SpawnPlayer(playerInfo.playerId, playerInfo.playerPosition, new Float3(0)); + var playerFrames = new PlayerFrame[120]; for (int j = 0; j < playerFrames.Length; j++) playerFrames[j] = new PlayerFrame(); - clientWorldState.playerFrameHistory.Add(playerId, playerFrames); + clientWorldState.playerFrameHistory.Add(playerInfo.playerId, playerFrames); } - SpawnPlayer(playerId, - new Float3(networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle()), - new Float3(networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle())); + Console.Print("received welcome: frame " + serverWorldState.frame); - //if (NetworkManager.IsClient) - players[playerId].GetScript().input.frame = ClientFrame; + if (!players.ContainsKey(NetworkManager.LocalPlayerClientId)) // listen server + players.Add(NetworkManager.LocalPlayerClientId, null); + //playerConnections.Add(NetworkManager.LocalPlayerClientId, connection); + } + break; + } + case GameModeMessageType.SpawnPlayer: + { + uint playerId = networkEvent.Message.ReadUInt32(); + ulong playerFrameIndex = networkEvent.Message.ReadUInt64(); + + if (!clientWorldState.playerFrameHistory.ContainsKey(playerId)) + { + var playerFrames = new PlayerFrame[120]; + for (int j = 0; j < playerFrames.Length; j++) + playerFrames[j] = new PlayerFrame(); + clientWorldState.playerFrameHistory.Add(playerId, playerFrames); + } + + SpawnPlayer(playerId, + new Float3(networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle()), + new Float3(networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle(), networkEvent.Message.ReadSingle())); + + //if (NetworkManager.IsClient) + players[playerId].GetScript().input.frame = ClientFrame; + break; + } + case GameModeMessageType.PlayerInput: + { + uint playerId = networkEvent.Sender.ConnectionId; + PlayerInputState inputState = new PlayerInputState(); + inputState.frame = networkEvent.Message.ReadUInt64(); + inputState.viewDeltaX = networkEvent.Message.ReadSingle(); + inputState.viewDeltaY = networkEvent.Message.ReadSingle(); + inputState.moveForward = networkEvent.Message.ReadSingle(); + inputState.moveRight = networkEvent.Message.ReadSingle(); + inputState.attacking = networkEvent.Message.ReadBoolean(); + inputState.jumping = networkEvent.Message.ReadBoolean(); + + UpdatePlayerInput(playerId, inputState); + + + + playerLastReceivedFrames[playerId] = inputState.frame; + break; + } + case GameModeMessageType.PlayerPosition: + { + //uint playerId = networkEvent.Sender.ConnectionId; + PlayerInputState inputState = default; //? + PlayerActorState actorState; + + ulong reportedFrame = networkEvent.Message.ReadUInt64(); + uint reportedPlayerId = networkEvent.Message.ReadUInt32(); + actorState.position.X = networkEvent.Message.ReadSingle(); + actorState.position.Y = networkEvent.Message.ReadSingle(); + actorState.position.Z = networkEvent.Message.ReadSingle(); + actorState.velocity.X = networkEvent.Message.ReadSingle(); + actorState.velocity.Y = networkEvent.Message.ReadSingle(); + actorState.velocity.Z = networkEvent.Message.ReadSingle(); + actorState.orientation.X = networkEvent.Message.ReadSingle(); + actorState.orientation.Y = networkEvent.Message.ReadSingle(); + actorState.orientation.Z = networkEvent.Message.ReadSingle(); + actorState.orientation.W = networkEvent.Message.ReadSingle(); + actorState.viewAngles.X = networkEvent.Message.ReadSingle(); + actorState.viewAngles.Y = networkEvent.Message.ReadSingle(); + actorState.viewAngles.Z = networkEvent.Message.ReadSingle(); + actorState.lastJumpTime = networkEvent.Message.ReadSingle(); + actorState.numJumps = networkEvent.Message.ReadInt32(); + actorState.jumped = networkEvent.Message.ReadBoolean(); + + inputState.frame = reportedFrame; + inputState.viewDeltaX = networkEvent.Message.ReadSingle(); + inputState.viewDeltaY = networkEvent.Message.ReadSingle(); + inputState.moveForward = networkEvent.Message.ReadSingle(); + inputState.moveRight = networkEvent.Message.ReadSingle(); + inputState.attacking = networkEvent.Message.ReadBoolean(); + inputState.jumping = networkEvent.Message.ReadBoolean(); + + //if (actorState.viewAngles != new Float3(90f, 0f, 0f)) + // Console.Print($"{reportedFrame} has viewangles: {actorState.viewAngles}"); + + //Assert.IsTrue(reportedFrame >= lastReceivedServerFrame); + if (reportedPlayerId == NetworkManager.LocalPlayerClientId && reportedFrame < lastReceivedServerFrame) + { + //Console.Print($"packet wrong order, last received: {lastReceivedServerFrame}, new: {reportedFrame}"); break; } - case GameModeMessageType.PlayerInput: + + if (IsClient) { - uint playerId = networkEvent.Sender.ConnectionId; - PlayerInputState inputState = new PlayerInputState(); - inputState.frame = networkEvent.Message.ReadUInt64(); - inputState.viewDeltaX = networkEvent.Message.ReadSingle(); - inputState.viewDeltaY = networkEvent.Message.ReadSingle(); - inputState.moveForward = networkEvent.Message.ReadSingle(); - inputState.moveRight = networkEvent.Message.ReadSingle(); - inputState.attacking = networkEvent.Message.ReadBoolean(); - inputState.jumping = networkEvent.Message.ReadBoolean(); + if (reportedPlayerId == NetworkManager.LocalPlayerClientId) + lastReceivedServerFrame = reportedFrame; - UpdatePlayerInput(playerId, inputState); + //Console.Print($"we drifted, corrected. client frame: {serverWorldState.frame}, server frame: {reportedFrame}"); + PlayerActor playerActor = scene.GetChildren().FirstOrDefault(x => + x.GetScript().PlayerId == reportedPlayerId); - - playerLastReceivedFrames[playerId] = inputState.frame; - break; - } - case GameModeMessageType.PlayerPosition: - { - //uint playerId = networkEvent.Sender.ConnectionId; - PlayerInputState inputState = default; //? - PlayerActorState actorState; - - ulong reportedFrame = networkEvent.Message.ReadUInt64(); - uint reportedPlayerId = networkEvent.Message.ReadUInt32(); - actorState.position.X = networkEvent.Message.ReadSingle(); - actorState.position.Y = networkEvent.Message.ReadSingle(); - actorState.position.Z = networkEvent.Message.ReadSingle(); - actorState.velocity.X = networkEvent.Message.ReadSingle(); - actorState.velocity.Y = networkEvent.Message.ReadSingle(); - actorState.velocity.Z = networkEvent.Message.ReadSingle(); - actorState.orientation.X = networkEvent.Message.ReadSingle(); - actorState.orientation.Y = networkEvent.Message.ReadSingle(); - actorState.orientation.Z = networkEvent.Message.ReadSingle(); - actorState.orientation.W = networkEvent.Message.ReadSingle(); - actorState.viewAngles.X = networkEvent.Message.ReadSingle(); - actorState.viewAngles.Y = networkEvent.Message.ReadSingle(); - actorState.viewAngles.Z = networkEvent.Message.ReadSingle(); - actorState.lastJumpTime = networkEvent.Message.ReadSingle(); - actorState.numJumps = networkEvent.Message.ReadInt32(); - actorState.jumped = networkEvent.Message.ReadBoolean(); - - inputState.frame = reportedFrame; - inputState.viewDeltaX = networkEvent.Message.ReadSingle(); - inputState.viewDeltaY = networkEvent.Message.ReadSingle(); - inputState.moveForward = networkEvent.Message.ReadSingle(); - inputState.moveRight = networkEvent.Message.ReadSingle(); - inputState.attacking = networkEvent.Message.ReadBoolean(); - inputState.jumping = networkEvent.Message.ReadBoolean(); - - //if (actorState.viewAngles != new Float3(90f, 0f, 0f)) - // Console.Print($"{reportedFrame} has viewangles: {actorState.viewAngles}"); - - //Assert.IsTrue(reportedFrame >= lastReceivedServerFrame); - if (reportedPlayerId == NetworkManager.LocalPlayerClientId && reportedFrame < lastReceivedServerFrame) + if (playerActor != null) { - //Console.Print($"packet wrong order, last received: {lastReceivedServerFrame}, new: {reportedFrame}"); - break; - } + PlayerInput playerInput = playerActor.GetScript().input; - if (IsClient) - { - if (reportedPlayerId == NetworkManager.LocalPlayerClientId) - lastReceivedServerFrame = reportedFrame; + if (IsLocalClient && reportedPlayerId == NetworkManager.LocalPlayerClientId) + { } + else + playerInput.SetState(reportedFrame, inputState, actorState); - //Console.Print($"we drifted, corrected. client frame: {serverWorldState.frame}, server frame: {reportedFrame}"); - PlayerActor playerActor = scene.GetChildren().FirstOrDefault(x => - x.GetScript().PlayerId == reportedPlayerId); - - - if (playerActor != null) + if (!IsLocalClient && playerInput is PlayerInputNetwork) { - PlayerInput playerInput = playerActor.GetScript().input; - - if (IsLocalClient && reportedPlayerId == NetworkManager.LocalPlayerClientId) - { } - else - playerInput.SetState(reportedFrame, inputState, actorState); - - if (!IsLocalClient && playerInput is PlayerInputNetwork) - { - playerActor.Position = actorState.position; - playerActor.GetScript().movementState.currentVelocity = actorState.velocity; - playerActor.GetScript().movementState.lastJumped = actorState.lastJumpTime; - playerActor.GetScript().movementState.numJumps = actorState.numJumps; - playerActor.GetScript().movementState.jumped = actorState.jumped; - playerActor.GetScript().SetCameraEulerAngles(actorState.viewAngles, true); - } - //playerActor.SetPosition(reportedPosition); + playerActor.Position = actorState.position; + playerActor.GetScript().movementState.currentVelocity = actorState.velocity; + playerActor.GetScript().movementState.lastJumped = actorState.lastJumpTime; + playerActor.GetScript().movementState.numJumps = actorState.numJumps; + playerActor.GetScript().movementState.jumped = actorState.jumped; + playerActor.GetScript().SetCameraEulerAngles(actorState.viewAngles, true); } + //playerActor.SetPosition(reportedPosition); } - - break; - } - default: - Console.PrintError($"GameModeManager: Unhandled message type: {messageType}"); - return false; - } - - return true; - } - - public bool OnClientConnecting(NetworkConnection connection) - { - //if (connection.ConnectionId != NetworkManager.LocalPlayerClientId) - { - Console.Print("sending welcome: frame " + serverWorldState.frame); - WelcomePlayerMessage welcomeMessage = new WelcomePlayerMessage(); - welcomeMessage.frame = serverWorldState.frame; - welcomeMessage.time = ServerTime; - welcomeMessage.players = new WelcomePlayerMessage.PlayerInfo[serverWorldState.actors.Count]; - - for (int i = 0; i < serverWorldState.actors.Count; i++) - { - PlayerActor playerActor = serverWorldState.actors[i]; - ref WelcomePlayerMessage.PlayerInfo playerInfo = ref welcomeMessage.players[i]; - playerInfo.playerId = playerActor.GetScript().PlayerId; - playerInfo.playerPosition = playerActor.Position; - - playerActor.GetScript().input.SetState(serverWorldState.frame, new PlayerInputState(), new PlayerActorState() - { - position = playerActor.Position, - velocity = playerActor.GetScript().movementState.currentVelocity, - orientation = playerActor.GetScript().rootActor.Orientation, - viewAngles = playerActor.GetScript().viewAngles, - lastJumpTime = playerActor.GetScript().movementState.lastJumped, - numJumps = playerActor.GetScript().movementState.numJumps, - jumped = playerActor.GetScript().movementState.jumped, - //onGround = playerActor.GetScript().movementState.onGround, - }); - } - NetworkMessage message = NetworkManager.ServerBeginSendMessage(); - welcomeMessage.Write(ref message); - NetworkManager.ServerEndSendMessage(ref message, connection); - } - return true; - } - - public bool OnClientConnected(NetworkConnection connection) - { - uint playerId = connection.ConnectionId; - if (NetworkManager.LocalPlayerClientId == 0) - NetworkManager.LocalPlayerClientId = playerId; - - var spawns = worldSpawn.GetChildren().Where(x => x.Name.StartsWith("PlayerSpawn_")).ToArray(); - Console.Print($"found {spawns.Length} spawns"); - - var randomSpawn = spawns.First(); - - - Float3 position = randomSpawn.Position + new Float3(0f, 4.1f, 0f); - Float3 eulerAngles = randomSpawn.Orientation.EulerAngles; - - players.Add(playerId, null); - playerConnections.Add(playerId, connection); - var playerFrames = new PlayerFrame[120]; - for (int i = 0; i < playerFrames.Length; i++) - playerFrames[i] = new PlayerFrame(); - serverWorldState.playerFrameHistory.Add(playerId, playerFrames); - - - SpawnPlayer(playerId, position, eulerAngles); - { - NetworkMessage message = NetworkManager.ServerBeginSendMessage(); - message.WriteByte((byte)GameModeMessageType.SpawnPlayer); - message.WriteUInt32(playerId); - message.WriteUInt64(serverWorldState.frame); - message.WriteSingle(position.X); - message.WriteSingle(position.Y); - message.WriteSingle(position.Z); - message.WriteSingle(eulerAngles.X); - message.WriteSingle(eulerAngles.Y); - message.WriteSingle(eulerAngles.Z); - NetworkManager.ServerEndSendMessage(ref message, connection); - } - - return true; - } - - private void SpawnPlayer(uint playerId, Float3 position, Vector3 eulerAngles) - { - if (IsServer && !playerLastFrame.ContainsKey(playerId)) - playerLastFrame.Add(playerId, serverWorldState.frame); - - //if (IsLocalClient && playerId == NetworkManager.LocalPlayerClientId) - // return; // Handled by listenserver - - //spawned = true; - - string prefabPath = Path.Combine(AssetManager.ContentPath, "Common"); - var playerPrefab = Content.Load(Path.Combine(prefabPath, "PlayerPrefab.prefab")); - if (playerPrefab == null) - Console.PrintError("GameModeManager: Failed to find PlayerPrefab"); - - PlayerActor playerActor = SpawnActor(playerPrefab); - playerActor.Initialize(playerId, position, eulerAngles); - //playerActor.Teleport(position, eulerAngles); - - if (!players.ContainsKey(playerId)) - players.Add(playerId, null); - players[playerId] = playerActor; - PlayerInput playerInput = playerActor.GetScript().input; - playerInput.frame = IsServer ? serverWorldState.frame : ClientFrame; - if (IsServer) - { - serverWorldState.actors.Add(playerActor); - if (!playerLastReceivedFrames.ContainsKey(playerId)) - playerLastReceivedFrames.Add(playerId, 0); - } - } - - private T SpawnActor(Prefab prefab) where T : Actor - { - T actor = PrefabManager.SpawnPrefab(prefab, scene).As(); - actor.PhysicsScene = scene.PhysicsScene; - return actor; - } - - private void UpdatePlayerInput(uint playerId, PlayerInputState inputState) - { - if (playerId == NetworkManager.LocalPlayerClientId) - { - playerLastFrame[playerId] = inputState.frame; - return; - } - - PlayerActor playerActor = players[playerId]; - PlayerMovement playerMovement = playerActor.GetScript(); - playerActor.UpdateNetworkInput(inputState); - - ulong startFrame = playerLastFrame[playerId]; - - if (startFrame >= inputState.frame) - return; // dropped frame, ignore - - - // simulate at least one or more frame when receiving input frames from player. missing frames use inputs from previous frames - var simframs = 0; - ulong frame = startFrame+1; - PlayerInputState prevInputState; - playerMovement.input.GetState(frame - 1, out prevInputState, out var _); - for (; frame <= inputState.frame; frame++) - { - if (!playerMovement.input.GetState(frame, out var lastInputState, out var lastActorState)) - { - // dropped frame, use previous input - lastInputState = prevInputState; - lastInputState.frame = frame; } - playerMovement.ApplyInputToCamera(lastInputState, true); - playerMovement.SimulatePlayerMovement(lastInputState); + break; + } + default: + Console.PrintError($"GameModeManager: Unhandled message type: {messageType}"); + return false; + } - playerMovement.input.SetState(frame, lastInputState, new PlayerActorState() + return true; + } + + public bool OnClientConnecting(NetworkConnection connection) + { + //if (connection.ConnectionId != NetworkManager.LocalPlayerClientId) + { + Console.Print("sending welcome: frame " + serverWorldState.frame); + WelcomePlayerMessage welcomeMessage = new WelcomePlayerMessage(); + welcomeMessage.frame = serverWorldState.frame; + welcomeMessage.time = ServerTime; + welcomeMessage.players = new WelcomePlayerMessage.PlayerInfo[serverWorldState.actors.Count]; + + for (int i = 0; i < serverWorldState.actors.Count; i++) + { + PlayerActor playerActor = serverWorldState.actors[i]; + ref WelcomePlayerMessage.PlayerInfo playerInfo = ref welcomeMessage.players[i]; + playerInfo.playerId = playerActor.GetScript().PlayerId; + playerInfo.playerPosition = playerActor.Position; + + playerActor.GetScript().input.SetState(serverWorldState.frame, new PlayerInputState(), new PlayerActorState() { position = playerActor.Position, - velocity = playerMovement.movementState.currentVelocity, - orientation = playerMovement.rootActor.Orientation, - viewAngles = playerMovement.viewAngles, - lastJumpTime = playerMovement.movementState.lastJumped, - numJumps = playerMovement.movementState.numJumps, - jumped = playerMovement.movementState.jumped, - //onGround = playerMovement.movementState.onGround, + velocity = playerActor.GetScript().movementState.currentVelocity, + orientation = playerActor.GetScript().rootActor.Orientation, + viewAngles = playerActor.GetScript().viewAngles, + lastJumpTime = playerActor.GetScript().movementState.lastJumped, + numJumps = playerActor.GetScript().movementState.numJumps, + jumped = playerActor.GetScript().movementState.jumped, + //onGround = playerActor.GetScript().movementState.onGround, }); + } + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + welcomeMessage.Write(ref message); + NetworkManager.ServerEndSendMessage(ref message, connection); + } + return true; + } - simframs++; - prevInputState = lastInputState; + public bool OnClientConnected(NetworkConnection connection) + { + uint playerId = connection.ConnectionId; + if (NetworkManager.LocalPlayerClientId == 0) + NetworkManager.LocalPlayerClientId = playerId; + + var spawns = worldSpawn.GetChildren().Where(x => x.Name.StartsWith("PlayerSpawn_")).ToArray(); + Console.Print($"found {spawns.Length} spawns"); + + var randomSpawn = spawns.First(); + + + Float3 position = randomSpawn.Position + new Float3(0f, 4.1f, 0f); + Float3 eulerAngles = randomSpawn.Orientation.EulerAngles; + + players.Add(playerId, null); + playerConnections.Add(playerId, connection); + var playerFrames = new PlayerFrame[120]; + for (int i = 0; i < playerFrames.Length; i++) + playerFrames[i] = new PlayerFrame(); + serverWorldState.playerFrameHistory.Add(playerId, playerFrames); + + + SpawnPlayer(playerId, position, eulerAngles); + { + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.SpawnPlayer); + message.WriteUInt32(playerId); + message.WriteUInt64(serverWorldState.frame); + message.WriteSingle(position.X); + message.WriteSingle(position.Y); + message.WriteSingle(position.Z); + message.WriteSingle(eulerAngles.X); + message.WriteSingle(eulerAngles.Y); + message.WriteSingle(eulerAngles.Z); + NetworkManager.ServerEndSendMessage(ref message, connection); + } + + return true; + } + + private void SpawnPlayer(uint playerId, Float3 position, Vector3 eulerAngles) + { + if (IsServer && !playerLastFrame.ContainsKey(playerId)) + playerLastFrame.Add(playerId, serverWorldState.frame); + + //if (IsLocalClient && playerId == NetworkManager.LocalPlayerClientId) + // return; // Handled by listenserver + + //spawned = true; + + string prefabPath = Path.Combine(AssetManager.ContentPath, "Common"); + var playerPrefab = Content.Load(Path.Combine(prefabPath, "PlayerPrefab.prefab")); + if (playerPrefab == null) + Console.PrintError("GameModeManager: Failed to find PlayerPrefab"); + + PlayerActor playerActor = SpawnActor(playerPrefab); + playerActor.Initialize(playerId, position, eulerAngles); + //playerActor.Teleport(position, eulerAngles); + + if (!players.ContainsKey(playerId)) + players.Add(playerId, null); + players[playerId] = playerActor; + PlayerInput playerInput = playerActor.GetScript().input; + playerInput.frame = IsServer ? serverWorldState.frame : ClientFrame; + if (IsServer) + { + serverWorldState.actors.Add(playerActor); + if (!playerLastReceivedFrames.ContainsKey(playerId)) + playerLastReceivedFrames.Add(playerId, 0); + } + } + + private T SpawnActor(Prefab prefab) where T : Actor + { + T actor = PrefabManager.SpawnPrefab(prefab, scene).As(); + actor.PhysicsScene = scene.PhysicsScene; + return actor; + } + + private void UpdatePlayerInput(uint playerId, PlayerInputState inputState) + { + if (playerId == NetworkManager.LocalPlayerClientId) + { + playerLastFrame[playerId] = inputState.frame; + return; + } + + PlayerActor playerActor = players[playerId]; + PlayerMovement playerMovement = playerActor.GetScript(); + playerActor.UpdateNetworkInput(inputState); + + ulong startFrame = playerLastFrame[playerId]; + + if (startFrame >= inputState.frame) + return; // dropped frame, ignore + + + // simulate at least one or more frame when receiving input frames from player. missing frames use inputs from previous frames + var simframs = 0; + ulong frame = startFrame+1; + PlayerInputState prevInputState; + playerMovement.input.GetState(frame - 1, out prevInputState, out var _); + for (; frame <= inputState.frame; frame++) + { + if (!playerMovement.input.GetState(frame, out var lastInputState, out var lastActorState)) + { + // dropped frame, use previous input + lastInputState = prevInputState; + lastInputState.frame = frame; } - if (playerActor.Position.Length < 1.0f) - simframs = simframs; + playerMovement.ApplyInputToCamera(lastInputState, true); + playerMovement.SimulatePlayerMovement(lastInputState); - playerLastFrame[playerId] = inputState.frame;//frame; + playerMovement.input.SetState(frame, lastInputState, new PlayerActorState() + { + position = playerActor.Position, + velocity = playerMovement.movementState.currentVelocity, + orientation = playerMovement.rootActor.Orientation, + viewAngles = playerMovement.viewAngles, + lastJumpTime = playerMovement.movementState.lastJumped, + numJumps = playerMovement.movementState.numJumps, + jumped = playerMovement.movementState.jumped, + //onGround = playerMovement.movementState.onGround, + }); - if (simframs > 1) - Console.Print($"simulated {simframs} frames"); + simframs++; + prevInputState = lastInputState; } + + if (playerActor.Position.Length < 1.0f) + simframs = simframs; + + playerLastFrame[playerId] = inputState.frame;//frame; + + if (simframs > 1) + Console.Print($"simulated {simframs} frames"); } } #endif \ No newline at end of file diff --git a/Source/Game/Hud/CrosshairWidget.cs b/Source/Game/Hud/CrosshairWidget.cs index aabfc98..e9caf81 100644 --- a/Source/Game/Hud/CrosshairWidget.cs +++ b/Source/Game/Hud/CrosshairWidget.cs @@ -3,23 +3,22 @@ using System.Diagnostics; using FlaxEngine; using FlaxEngine.GUI; -namespace Game -{ +namespace Game; + public class CrosshairWidget : Script +{ + private Image control; + + public override void OnAwake() { - private Image control; + var uiControl = Actor.As(); + //control = new Image(); + //uiControl.Control = control; - public override void OnAwake() - { - var uiControl = Actor.As(); - //control = new Image(); - //uiControl.Control = control; + control = (Image)uiControl.Control; + } - control = (Image)uiControl.Control; - } - - public override void OnUpdate() - { - } + public override void OnUpdate() + { } } \ No newline at end of file diff --git a/Source/Game/Hud/PerformanceWidget.cs b/Source/Game/Hud/PerformanceWidget.cs index 67205b9..75832f3 100644 --- a/Source/Game/Hud/PerformanceWidget.cs +++ b/Source/Game/Hud/PerformanceWidget.cs @@ -3,71 +3,71 @@ using System.Diagnostics; using FlaxEngine; using FlaxEngine.GUI; -namespace Game +namespace Game; + +[ExecuteInEditMode] +public class PerformanceWidget : Script { - [ExecuteInEditMode] - public class PerformanceWidget : Script + private const double updateInterval = 0.2; + + public UIControl control; + private Label label; + + private Stopwatch stopwatch; + private Stopwatch stopwatch2; + + private double updateTimeAvg; + private double updateTimeAvg2; + private ulong updateTimeCount; + private ulong updateTimeCount2; + + public override void OnAwake() { - private const double updateInterval = 0.2; + label = (Label)control.Control; + label.Text = $"0fps"; + control.HideFlags = HideFlags.None; - public UIControl control; - private Label label; + stopwatch = Stopwatch.StartNew(); + stopwatch2 = Stopwatch.StartNew(); + } - private Stopwatch stopwatch; - private Stopwatch stopwatch2; - - private double updateTimeAvg; - private double updateTimeAvg2; - private ulong updateTimeCount; - private ulong updateTimeCount2; - - public override void OnAwake() + public override void OnUpdate() + { + updateTimeCount2++; + double elapsed2 = stopwatch2.Elapsed.TotalSeconds; + if (elapsed2 >= updateInterval * 10) { - label = (Label)control.Control; - label.Text = $"0fps"; - control.HideFlags = HideFlags.None; - - stopwatch = Stopwatch.StartNew(); - stopwatch2 = Stopwatch.StartNew(); + stopwatch2.Restart(); + updateTimeAvg2 = elapsed2 / updateTimeCount2; + updateTimeCount2 = 0; } - public override void OnUpdate() + updateTimeCount++; + double elapsed = stopwatch.Elapsed.TotalSeconds; + if (elapsed >= updateInterval) { - updateTimeCount2++; - double elapsed2 = stopwatch2.Elapsed.TotalSeconds; - if (elapsed2 >= updateInterval * 10) - { - stopwatch2.Restart(); - updateTimeAvg2 = elapsed2 / updateTimeCount2; - updateTimeCount2 = 0; - } + stopwatch.Restart(); + updateTimeAvg = elapsed / updateTimeCount; + updateTimeCount = 0; - updateTimeCount++; - double elapsed = stopwatch.Elapsed.TotalSeconds; - if (elapsed >= updateInterval) - { - stopwatch.Restart(); - updateTimeAvg = elapsed / updateTimeCount; - updateTimeCount = 0; + label.Text = ""; - label.Text = ""; - - long triangles = 0; - long drawCalls = 0; + long triangles = 0; + long drawCalls = 0; #if BUILD_DEBUG || BUILD_DEVELOPMENT - var gpuEvents = ProfilingTools.EventsGPU; - if (gpuEvents.Length > 0) - { + var gpuEvents = ProfilingTools.EventsGPU; + if (gpuEvents.Length > 0) + { triangles = gpuEvents[0].Stats.Triangles; drawCalls = gpuEvents[0].Stats.DrawCalls; - } + } #endif - label.Text += $"{triangles} tris\n {drawCalls} drawcalls\n"; - label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg2)}fps2\n"; - label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg)}fps"; - } + label.Text += $"{triangles} tris\n {drawCalls} drawcalls\n"; + label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg2)}fps2\n"; + label.Text += $"{(int)Math.Round(1.0f / updateTimeAvg)}fps"; + } #if false #if BUILD_DEVELOPMENT @@ -98,6 +98,5 @@ namespace Game } #endif #endif - } } } \ No newline at end of file diff --git a/Source/Game/Hud/SpeedWidget.cs b/Source/Game/Hud/SpeedWidget.cs index ad925bc..8257d46 100644 --- a/Source/Game/Hud/SpeedWidget.cs +++ b/Source/Game/Hud/SpeedWidget.cs @@ -3,18 +3,17 @@ using System.Diagnostics; using FlaxEngine; using FlaxEngine.GUI; -namespace Game +namespace Game; + +public class SpeedWidget : Script { - public class SpeedWidget : Script + public override void OnAwake() { - public override void OnAwake() - { - } + } - public override void OnUpdate() - { + public override void OnUpdate() + { - } } } \ No newline at end of file diff --git a/Source/Game/Level/BrushMaterialList.cs b/Source/Game/Level/BrushMaterialList.cs index c6ee75a..35c015f 100644 --- a/Source/Game/Level/BrushMaterialList.cs +++ b/Source/Game/Level/BrushMaterialList.cs @@ -1,23 +1,22 @@ using FlaxEngine; -namespace Game +namespace Game; + +/// +/// List of supported materials for loaded levels. +/// Maps the given texture/shader name to Flax Material/MaterialInstance. +/// +public class BrushMaterialList { - /// - /// List of supported materials for loaded levels. - /// Maps the given texture/shader name to Flax Material/MaterialInstance. - /// - public class BrushMaterialList - { - [EditorDisplay(name: "Material Assets")] - public BrushMaterialListEntry[] materialAssets; - } + [EditorDisplay(name: "Material Assets")] + public BrushMaterialListEntry[] materialAssets; +} - public struct BrushMaterialListEntry - { - [EditorOrder(1)] [EditorDisplay(name: "Name")] - public string name; +public struct BrushMaterialListEntry +{ + [EditorOrder(1)] [EditorDisplay(name: "Name")] + public string name; - [EditorOrder(2)] [EditorDisplay(name: "Material")] - public MaterialBase asset; - } + [EditorOrder(2)] [EditorDisplay(name: "Material")] + public MaterialBase asset; } \ No newline at end of file diff --git a/Source/Game/Level/BrushScript.cs b/Source/Game/Level/BrushScript.cs index 808cffc..df7a64e 100644 --- a/Source/Game/Level/BrushScript.cs +++ b/Source/Game/Level/BrushScript.cs @@ -1,10 +1,9 @@ using System; using FlaxEngine; -namespace Game +namespace Game; + +[ExecuteInEditMode] +public class BrushScript : Script { - [ExecuteInEditMode] - public class BrushScript : Script - { - } } \ No newline at end of file diff --git a/Source/Game/Level/LevelScript.cs b/Source/Game/Level/LevelScript.cs index dc6ad8f..a21d213 100644 --- a/Source/Game/Level/LevelScript.cs +++ b/Source/Game/Level/LevelScript.cs @@ -2,18 +2,17 @@ using FlaxEngine; using FlaxEditor; -namespace Game -{ - [ExecuteInEditMode] - public class LevelScript : Script - { - public string MapName; - public DateTime MapTimestamp; - } +namespace Game; - public class LevelScript2 : Script - { - public string MapName; - public DateTime MapTimestamp; - } +[ExecuteInEditMode] +public class LevelScript : Script +{ + public string MapName; + public DateTime MapTimestamp; +} + +public class LevelScript2 : Script +{ + public string MapName; + public DateTime MapTimestamp; } \ No newline at end of file diff --git a/Source/Game/Level/MapParser.cs b/Source/Game/Level/MapParser.cs index 00631b1..a44767e 100644 --- a/Source/Game/Level/MapParser.cs +++ b/Source/Game/Level/MapParser.cs @@ -7,523 +7,522 @@ using FlaxEngine; // https://web.archive.org/web/20160316213335/http://forums.ubergames.net/topic/2658-understanding-the-quake-3-map-format/ // https://web.archive.org/web/20210228125854/https://forums.thedarkmod.com/index.php?/topic/15668-plugin-request-save-map-in-quake-3-format/ -namespace Game +namespace Game; + +public struct MapFacePlane { - public struct MapFacePlane - { - public Float3 v1, v2, v3; - public Plane plane; - public string texture; - public Float2 offset; - public float rotation; - public Float2 scale; - public int contentFlags, surfaceFlags, surfaceValue; - } + public Float3 v1, v2, v3; + public Plane plane; + public string texture; + public Float2 offset; + public float rotation; + public Float2 scale; + public int contentFlags, surfaceFlags, surfaceValue; +} - public class MapBrush - { - public MapFacePlane[] planes; - } +public class MapBrush +{ + public MapFacePlane[] planes; +} - public class MapPatch - { - public string name; - } +public class MapPatch +{ + public string name; +} - public struct PatchVertex - { - public Float3 v; - public Float2 uv; - } +public struct PatchVertex +{ + public Float3 v; + public Float2 uv; +} - public class MapEntity - { - public List brushes = new List(); - public List entities = new List(); - public List patches = new List(); - public Dictionary properties = new Dictionary(); - } +public class MapEntity +{ + public List brushes = new List(); + public List entities = new List(); + public List patches = new List(); + public Dictionary properties = new Dictionary(); +} - public static class MapParser +public static class MapParser +{ + public static MapEntity Parse(byte[] data) { - public static MapEntity Parse(byte[] data) + if (data.Length == 0) + return null; + + MapEntity rootEntity = new MapEntity(); + MapEntity currentEntity = rootEntity; + + int level = 0; + int index = 0; + //for (int i=0; i 2) - // throw new Exception("Failed to parse .map file: unexpected entity found at line " + lineNumber.ToString()); - } while (++index < data.Length); - - return rootEntity; - } - - private static void ParseComment(byte[] data, ref int index) - { - for (; index < data.Length; index++) - if (data[index] == '\n') + case '\n': + case '\r': break; - } - - private static void ParseEntity(MapEntity currentEntity, byte[] data, ref int index) - { - bool entityParsed = false; - do - { - char c = (char)data[index]; - char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0; - - switch (c) + case '/': + if (c1 == '/') + ParseComment(data, ref index); + else + throw new Exception("unexpected character: '" + c + "'"); + break; + case '{': { - case '\n': - case '\r': - break; + currentEntity = new MapEntity(); + rootEntity.entities.Add(currentEntity); - case '/': - if (c1 == '/') - ParseComment(data, ref index); - else - throw new Exception("unexpected character: '" + c + "'"); - break; + level++; - // "name" "value" - case '"': - { - string propName = ParseQuotedString(data, ref index); - string propValue = ParseQuotedString(data, ref index); - - if (currentEntity.properties.ContainsKey(propName)) - throw new Exception("Failed to parse .map file: multiple properties defined for " + - propName + - " at line ?"); // + lineNumber.ToString()); - currentEntity.properties.Add(propName, propValue); - break; - } - - // brush - case '{': - { - index++; - for (; index < data.Length; index++) - { - if (data[index] != ' ' && data[index] != '\r' && data[index] != '\n') - break; - //index++; - } - - if (index >= data.Length) + for (; index < data.Length; index++) + if (data[index] == '\n') break; + index++; - if (data[index] == '(') - currentEntity.brushes.Add(ParseBrush(data, ref index)); - else if (char.IsLetter((char)data[index]) || char.IsNumber((char)data[index])) - currentEntity.patches.Add(ParsePatch(data, ref index)); - break; - } - case '}': - { - entityParsed = true; - break; - } - - default: - throw new Exception("unsupported character: '" + c + "'"); + ParseEntity(currentEntity, data, ref index); + break; } - } while (index++ < data.Length && !entityParsed); - } - - private static string ParseQuotedString(byte[] data, ref int index) - { - StringBuilder sb = new StringBuilder(); - index++; - - for (; index < data.Length; index++) - { - if (data[index] == '"') - break; - sb.Append((char)data[index]); - } - - index++; - - while (index < data.Length) - { - if (data[index] != ' ' && data[index] != '\t') - break; - index++; - } - - return sb.ToString(); - } - - private static string ParseString(byte[] data, ref int index) - { - StringBuilder sb = new StringBuilder(); - - for (; index < data.Length; index++) - { - if (data[index] == ' ' || data[index] == '\r' || data[index] == '\n') - break; - sb.Append((char)data[index]); - } - - //if (data[index] == '\n') - // index++; - - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - return sb.ToString(); - } - - private static float ParseFloat(byte[] data, ref int index) - { - string fs = ParseString(data, ref index); - - if (float.TryParse(fs, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) - return value; - //else if (float.TryParse(fs, CultureInfo.InvariantCulture, out intValue)) - // return intValue; - throw new Exception("failed to ParseFloat: " + fs); - } - - private static int ParseInt(byte[] data, ref int index) - { - string fs = ParseString(data, ref index); - - if (int.TryParse(fs, out int value)) - return value; - throw new Exception("failed to ParseInt: " + fs); - } - - private static Float3 ParseFloat3(byte[] data, ref int index) - { - return new Float3( - ParseFloat(data, ref index), - ParseFloat(data, ref index), - ParseFloat(data, ref index) - ); - } - - private static Float2 ParseFloat2(byte[] data, ref int index) - { - return new Float2( - ParseFloat(data, ref index), - ParseFloat(data, ref index) - ); - } - - private static Float3 ParsePlaneFloat3(byte[] data, ref int index) - { - index++; - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - Float3 vector = ParseFloat3(data, ref index); - // rounding - /*float temp = vector.Z; - vector.Z = vector.Y; - vector.Y = temp;*/ - /*vector.X = (float)Math.Round(vector.X, 1); - vector.Y = (float)Math.Round(vector.Y, 1); - vector.Z = (float)Math.Round(vector.Z, 1);*/ - - while (index < data.Length) - { - if (data[index] == ')') - break; - index++; - } - - index++; - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - return vector; - } - - private static MapBrush ParseBrush(byte[] data, ref int index) - { - MapBrush brush = new MapBrush(); - - var planes = new List(6); - - bool brushParsed = false; - do - { - char c = (char)data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) + case '}': { - case '\r': - case '\n': + //currentEntity = rootEntity; + break; + } + default: + throw new Exception("unsupported character: '" + c + "'"); + } + + //if (level < 0 || level > 2) + // throw new Exception("Failed to parse .map file: unexpected entity found at line " + lineNumber.ToString()); + } while (++index < data.Length); + + return rootEntity; + } + + private static void ParseComment(byte[] data, ref int index) + { + for (; index < data.Length; index++) + if (data[index] == '\n') + break; + } + + private static void ParseEntity(MapEntity currentEntity, byte[] data, ref int index) + { + bool entityParsed = false; + do + { + char c = (char)data[index]; + char c1 = index + 1 < data.Length ? (char)data[index + 1] : (char)0; + + switch (c) + { + case '\n': + case '\r': + break; + + case '/': + if (c1 == '/') + ParseComment(data, ref index); + else + throw new Exception("unexpected character: '" + c + "'"); + break; + + // "name" "value" + case '"': + { + string propName = ParseQuotedString(data, ref index); + string propValue = ParseQuotedString(data, ref index); + + if (currentEntity.properties.ContainsKey(propName)) + throw new Exception("Failed to parse .map file: multiple properties defined for " + + propName + + " at line ?"); // + lineNumber.ToString()); + currentEntity.properties.Add(propName, propValue); + break; + } + + // brush + case '{': + { + index++; + for (; index < data.Length; index++) + { + if (data[index] != ' ' && data[index] != '\r' && data[index] != '\n') + break; + //index++; + } + + if (index >= data.Length) break; - // brush face (quake format): - // ( ) ( ) ( ) + if (data[index] == '(') + currentEntity.brushes.Add(ParseBrush(data, ref index)); + else if (char.IsLetter((char)data[index]) || char.IsNumber((char)data[index])) + currentEntity.patches.Add(ParsePatch(data, ref index)); + break; + } + case '}': + { + entityParsed = true; + break; + } - // brush face (quake3 format): - // ( ) ( ) ( ) + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !entityParsed); + } - // brush face (valve format): - // ( ) ( ) ( ) [ ] [ ] - case '(': + private static string ParseQuotedString(byte[] data, ref int index) + { + StringBuilder sb = new StringBuilder(); + index++; + + for (; index < data.Length; index++) + { + if (data[index] == '"') + break; + sb.Append((char)data[index]); + } + + index++; + + while (index < data.Length) + { + if (data[index] != ' ' && data[index] != '\t') + break; + index++; + } + + return sb.ToString(); + } + + private static string ParseString(byte[] data, ref int index) + { + StringBuilder sb = new StringBuilder(); + + for (; index < data.Length; index++) + { + if (data[index] == ' ' || data[index] == '\r' || data[index] == '\n') + break; + sb.Append((char)data[index]); + } + + //if (data[index] == '\n') + // index++; + + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + return sb.ToString(); + } + + private static float ParseFloat(byte[] data, ref int index) + { + string fs = ParseString(data, ref index); + + if (float.TryParse(fs, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) + return value; + //else if (float.TryParse(fs, CultureInfo.InvariantCulture, out intValue)) + // return intValue; + throw new Exception("failed to ParseFloat: " + fs); + } + + private static int ParseInt(byte[] data, ref int index) + { + string fs = ParseString(data, ref index); + + if (int.TryParse(fs, out int value)) + return value; + throw new Exception("failed to ParseInt: " + fs); + } + + private static Float3 ParseFloat3(byte[] data, ref int index) + { + return new Float3( + ParseFloat(data, ref index), + ParseFloat(data, ref index), + ParseFloat(data, ref index) + ); + } + + private static Float2 ParseFloat2(byte[] data, ref int index) + { + return new Float2( + ParseFloat(data, ref index), + ParseFloat(data, ref index) + ); + } + + private static Float3 ParsePlaneFloat3(byte[] data, ref int index) + { + index++; + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + Float3 vector = ParseFloat3(data, ref index); + // rounding + /*float temp = vector.Z; + vector.Z = vector.Y; + vector.Y = temp;*/ + /*vector.X = (float)Math.Round(vector.X, 1); + vector.Y = (float)Math.Round(vector.Y, 1); + vector.Z = (float)Math.Round(vector.Z, 1);*/ + + while (index < data.Length) + { + if (data[index] == ')') + break; + index++; + } + + index++; + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + return vector; + } + + private static MapBrush ParseBrush(byte[] data, ref int index) + { + MapBrush brush = new MapBrush(); + + var planes = new List(6); + + bool brushParsed = false; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) + { + case '\r': + case '\n': + break; + + // brush face (quake format): + // ( ) ( ) ( ) + + // brush face (quake3 format): + // ( ) ( ) ( ) + + // brush face (valve format): + // ( ) ( ) ( ) [ ] [ ] + case '(': + { + MapFacePlane plane = new MapFacePlane(); + plane.v1 = ParsePlaneFloat3(data, ref index); + plane.v2 = ParsePlaneFloat3(data, ref index); + plane.v3 = ParsePlaneFloat3(data, ref index); + plane.texture = ParseString(data, ref index); + + if (true) // quake or quake3 format { - MapFacePlane plane = new MapFacePlane(); - plane.v1 = ParsePlaneFloat3(data, ref index); - plane.v2 = ParsePlaneFloat3(data, ref index); - plane.v3 = ParsePlaneFloat3(data, ref index); - plane.texture = ParseString(data, ref index); + plane.offset = ParseFloat2(data, ref index); + plane.rotation = ParseFloat(data, ref index); + plane.scale = ParseFloat2(data, ref index); - if (true) // quake or quake3 format + char peekChar = (char)data[index]; + if (peekChar != '\n') // quake3 format { - plane.offset = ParseFloat2(data, ref index); - plane.rotation = ParseFloat(data, ref index); - plane.scale = ParseFloat2(data, ref index); - - char peekChar = (char)data[index]; - if (peekChar != '\n') // quake3 format - { - plane.contentFlags = ParseInt(data, ref index); - plane.surfaceFlags = ParseInt(data, ref index); - plane.surfaceValue = ParseInt(data, ref index); - } + plane.contentFlags = ParseInt(data, ref index); + plane.surfaceFlags = ParseInt(data, ref index); + plane.surfaceValue = ParseInt(data, ref index); } - - // Flip Y and Z - plane.v1 = new Float3(plane.v1.X, plane.v1.Z, plane.v1.Y); - plane.v2 = new Float3(plane.v2.X, plane.v2.Z, plane.v2.Y); - plane.v3 = new Float3(plane.v3.X, plane.v3.Z, plane.v3.Y); - - plane.plane = new Plane(plane.v1, plane.v2, plane.v3); - - planes.Add(plane); - break; } - case '}': - { - brushParsed = true; - break; - } + // Flip Y and Z + plane.v1 = new Float3(plane.v1.X, plane.v1.Z, plane.v1.Y); + plane.v2 = new Float3(plane.v2.X, plane.v2.Z, plane.v2.Y); + plane.v3 = new Float3(plane.v3.X, plane.v3.Z, plane.v3.Y); - default: - if (char.IsLetter(c) || char.IsNumber(c)) - { - // patch name - } + plane.plane = new Plane(plane.v1, plane.v2, plane.v3); - throw new Exception("unsupported character: '" + c + "'"); + planes.Add(plane); + break; } - } while (index++ < data.Length && !brushParsed); - brush.planes = planes.ToArray(); + case '}': + { + brushParsed = true; + break; + } - return brush; + default: + if (char.IsLetter(c) || char.IsNumber(c)) + { + // patch name + } + + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !brushParsed); + + brush.planes = planes.ToArray(); + + return brush; + } + + private static MapPatch ParsePatch(byte[] data, ref int index) + { + MapPatch patch = new MapPatch(); + + patch.name = ParseString(data, ref index); + + bool patchParsed = false; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) + { + case '\r': + case '\n': + break; + + case '{': + { + index++; + ParsePatchInner(patch, data, ref index); + break; + } + + case '}': + { + patchParsed = true; + break; + } + + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !patchParsed); + + return patch; + } + + // unfinished and untested + private static void ParsePatchInner(MapPatch patch, byte[] data, ref int index) + { + string shaderName = ParseString(data, ref index); + + while (index < data.Length) + { + if (data[index] == '(') + break; + index++; } - private static MapPatch ParsePatch(byte[] data, ref int index) + index++; + + int width = ParseInt(data, ref index); + int height = ParseInt(data, ref index); + int dummy1 = ParseInt(data, ref index); + int dummy2 = ParseInt(data, ref index); + int dummy3 = ParseInt(data, ref index); + + while (index < data.Length) { - MapPatch patch = new MapPatch(); - - patch.name = ParseString(data, ref index); - - bool patchParsed = false; - do - { - char c = (char)data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) - { - case '\r': - case '\n': - break; - - case '{': - { - index++; - ParsePatchInner(patch, data, ref index); - break; - } - - case '}': - { - patchParsed = true; - break; - } - - default: - throw new Exception("unsupported character: '" + c + "'"); - } - } while (index++ < data.Length && !patchParsed); - - return patch; + if (data[index] == ')') + break; + index++; } - // unfinished and untested - private static void ParsePatchInner(MapPatch patch, byte[] data, ref int index) + index++; + + while (index < data.Length) { - string shaderName = ParseString(data, ref index); - - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - + if (data[index] == '(') + break; index++; + } - int width = ParseInt(data, ref index); - int height = ParseInt(data, ref index); - int dummy1 = ParseInt(data, ref index); - int dummy2 = ParseInt(data, ref index); - int dummy3 = ParseInt(data, ref index); + index++; - while (index < data.Length) + var vertices = new PatchVertex[width * height]; + + bool verticesParsed = false; + int vertexIndex = 0; + do + { + char c = (char)data[index]; + //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; + + switch (c) { - if (data[index] == ')') + case '\r': + case '\n': break; - index++; - } - index++; - - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - - index++; - - var vertices = new PatchVertex[width * height]; - - bool verticesParsed = false; - int vertexIndex = 0; - do - { - char c = (char)data[index]; - //char c1 = (index + 1 < data.Length) ? (char) data[index + 1] : (char) 0; - - switch (c) + case '(': { - case '\r': - case '\n': - break; + index++; - case '(': + for (int iw = 0; iw < width; iw++) { - index++; - - for (int iw = 0; iw < width; iw++) + while (index < data.Length) { - while (index < data.Length) - { - if (data[index] == '(') - break; - index++; - } - - index++; - - while (index < data.Length) - { - if (data[index] != ' ') - break; - index++; - } - - vertices[vertexIndex].v = ParseFloat3(data, ref index); - vertices[vertexIndex].uv = - new Float2(ParseFloat(data, ref index), ParseFloat(data, ref index)); - vertexIndex++; - - while (index < data.Length) - { - if (data[index] == ')') - break; - index++; - } - + if (data[index] == '(') + break; index++; } + index++; - break; + while (index < data.Length) + { + if (data[index] != ' ') + break; + index++; + } + + vertices[vertexIndex].v = ParseFloat3(data, ref index); + vertices[vertexIndex].uv = + new Float2(ParseFloat(data, ref index), ParseFloat(data, ref index)); + vertexIndex++; + + while (index < data.Length) + { + if (data[index] == ')') + break; + index++; + } + + index++; } - case '}': - { - verticesParsed = true; - break; - } - default: - throw new Exception("unsupported character: '" + c + "'"); + break; } - } while (index++ < data.Length && !verticesParsed); - } + + case '}': + { + verticesParsed = true; + break; + } + + default: + throw new Exception("unsupported character: '" + c + "'"); + } + } while (index++ < data.Length && !verticesParsed); } } \ No newline at end of file diff --git a/Source/Game/Level/Q3MapImporter.cs b/Source/Game/Level/Q3MapImporter.cs index 0545847..e6cf71b 100644 --- a/Source/Game/Level/Q3MapImporter.cs +++ b/Source/Game/Level/Q3MapImporter.cs @@ -17,622 +17,1026 @@ using FlaxEngine.GUI; using Console = Game.Console; using Debug = FlaxEngine.Debug; -namespace Game +namespace Game; + +public class BrushGeometryMesh { - public class BrushGeometryMesh + public List indices = new List(); + public MaterialBase material; + public List normals = new List(); + public List uvs = new List(); + public List vertices = new List(); +} + +public class BrushGeometry +{ + public MapBrush brush; + public Dictionary brushMaterials; + public BrushGeometryMesh[] meshes; + public Model model; + public Float3 offset; + public Float3[] vertices; // all vertices +} + +[ExecuteInEditMode] +public class Q3MapImporter : Script +{ + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map";u8 + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q3.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_valve.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\dm4.map"; + + public string mapPath;// = @"C:\dev\Goake\maps\aerowalk\aerowalk.map"; + //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map"; + + public bool importLights = true; + private bool generateSdf = true; + private bool childModelSdf = true; + + private Model model; + private MaterialBase missingMaterial; + + private bool resetLights = false; + private bool dirtyLights = false; + private bool dirtyMap = false; + + private float brightnessMultiplier_ = 0.82f; + private float lightRadiusMultiplier_ = 9.45f; + private float fallOffExponent_ = 2.0f; + private float saturationMultiplier_ = 1.0f; + private float indirectLightMultiplier_ = 1.0f; + private List lightEnts = new List(); + private Actor worldSpawnActor = null; + + [Range(0.1f, 4f)] + public float BrightnessMultiplier { - public List indices = new List(); - public MaterialBase material; - public List normals = new List(); - public List uvs = new List(); - public List vertices = new List(); + get => brightnessMultiplier_; + set { brightnessMultiplier_ = value; resetLights = true; } } - public class BrushGeometry + [Range(0.1f, 40f)] + public float LightRadiusMultiplier { - public MapBrush brush; - public Dictionary brushMaterials; - public BrushGeometryMesh[] meshes; - public Model model; - public Float3 offset; - public Float3[] vertices; // all vertices + get => lightRadiusMultiplier_; + set { lightRadiusMultiplier_ = value; resetLights = true; } } - [ExecuteInEditMode] - public class Q3MapImporter : Script + [Range(2f, 8f)] + public float FallOffExponent { - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q1.map";u8 - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_q3.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube_valve.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\dm4.map"; + get => fallOffExponent_; + set { fallOffExponent_ = value; resetLights = true; } + } - public string mapPath;// = @"C:\dev\Goake\maps\aerowalk\aerowalk.map"; - //private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\problematic.map"; + [Range(0.01f, 1f)] + public float SaturationMultiplier + { + get => saturationMultiplier_; + set { saturationMultiplier_ = value; resetLights = true; } + } - public bool importLights = true; - private bool generateSdf = true; - private bool childModelSdf = true; + [Range(1f, 100f)] + public float IndirectLightMultiplier + { + get => indirectLightMultiplier_; + set { indirectLightMultiplier_ = value; resetLights = true; } + } - private Model model; - private MaterialBase missingMaterial; - - private bool resetLights = false; - private bool dirtyLights = false; - private bool dirtyMap = false; - - private float brightnessMultiplier_ = 0.82f; - private float lightRadiusMultiplier_ = 9.45f; - private float fallOffExponent_ = 2.0f; - private float saturationMultiplier_ = 1.0f; - private float indirectLightMultiplier_ = 1.0f; - private List lightEnts = new List(); - private Actor worldSpawnActor = null; - - [Range(0.1f, 4f)] - public float BrightnessMultiplier + public bool StaticBatching + { + get => staticBatching; + set { - get => brightnessMultiplier_; - set { brightnessMultiplier_ = value; resetLights = true; } + if (staticBatching == value) + return; + staticBatching = value; + dirtyLights = true; + dirtyMap = true; + } + } + + public bool LoadCollidersOnly + { + get => collidersOnly; + set + { + if (collidersOnly == value) + return; + collidersOnly = value; + dirtyLights = true; + dirtyMap = true; + } + } + + + private static void QuickHull(Float3[] points, out Float3[] outVertices) + { + var verts = new List(); + var tris = new List(); + var normals = new List(); + + ConvexHullCalculator calc = new ConvexHullCalculator(); + calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); + + var finalPoints = new List(); + + foreach (int tri in tris) + finalPoints.Add(verts[tri]); + + outVertices = finalPoints.ToArray(); + + //verts = new QuickHull().QuickHull2(points); + //outVertices = verts.ToArray(); frf f + } + + private MapEntity root; + + private static IEnumerable> DifferentCombinations(IEnumerable elements, int k) + { + return k == 0 + ? new[] { new T[0] } + : elements.SelectMany((e, i) => + DifferentCombinations(elements.Skip(i + 1), k - 1).Select(c => new[] { e }.Concat(c))); + } + + /// + /// Triangulates the brush by calculating intersection points between triplets of planes. + /// Does not work well with off-axis aligned planes. + /// + public static void TriangulateBrush(MapBrush brush, out Float3[] vertices) + { + var planePoints = new HashSet(); + + var planes = new List(); + float maxDist = 0f; + foreach (MapFacePlane brushPlane in brush.planes) + { + if (Mathf.Abs(brushPlane.plane.D) > maxDist) + maxDist = Mathf.Abs(brushPlane.plane.D); + planes.Add(brushPlane.plane); } - [Range(0.1f, 40f)] - public float LightRadiusMultiplier + maxDist *= Mathf.Sqrt(3); + + var combinations = DifferentCombinations(planes, 3).ToList(); + + // pass 1: get all intersection points + foreach (var comb in combinations) { - get => lightRadiusMultiplier_; - set { lightRadiusMultiplier_ = value; resetLights = true; } + Plane p1 = comb.Skip(0).First(); + Plane p2 = comb.Skip(1).First(); + Plane p3 = comb.Skip(2).First(); + + //var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D)); + + // intersection of three planes + double denom = Float3.Dot(p1.Normal, Float3.Cross(p2.Normal, p3.Normal)); + //if (denom < 0.0000000001) + // continue; + + + Float3 intersection = (Float3.Cross(p2.Normal, p3.Normal) * -p1.D + + Float3.Cross(p3.Normal, p1.Normal) * -p2.D + + Float3.Cross(p1.Normal, p2.Normal) * -p3.D) / (float)denom; + + if (Mathf.Abs(intersection.X) > maxDist * 1f || Mathf.Abs(intersection.Y) > maxDist * 1f || + Mathf.Abs(intersection.Z) > maxDist * 1f) + continue; + + if (Math.Abs(denom) < 0.0000000001) + continue; + //if (intersection.Length > maxDist*2f) + // continue; + + // Flip Y and Z + /*var temp = intersection.Y; + intersection.Y = intersection.Z; + intersection.Z = temp;*/ + + //if (intersection.Length >= maxDist) + // temp = temp; + + planePoints.Add(intersection); } - [Range(2f, 8f)] - public float FallOffExponent + // remove duplicate points + var planePoints3 = planePoints; + planePoints = new HashSet(); + + foreach (Float3 p1 in planePoints3) { - get => fallOffExponent_; - set { fallOffExponent_ = value; resetLights = true; } + bool found = false; + foreach (Float3 p2 in planePoints) + if (Mathf.Abs((p1 - p2).Length) < 0.00001f) + { + found = true; + break; + } + + if (!found) + planePoints.Add(p1); } - [Range(0.01f, 1f)] - public float SaturationMultiplier + if (planePoints.Count != planePoints3.Count) + Console.Print("culled " + (planePoints3.Count - planePoints.Count) + " points while triangulation"); + + // pass 2: cull points behind clipping planes + var planePoints2 = planePoints; + planePoints = new HashSet(); + + foreach (Float3 p in planePoints2) { - get => saturationMultiplier_; - set { saturationMultiplier_ = value; resetLights = true; } - } - - [Range(1f, 100f)] - public float IndirectLightMultiplier - { - get => indirectLightMultiplier_; - set { indirectLightMultiplier_ = value; resetLights = true; } - } - - public bool StaticBatching - { - get => staticBatching; - set - { - if (staticBatching == value) - return; - staticBatching = value; - dirtyLights = true; - dirtyMap = true; - } - } - - public bool LoadCollidersOnly - { - get => collidersOnly; - set - { - if (collidersOnly == value) - return; - collidersOnly = value; - dirtyLights = true; - dirtyMap = true; - } - } - - - private static void QuickHull(Float3[] points, out Float3[] outVertices) - { - var verts = new List(); - var tris = new List(); - var normals = new List(); - - ConvexHullCalculator calc = new ConvexHullCalculator(); - calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); - - var finalPoints = new List(); - - foreach (int tri in tris) - finalPoints.Add(verts[tri]); - - outVertices = finalPoints.ToArray(); - - //verts = new QuickHull().QuickHull2(points); - //outVertices = verts.ToArray(); frf f - } - - private MapEntity root; - - private static IEnumerable> DifferentCombinations(IEnumerable elements, int k) - { - return k == 0 - ? new[] { new T[0] } - : elements.SelectMany((e, i) => - DifferentCombinations(elements.Skip(i + 1), k - 1).Select(c => new[] { e }.Concat(c))); - } - - /// - /// Triangulates the brush by calculating intersection points between triplets of planes. - /// Does not work well with off-axis aligned planes. - /// - public static void TriangulateBrush(MapBrush brush, out Float3[] vertices) - { - var planePoints = new HashSet(); - - var planes = new List(); - float maxDist = 0f; + bool front = true; foreach (MapFacePlane brushPlane in brush.planes) { - if (Mathf.Abs(brushPlane.plane.D) > maxDist) - maxDist = Mathf.Abs(brushPlane.plane.D); - planes.Add(brushPlane.plane); - } + float dot = -Plane.DotCoordinate(brushPlane.plane, p); - maxDist *= Mathf.Sqrt(3); - - var combinations = DifferentCombinations(planes, 3).ToList(); - - // pass 1: get all intersection points - foreach (var comb in combinations) - { - Plane p1 = comb.Skip(0).First(); - Plane p2 = comb.Skip(1).First(); - Plane p3 = comb.Skip(2).First(); - - //var maxDist = Math.Abs(p1.D * p2.D * p3.D);//Math.Max(p1.D, Math.Max(p2.D, p3.D)); - - // intersection of three planes - double denom = Float3.Dot(p1.Normal, Float3.Cross(p2.Normal, p3.Normal)); - //if (denom < 0.0000000001) - // continue; - - - Float3 intersection = (Float3.Cross(p2.Normal, p3.Normal) * -p1.D + - Float3.Cross(p3.Normal, p1.Normal) * -p2.D + - Float3.Cross(p1.Normal, p2.Normal) * -p3.D) / (float)denom; - - if (Mathf.Abs(intersection.X) > maxDist * 1f || Mathf.Abs(intersection.Y) > maxDist * 1f || - Mathf.Abs(intersection.Z) > maxDist * 1f) - continue; - - if (Math.Abs(denom) < 0.0000000001) - continue; - //if (intersection.Length > maxDist*2f) - // continue; - - // Flip Y and Z - /*var temp = intersection.Y; - intersection.Y = intersection.Z; - intersection.Z = temp;*/ - - //if (intersection.Length >= maxDist) - // temp = temp; - - planePoints.Add(intersection); - } - - // remove duplicate points - var planePoints3 = planePoints; - planePoints = new HashSet(); - - foreach (Float3 p1 in planePoints3) - { - bool found = false; - foreach (Float3 p2 in planePoints) - if (Mathf.Abs((p1 - p2).Length) < 0.00001f) - { - found = true; - break; - } - - if (!found) - planePoints.Add(p1); - } - - if (planePoints.Count != planePoints3.Count) - Console.Print("culled " + (planePoints3.Count - planePoints.Count) + " points while triangulation"); - - // pass 2: cull points behind clipping planes - var planePoints2 = planePoints; - planePoints = new HashSet(); - - foreach (Float3 p in planePoints2) - { - bool front = true; - foreach (MapFacePlane brushPlane in brush.planes) + if (dot < -0.01f) { - float dot = -Plane.DotCoordinate(brushPlane.plane, p); - - if (dot < -0.01f) - { - front = false; - break; - } - } - - if (front) - planePoints.Add(p); - } - - if (planePoints.Count > 0) - { - QuickHull(planePoints.ToArray(), out vertices); - return; - - } - - vertices = new Float3[0]; - } - -#if FLAX_EDITOR - private void OnEditorPlayModeStart() - { - try - { - if (worldSpawnActor) - worldSpawnActor.HideFlags &= ~HideFlags.DontSave; - } - catch (Exception e) - { - FlaxEngine.Debug.Log("OnEditorPlayModeStart error: " + e.Message); - } - } - - private void OnEditorPlayModeEnd() - { - try - { - if (worldSpawnActor) - worldSpawnActor.HideFlags |= HideFlags.DontSave; - dirtyLights = true; - } - catch (Exception e) - { - FlaxEngine.Debug.Log("OnEditorPlayModeEnd error: " + e.Message); - } - } - - public override void OnEnable() - { - Editor.Instance.PlayModeBeginning += OnEditorPlayModeStart; - Editor.Instance.PlayModeEnd += OnEditorPlayModeEnd; - } - -#if FLAX_EDITOR - private void OnSceneUnloading(Scene scene, Guid sceneId) - { - if (Editor.Instance == null) - return; - if (Editor.Instance.StateMachine.CurrentState is FlaxEditor.States.ChangingScenesState) - //if (!Editor.IsPlayMode) - UnloadMap(); - } -#endif - - public override void OnDisable() - { - if (Editor.Instance == null) - return; - Editor.Instance.PlayModeBeginning -= OnEditorPlayModeStart; - Editor.Instance.PlayModeEnd -= OnEditorPlayModeEnd; - - //UnloadMap(); - } -#endif - public override void OnStart() - { - sceneLighting = lastSceneLighting = EngineSubsystem.SceneLighting == "1"; - sceneShadows = lastSceneShadows = EngineSubsystem.SceneShadows == "1"; - staticBatching = lastStaticBatching = EngineSubsystem.StaticBatch == "1"; - globalIllumination = EngineSubsystem.GlobalIllumination == "1"; - - LoadMap(false); - } - - private List brushGeometries; - private bool lastSceneLighting = false; - private bool lastSceneShadows = false; - private bool lastStaticBatching = false; - private bool lastGlobalIllumination = false; - private bool sceneLighting = false; - private bool sceneShadows = false; - private bool staticBatching = false; - private bool collidersOnly = false; - private bool globalIllumination = false; - public override void OnUpdate() - { - sceneLighting = EngineSubsystem.SceneLighting == "1"; - if (lastSceneLighting != sceneLighting) - { - lastSceneLighting = sceneLighting; - dirtyLights = true; - } - sceneShadows = EngineSubsystem.SceneShadows == "1"; - if (lastSceneShadows != sceneShadows) - { - lastSceneShadows = sceneShadows; - dirtyLights = true; - } - var staticBatching = EngineSubsystem.StaticBatch == "1"; - if (lastStaticBatching != staticBatching) - { - lastStaticBatching = staticBatching; - StaticBatching = staticBatching; - } - globalIllumination = EngineSubsystem.GlobalIllumination == "1"; - if (lastGlobalIllumination != globalIllumination) - { - lastGlobalIllumination = globalIllumination; - dirtyMap = true; - } - - if (resetLights) - { - resetLights = false; - if (worldSpawnActor == null || !worldSpawnActor || root == null) - { - Debug.Log("worldspawn or root is null"); - return; - } - - foreach (var light in worldSpawnActor.GetChildren()) - Destroy(light); - - int lightIndex = 0; - foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) - switch (entity.properties["classname"]) - { - case "light": - if (importLights) - ParseLight(entity, ref lightIndex); - break; - //case "info_player_deathmatch": - // ParsePlayerSpawn(entity, ref playerSpawnIndex); - // break; - } - } - - if (dirtyMap) - { - dirtyMap = false; - FlaxEngine.Debug.Log("StaticBatching changed, reloading map"); - LoadMap(true); - } - - if (dirtyLights) - { - dirtyLights = false; - foreach (var light in worldSpawnActor.GetChildren()) - { - light.IsActive = sceneLighting; - if (light is PointLight pointLight) - pointLight.ShadowsStrength = sceneShadows ? 1.0f : 0.0f; + front = false; + break; } } + + if (front) + planePoints.Add(p); } - private static bool IsMapDirty(Actor worldSpawnActor, string mapPath) + if (planePoints.Count > 0) { -#if FLAX_EDITOR - LevelScript levelScript = worldSpawnActor.GetScript(); - - if (levelScript.MapName != mapPath) - return true; - - DateTime timestamp = File.GetLastWriteTime(mapPath); - if (timestamp != levelScript.MapTimestamp) - return true; -#endif - return false; + QuickHull(planePoints.ToArray(), out vertices); + return; + } - private void UnloadMap() - { - if (!worldSpawnActor) - return; - - var virtualAssets = new List(); - var allActors = GetChildrenRecursive(worldSpawnActor).ToList(); - virtualAssets.AddRange(allActors.OfType().Where(x => x.Model != null && x.Model.IsVirtual).Select(x => x.Model)); - virtualAssets.AddRange(allActors.OfType().Where(x => x.CollisionData != null && x.CollisionData.IsVirtual).Select(x => x.CollisionData)); - - foreach (var asset in virtualAssets) - Content.UnloadAsset(asset); - - worldSpawnActor.DestroyChildren(); - FlaxEngine.Object.Destroy(worldSpawnActor); - worldSpawnActor = null; - resetLights = false; + vertices = new Float3[0]; + } #if FLAX_EDITOR - Level.SceneUnloading -= OnSceneUnloading; -#endif - - IEnumerable GetChildrenRecursive(Actor actor) - { - foreach (var act in actor.GetChildren()) - { - yield return act; - - foreach (var child in GetChildrenRecursive(act)) - yield return child; - } - } - } - - private void LoadMap(bool forceLoad) + private void OnEditorPlayModeStart() + { + try { - if (string.IsNullOrEmpty(mapPath)) - return; - - Stopwatch sw = Stopwatch.StartNew(); - - - - - string mapPath_ = mapPath; - if (!File.Exists(mapPath_)) - mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), mapPath); - if (!File.Exists(mapPath_)) - mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", mapPath); - - byte[] mapChars = File.ReadAllBytes(mapPath_); - root = MapParser.Parse(mapChars); - sw.Stop(); - //Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); - - dirtyMap = false; - - worldSpawnActor = Actor.FindActor("WorldSpawn"); - if (worldSpawnActor != null) - { - if (!forceLoad && !IsMapDirty(worldSpawnActor, mapPath)) - return; - - FlaxEngine.Debug.Log($"Map dirty, reloading."); - UnloadMap(); - } - -#if FLAX_EDITOR - Level.SceneUnloading += OnSceneUnloading; -#endif - //else - // FlaxEngine.Debug.Log("No WorldSpawn, loading map"); - - bool oneMesh = false; - bool useStaticBatching = StaticBatching && !LoadCollidersOnly; - bool convexMesh = true; - - - FlaxEngine.Debug.Log("Loading map, static batching: " + useStaticBatching); - { - string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); - string assetPath = Path.Combine(matBasePath, "missing.flax"); - missingMaterial = Content.Load(assetPath); - } - - ConcurrentBag sdfModels = new ConcurrentBag(); - - - - if (worldSpawnActor == null) - { - worldSpawnActor = Actor.AddChild(); - worldSpawnActor.Name = "WorldSpawn"; + if (worldSpawnActor) worldSpawnActor.HideFlags &= ~HideFlags.DontSave; - //worldSpawnActor.HideFlags |= HideFlags.DontSave; - //worldSpawnActor.HideFlags |= HideFlags.DontSelect; - } + } + catch (Exception e) + { + FlaxEngine.Debug.Log("OnEditorPlayModeStart error: " + e.Message); + } + } - LevelScript levelScript = - worldSpawnActor.GetScript() ?? worldSpawnActor.AddScript(); + private void OnEditorPlayModeEnd() + { + try + { + if (worldSpawnActor) + worldSpawnActor.HideFlags |= HideFlags.DontSave; + dirtyLights = true; + } + catch (Exception e) + { + FlaxEngine.Debug.Log("OnEditorPlayModeEnd error: " + e.Message); + } + } + + public override void OnEnable() + { + Editor.Instance.PlayModeBeginning += OnEditorPlayModeStart; + Editor.Instance.PlayModeEnd += OnEditorPlayModeEnd; + } #if FLAX_EDITOR - levelScript.MapTimestamp = File.GetLastWriteTime(mapPath); - levelScript.MapName = mapPath; + private void OnSceneUnloading(Scene scene, Guid sceneId) + { + if (Editor.Instance == null) + return; + if (Editor.Instance.StateMachine.CurrentState is FlaxEditor.States.ChangingScenesState) + //if (!Editor.IsPlayMode) + UnloadMap(); + } #endif - if (!oneMesh) + public override void OnDisable() + { + if (Editor.Instance == null) + return; + Editor.Instance.PlayModeBeginning -= OnEditorPlayModeStart; + Editor.Instance.PlayModeEnd -= OnEditorPlayModeEnd; + + //UnloadMap(); + } +#endif + public override void OnStart() + { + sceneLighting = lastSceneLighting = EngineSubsystem.SceneLighting == "1"; + sceneShadows = lastSceneShadows = EngineSubsystem.SceneShadows == "1"; + staticBatching = lastStaticBatching = EngineSubsystem.StaticBatch == "1"; + globalIllumination = EngineSubsystem.GlobalIllumination == "1"; + + LoadMap(false); + } + + private List brushGeometries; + private bool lastSceneLighting = false; + private bool lastSceneShadows = false; + private bool lastStaticBatching = false; + private bool lastGlobalIllumination = false; + private bool sceneLighting = false; + private bool sceneShadows = false; + private bool staticBatching = false; + private bool collidersOnly = false; + private bool globalIllumination = false; + public override void OnUpdate() + { + sceneLighting = EngineSubsystem.SceneLighting == "1"; + if (lastSceneLighting != sceneLighting) + { + lastSceneLighting = sceneLighting; + dirtyLights = true; + } + sceneShadows = EngineSubsystem.SceneShadows == "1"; + if (lastSceneShadows != sceneShadows) + { + lastSceneShadows = sceneShadows; + dirtyLights = true; + } + var staticBatching = EngineSubsystem.StaticBatch == "1"; + if (lastStaticBatching != staticBatching) + { + lastStaticBatching = staticBatching; + StaticBatching = staticBatching; + } + globalIllumination = EngineSubsystem.GlobalIllumination == "1"; + if (lastGlobalIllumination != globalIllumination) + { + lastGlobalIllumination = globalIllumination; + dirtyMap = true; + } + + if (resetLights) + { + resetLights = false; + if (worldSpawnActor == null || !worldSpawnActor || root == null) { - var materials = new Dictionary(); + Debug.Log("worldspawn or root is null"); + return; + } + + foreach (var light in worldSpawnActor.GetChildren()) + Destroy(light); + + int lightIndex = 0; + foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) + switch (entity.properties["classname"]) { - BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") - ?.CreateInstance(); - if (brushMaterialList != null) - foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets) - materials.Add(m.name, m.asset); + case "light": + if (importLights) + ParseLight(entity, ref lightIndex); + break; + //case "info_player_deathmatch": + // ParsePlayerSpawn(entity, ref playerSpawnIndex); + // break; + } + } + + if (dirtyMap) + { + dirtyMap = false; + FlaxEngine.Debug.Log("StaticBatching changed, reloading map"); + LoadMap(true); + } + + if (dirtyLights) + { + dirtyLights = false; + foreach (var light in worldSpawnActor.GetChildren()) + { + light.IsActive = sceneLighting; + if (light is PointLight pointLight) + pointLight.ShadowsStrength = sceneShadows ? 1.0f : 0.0f; + } + } + } + + private static bool IsMapDirty(Actor worldSpawnActor, string mapPath) + { +#if FLAX_EDITOR + LevelScript levelScript = worldSpawnActor.GetScript(); + + if (levelScript.MapName != mapPath) + return true; + + DateTime timestamp = File.GetLastWriteTime(mapPath); + if (timestamp != levelScript.MapTimestamp) + return true; +#endif + return false; + } + + private void UnloadMap() + { + if (!worldSpawnActor) + return; + + var virtualAssets = new List(); + var allActors = GetChildrenRecursive(worldSpawnActor).ToList(); + virtualAssets.AddRange(allActors.OfType().Where(x => x.Model != null && x.Model.IsVirtual).Select(x => x.Model)); + virtualAssets.AddRange(allActors.OfType().Where(x => x.CollisionData != null && x.CollisionData.IsVirtual).Select(x => x.CollisionData)); + + foreach (var asset in virtualAssets) + Content.UnloadAsset(asset); + + worldSpawnActor.DestroyChildren(); + FlaxEngine.Object.Destroy(worldSpawnActor); + worldSpawnActor = null; + resetLights = false; + +#if FLAX_EDITOR + Level.SceneUnloading -= OnSceneUnloading; +#endif + + IEnumerable GetChildrenRecursive(Actor actor) + { + foreach (var act in actor.GetChildren()) + { + yield return act; + + foreach (var child in GetChildrenRecursive(act)) + yield return child; + } + } + } + + private void LoadMap(bool forceLoad) + { + if (string.IsNullOrEmpty(mapPath)) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + + + + string mapPath_ = mapPath; + if (!File.Exists(mapPath_)) + mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), mapPath); + if (!File.Exists(mapPath_)) + mapPath_ = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", mapPath); + + byte[] mapChars = File.ReadAllBytes(mapPath_); + root = MapParser.Parse(mapChars); + sw.Stop(); + //Console.Print("Map parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); + + dirtyMap = false; + + worldSpawnActor = Actor.FindActor("WorldSpawn"); + if (worldSpawnActor != null) + { + if (!forceLoad && !IsMapDirty(worldSpawnActor, mapPath)) + return; + + FlaxEngine.Debug.Log($"Map dirty, reloading."); + UnloadMap(); + } + +#if FLAX_EDITOR + Level.SceneUnloading += OnSceneUnloading; +#endif + //else + // FlaxEngine.Debug.Log("No WorldSpawn, loading map"); + + bool oneMesh = false; + bool useStaticBatching = StaticBatching && !LoadCollidersOnly; + bool convexMesh = true; + + + FlaxEngine.Debug.Log("Loading map, static batching: " + useStaticBatching); + { + string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); + string assetPath = Path.Combine(matBasePath, "missing.flax"); + missingMaterial = Content.Load(assetPath); + } + + ConcurrentBag sdfModels = new ConcurrentBag(); + + + + if (worldSpawnActor == null) + { + worldSpawnActor = Actor.AddChild(); + worldSpawnActor.Name = "WorldSpawn"; + worldSpawnActor.HideFlags &= ~HideFlags.DontSave; + //worldSpawnActor.HideFlags |= HideFlags.DontSave; + //worldSpawnActor.HideFlags |= HideFlags.DontSelect; + } + + LevelScript levelScript = + worldSpawnActor.GetScript() ?? worldSpawnActor.AddScript(); + +#if FLAX_EDITOR + levelScript.MapTimestamp = File.GetLastWriteTime(mapPath); + levelScript.MapName = mapPath; +#endif + + if (!oneMesh) + { + var materials = new Dictionary(); + { + BrushMaterialList brushMaterialList = Engine.GetCustomSettings("BrushMaterialsLegacy") + ?.CreateInstance(); + if (brushMaterialList != null) + foreach (BrushMaterialListEntry m in brushMaterialList.materialAssets) + materials.Add(m.name, m.asset); + } + + brushGeometries = new List(root.entities[0].brushes.Count); + + // pass 1: triangulation + sw.Restart(); + int brushIndex = 0; + int totalverts = 0; + foreach (MapBrush brush in root.entities[0].brushes) + { + try + { + BrushGeometry geom = new BrushGeometry(); + + TriangulateBrush(brush, out geom.vertices); + geom.brush = brush; + + brushGeometries.Add(geom); + totalverts += geom.vertices.Length; + + Assert.IsTrue(geom.vertices.Length > 0); + + foreach (Float3 vert in geom.vertices) + geom.offset += vert; + geom.offset /= geom.vertices.Length; + + for (int i = 0; i < geom.vertices.Length; i++) + geom.vertices[i] -= geom.offset; + + var brushMaterials = new Dictionary(); + foreach (MapFacePlane brushPlane in geom.brush.planes) + { + string textureName = brushPlane.texture; + if (brushMaterials.ContainsKey(textureName)) + continue; + + if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial)) + { + string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); + string assetPath = Path.Combine(matBasePath, textureName + ".flax"); + brushMaterial = Content.Load(assetPath); + if (brushMaterial != null) + { + materials.Add(textureName, brushMaterial); + } + else + { + // TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate + Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath); + materials.Add(textureName, missingMaterial); + brushMaterial = missingMaterial; + } + } + + brushMaterials.Add(textureName, brushMaterial); + } + + geom.brushMaterials = brushMaterials; + geom.meshes = new BrushGeometryMesh[brushMaterials.Count]; + for (int i = 0; i < geom.meshes.Length; i++) + { + geom.meshes[i] = new BrushGeometryMesh(); + geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]]; + } + } + catch (Exception e) + { + Console.Print("Failed to triangulate brush " + brushIndex + ": " + e.Message); + //FlaxEngine.Engine.RequestExit(); } - brushGeometries = new List(root.entities[0].brushes.Count); + brushIndex++; + } - // pass 1: triangulation - sw.Restart(); - int brushIndex = 0; - int totalverts = 0; - foreach (MapBrush brush in root.entities[0].brushes) + sw.Stop(); + //Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms"); + + // pass 2: texturing + brushIndex = 0; + sw.Restart(); + foreach (BrushGeometry geom in brushGeometries) + { + var brushVertices = geom.vertices; + for (int i = 0; i < brushVertices.Length; i += 3) { - try - { - BrushGeometry geom = new BrushGeometry(); + Float3 v1 = brushVertices[i + 0]; + Float3 v2 = brushVertices[i + 1]; + Float3 v3 = brushVertices[i + 2]; - TriangulateBrush(brush, out geom.vertices); - geom.brush = brush; + Float3 normal = -Float3.Cross(v3 - v1, v2 - v1).Normalized; - brushGeometries.Add(geom); - totalverts += geom.vertices.Length; - - Assert.IsTrue(geom.vertices.Length > 0); - - foreach (Float3 vert in geom.vertices) - geom.offset += vert; - geom.offset /= geom.vertices.Length; - - for (int i = 0; i < geom.vertices.Length; i++) - geom.vertices[i] -= geom.offset; - - var brushMaterials = new Dictionary(); - foreach (MapFacePlane brushPlane in geom.brush.planes) + // fetch the texture parameters from the plane with matching normal + Float2 uvScale = new Float2(0f); + float uvRotation = 0f; + Float2 uvOffset = new Float2(0f); + bool found = false; + int meshIndex = 0; + foreach (MapFacePlane brushPlane in geom.brush.planes) + if ((brushPlane.plane.Normal - normal).Length < 0.01f) { - string textureName = brushPlane.texture; - if (brushMaterials.ContainsKey(textureName)) - continue; - - if (!materials.TryGetValue(textureName, out MaterialBase brushMaterial)) - { - string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); - string assetPath = Path.Combine(matBasePath, textureName + ".flax"); - brushMaterial = Content.Load(assetPath); - if (brushMaterial != null) - { - materials.Add(textureName, brushMaterial); - } - else - { - // TODO: engine doesn't seem to always load the asset even though it exists, bug? seems to happen at low framerate - Console.Print("Material '" + textureName + "' not found for brush, assetPath: " + assetPath); - materials.Add(textureName, missingMaterial); - brushMaterial = missingMaterial; - } - } - - brushMaterials.Add(textureName, brushMaterial); + normal = brushPlane.plane.Normal; // for consistency + uvScale = 1f / brushPlane.scale; + uvRotation = brushPlane.rotation; + uvOffset = brushPlane.offset * brushPlane.scale; + found = true; + meshIndex = geom.brushMaterials.Keys.ToList().IndexOf(brushPlane.texture); // ugh? + break; } - geom.brushMaterials = brushMaterials; - geom.meshes = new BrushGeometryMesh[brushMaterials.Count]; + if (!found) + Console.Print("no matching plane found for brush " + brushIndex + ", bad geometry?"); + + Float2 uv1, uv2, uv3; + // if quake format + { + // The texture is projected to the surface from three angles, the axis with least + // distortion is chosen here. + + // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. + // This behaviour is seemingly random in different engines and editors, so let's not bother. + + Float3 textureNormal = new Float3((float)Math.Round(normal.X, 4), + (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); + + float dotX = Math.Abs(Float3.Dot(textureNormal, Float3.Right)); + float dotY = Math.Abs(Float3.Dot(textureNormal, Float3.Up)); + float dotZ = Math.Abs(Float3.Dot(textureNormal, Float3.Forward)); + + Float3 axis; + if (dotY >= dotX && dotY >= dotZ) + axis = -Float3.Up; + else if (dotX >= dotY && dotX >= dotZ) + axis = Float3.Right; + else if (dotZ >= dotX && dotZ >= dotY) + axis = -Float3.Forward; + else + axis = Float3.Right; + + Float3 axisForward = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f + ? -Float3.Forward + : Float3.Up; + Float3 axisForward2 = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f + ? Float3.Up + : -Float3.Forward; + + Quaternion rot = Quaternion.Identity; + rot = rot * Quaternion.LookRotation(axis, axisForward); + rot = rot * Quaternion.RotationAxis(-Float3.Forward, + 180f * Mathf.DegreesToRadians); + rot = rot * Quaternion.RotationAxis( + Mathf.Abs(Float3.Dot(axis, Float3.Right)) > 0.01f + ? Float3.Right + : axisForward2, + uvRotation * Mathf.DegreesToRadians); + + uv1 = ((Float2)((v1 + geom.offset) * rot) + uvOffset) * uvScale; + uv2 = ((Float2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; + uv3 = ((Float2)((v3 + geom.offset) * rot) + uvOffset) * uvScale; + } + BrushGeometryMesh mesh = geom.meshes[meshIndex]; + + mesh.indices.Add((uint)mesh.vertices.Count + 0); + mesh.indices.Add((uint)mesh.vertices.Count + 1); + mesh.indices.Add((uint)mesh.vertices.Count + 2); + + mesh.vertices.Add(v1); + mesh.vertices.Add(v2); + mesh.vertices.Add(v3); + + mesh.uvs.Add(uv1); + mesh.uvs.Add(uv2); + mesh.uvs.Add(uv3); + + mesh.normals.Add(normal); + mesh.normals.Add(normal); + mesh.normals.Add(normal); + } + + geom.model = Content.CreateVirtualAsset(); + geom.model.SetupLODs(new[] { geom.meshes.Length }); + geom.model.SetupMaterialSlots(geom.meshes.Length); + + for (int i = 0; i < geom.meshes.Length; i++) + { + BrushGeometryMesh mesh = geom.meshes[i]; + if (mesh.vertices.Count == 0) + continue; + + geom.model.LODs[0].Meshes[i].UpdateMesh(mesh.vertices, mesh.indices, mesh.normals, + null, mesh.uvs); + geom.model.LODs[0].Meshes[i].MaterialSlotIndex = i; + geom.model.MaterialSlots[i].Material = geom.meshes[i].material; + } + + //Not supported yet, should be done here + //geom.model.GenerateSDF(); + + brushIndex++; + } + + sw.Stop(); + //Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); + + // pass 3: static models & collision + sw.Restart(); + + if (!useStaticBatching) + { + brushIndex = 0; + foreach (BrushGeometry geom in brushGeometries) + { + Actor childModel; + if (LoadCollidersOnly) + { + childModel = worldSpawnActor.AddChild(); + } + else + { + StaticModel staticModel = worldSpawnActor.AddChild(); + staticModel.Model = geom.model; + //staticModel.DrawModes = DrawPass.None; + for (int i = 0; i < geom.meshes.Length; i++) - { - geom.meshes[i] = new BrushGeometryMesh(); - geom.meshes[i].material = geom.brushMaterials[geom.brushMaterials.Keys.ToList()[i]]; - } + staticModel.SetMaterial(i, geom.meshes[i].material); + + childModel = staticModel; } - catch (Exception e) + childModel.Name = "Brush_" + brushIndex; + childModel.Position = geom.offset; + + BrushScript brushScript = childModel.AddScript(); + + uint[] indices = new uint[geom.vertices.Length]; + for (uint i = 0; i < indices.Length; i++) + indices[i] = i; + + bool isClipMaterial = false; + bool isMissingMaterial = false; + if (geom.meshes.Length == 1 && !LoadCollidersOnly) { - Console.Print("Failed to triangulate brush " + brushIndex + ": " + e.Message); - //FlaxEngine.Engine.RequestExit(); + MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); + if (info != null && (bool)info.Value) + { + StaticModel staticModel = childModel.As(); + var entries = staticModel.Entries; + entries[0].Visible = false; + entries[0].ShadowsMode = ShadowsCastingMode.None; + entries[0].ReceiveDecals = false; + staticModel.Entries = entries; + isClipMaterial = true; + } + + if (geom.meshes[0].material == missingMaterial) + isMissingMaterial = true; } + /* + if (!LoadCollidersOnly) + { + StaticModel staticModel = childModel.As(); + var entries = staticModel.Entries; + for (int i=0; i < entries.Length; i++) + entries[i].Visible = false; + staticModel.Entries = entries; + }*/ + + if (!isClipMaterial && !isMissingMaterial && !LoadCollidersOnly) + sdfModels.Add(geom.model); + + CollisionData collisionData = Content.CreateVirtualAsset(); + if (collisionData.CookCollision( + convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, + geom.vertices, + indices)) + { + bool failed = true; + if (convexMesh) + { + // fallback to triangle mesh + failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, + geom.vertices, + indices); + if (!failed) + Console.PrintWarning("Hull brush " + brushIndex + " is not convex"); + } + + if (failed) + throw new Exception("failed to cook final collision"); + } + + MeshCollider meshCollider = childModel.AddChild(); + meshCollider.CollisionData = collisionData; + brushIndex++; + } + } + else + { + // create brush holder actors and collision + brushIndex = 0; + foreach (BrushGeometry geom in brushGeometries) + { + Actor childModel; + if (childModelSdf) + { + StaticModel staticModel = worldSpawnActor.AddChild(); + staticModel.DrawModes = DrawPass.GlobalSDF | DrawPass.GlobalSurfaceAtlas; + staticModel.Model = geom.model; + childModel = staticModel; + } + else + childModel = worldSpawnActor.AddChild(); + childModel.Name = "Brush_" + brushIndex; + //childModel.Model = geom.model; + childModel.Position = geom.offset; + + //for (int i = 0; i < geom.meshes.Length; i++) + // childModel.SetMaterial(i, geom.meshes[i].material); + + BrushScript brushScript = childModel.AddScript(); + + uint[] indices = new uint[geom.vertices.Length]; + for (uint i = 0; i < indices.Length; i++) + indices[i] = i; + + bool isClipMaterial = false; + bool isMissingMaterial = false; + if (geom.meshes.Length == 1 && childModelSdf) + { + MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); + if (info != null && (bool)info.Value) + { + var staticModel = childModel as StaticModel; + var entries = staticModel.Entries; + entries[0].Visible = false; + entries[0].ShadowsMode = ShadowsCastingMode.None; + entries[0].ReceiveDecals = false; + staticModel.Entries = entries; + isClipMaterial = true; + } + + if (geom.meshes[0].material == missingMaterial) + isMissingMaterial = true; + } + + /*{ + var entries = childModel.Entries; + for (int i=0; i < entries.Length; i++) + entries[i].Visible = false; + childModel.Entries = entries; + }*/ + + if (childModelSdf && !isClipMaterial && !isMissingMaterial) + sdfModels.Add(geom.model); + + CollisionData collisionData = Content.CreateVirtualAsset(); + if (collisionData.CookCollision( + convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, + geom.vertices, + indices)) + { + bool failed = true; + if (convexMesh) + { + // fallback to triangle mesh + failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, + geom.vertices, + indices); + if (!failed) + Console.PrintWarning("Hull brush " + brushIndex + " is not convex"); + } + + if (failed) + throw new Exception("failed to cook final collision"); + } + + MeshCollider meshCollider = childModel.AddChild(); + meshCollider.CollisionData = collisionData; + brushIndex++; + } + + // collect batches + brushIndex = 0; + Dictionary> batches = new Dictionary>(); + foreach (BrushGeometry geom in brushGeometries) + { + bool isClipMaterial = false; + bool isMissingMaterial = false; + if (geom.meshes.Length == 1) + { + MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); + if (info != null && (bool)info.Value) + isClipMaterial = true; + + if (geom.meshes[0].material == missingMaterial) + isMissingMaterial = true; + } + + var brushMaterial = geom.meshes[0].material; + + if (!isClipMaterial) + { + if (!batches.TryGetValue(brushMaterial, out var batchGeometries)) + { + batchGeometries = new List(); + batches.Add(brushMaterial, batchGeometries); + } + + batchGeometries.Add(geom); + } + + /*{ + var entries = childModel.Entries; + for (int i=0; i < entries.Length; i++) + entries[i].Visible = false; + childModel.Entries = entries; + }*/ + + //if (!isClipMaterial && !isMissingMaterial) + // sdfModels.Add(geom.model); + brushIndex++; } - sw.Stop(); - //Console.Print("Pass 1: triangulation: " + sw.Elapsed.TotalMilliseconds + "ms"); - - // pass 2: texturing - brushIndex = 0; - sw.Restart(); - foreach (BrushGeometry geom in brushGeometries) + foreach (var kvp in batches) { - var brushVertices = geom.vertices; + List normals = new List(); + List uvs = new List(); + List vertices = new List(); + List indices = new List(); + + uint indicesOffset = 0; + foreach (BrushGeometry geom in kvp.Value) + { + for (int i = 0; i < geom.meshes[0].vertices.Count; i++) + { + var v = geom.meshes[0].vertices[i]; + var n = geom.meshes[0].normals[i]; + var uv = geom.meshes[0].uvs[i]; + + vertices.Add(v + geom.offset); + uvs.Add(uv); + normals.Add(n); + + indices.Add(indicesOffset); + indicesOffset++; + } + } + + var batchModel = Content.CreateVirtualAsset(); + batchModel.SetupLODs(new[] { 1 }); + batchModel.SetupMaterialSlots(1); + + batchModel.LODs[0].Meshes[0].UpdateMesh(vertices, indices, normals, + null, uvs); + batchModel.LODs[0].Meshes[0].MaterialSlotIndex = 0; + batchModel.MaterialSlots[0].Material = kvp.Key; + + StaticModel childModel = worldSpawnActor.AddChild(); + childModel.Name = "Batch_" + kvp.Key.Path; + childModel.Model = batchModel; + //childModel.Position = geom.offset; + + if (!childModelSdf) + sdfModels.Add(batchModel); + } + } + + sw.Stop(); + //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); + } +#if false + else + { + var vertices = new List(); + var uvs = new List(); + var normals = new List(); + + sw.Restart(); + int brushIndex = 0; + foreach (MapBrush brush in root.entities[0].brushes) + { + try + { + TriangulateBrush(brush, out var brushVertices); + var brushUvs = new Float2[brushVertices.Length]; + var brushNormals = new Float3[brushVertices.Length]; + for (int i = 0; i < brushVertices.Length; i += 3) { Float3 v1 = brushVertices[i + 0]; @@ -646,8 +1050,7 @@ namespace Game float uvRotation = 0f; Float2 uvOffset = new Float2(0f); bool found = false; - int meshIndex = 0; - foreach (MapFacePlane brushPlane in geom.brush.planes) + foreach (MapFacePlane brushPlane in brush.planes) if ((brushPlane.plane.Normal - normal).Length < 0.01f) { normal = brushPlane.plane.Normal; // for consistency @@ -655,12 +1058,11 @@ namespace Game uvRotation = brushPlane.rotation; uvOffset = brushPlane.offset * brushPlane.scale; found = true; - meshIndex = geom.brushMaterials.Keys.ToList().IndexOf(brushPlane.texture); // ugh? break; } if (!found) - Console.Print("no matching plane found for brush " + brushIndex + ", bad geometry?"); + Console.Print("no matching plane found, bad geometry?"); Float2 uv1, uv2, uv3; // if quake format @@ -670,7 +1072,6 @@ namespace Game // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. // This behaviour is seemingly random in different engines and editors, so let's not bother. - Float3 textureNormal = new Float3((float)Math.Round(normal.X, 4), (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); @@ -705,829 +1106,427 @@ namespace Game : axisForward2, uvRotation * Mathf.DegreesToRadians); - uv1 = ((Float2)((v1 + geom.offset) * rot) + uvOffset) * uvScale; - uv2 = ((Float2)((v2 + geom.offset) * rot) + uvOffset) * uvScale; - uv3 = ((Float2)((v3 + geom.offset) * rot) + uvOffset) * uvScale; + uv1 = ((Float2)(v1 * rot) + uvOffset) * uvScale; + uv2 = ((Float2)(v2 * rot) + uvOffset) * uvScale; + uv3 = ((Float2)(v3 * rot) + uvOffset) * uvScale; } - BrushGeometryMesh mesh = geom.meshes[meshIndex]; - mesh.indices.Add((uint)mesh.vertices.Count + 0); - mesh.indices.Add((uint)mesh.vertices.Count + 1); - mesh.indices.Add((uint)mesh.vertices.Count + 2); + brushUvs[i + 0] = uv1; + brushUvs[i + 1] = uv2; + brushUvs[i + 2] = uv3; - mesh.vertices.Add(v1); - mesh.vertices.Add(v2); - mesh.vertices.Add(v3); - - mesh.uvs.Add(uv1); - mesh.uvs.Add(uv2); - mesh.uvs.Add(uv3); - - mesh.normals.Add(normal); - mesh.normals.Add(normal); - mesh.normals.Add(normal); + brushNormals[i + 0] = normal; + brushNormals[i + 1] = normal; + brushNormals[i + 2] = normal; } - geom.model = Content.CreateVirtualAsset(); - geom.model.SetupLODs(new[] { geom.meshes.Length }); - geom.model.SetupMaterialSlots(geom.meshes.Length); - - for (int i = 0; i < geom.meshes.Length; i++) - { - BrushGeometryMesh mesh = geom.meshes[i]; - if (mesh.vertices.Count == 0) - continue; - - geom.model.LODs[0].Meshes[i].UpdateMesh(mesh.vertices, mesh.indices, mesh.normals, - null, mesh.uvs); - geom.model.LODs[0].Meshes[i].MaterialSlotIndex = i; - geom.model.MaterialSlots[i].Material = geom.meshes[i].material; - } - - //Not supported yet, should be done here - //geom.model.GenerateSDF(); - - brushIndex++; + vertices.AddRange(brushVertices); + uvs.AddRange(brushUvs); + normals.AddRange(brushNormals); } - - sw.Stop(); - //Console.Print("Pass 2: texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); - - // pass 3: static models & collision - sw.Restart(); - - if (!useStaticBatching) + catch (Exception e) { - brushIndex = 0; - foreach (BrushGeometry geom in brushGeometries) - { - Actor childModel; - if (LoadCollidersOnly) - { - childModel = worldSpawnActor.AddChild(); - } - else - { - StaticModel staticModel = worldSpawnActor.AddChild(); - staticModel.Model = geom.model; - //staticModel.DrawModes = DrawPass.None; - - for (int i = 0; i < geom.meshes.Length; i++) - staticModel.SetMaterial(i, geom.meshes[i].material); - - childModel = staticModel; - } - childModel.Name = "Brush_" + brushIndex; - childModel.Position = geom.offset; - - BrushScript brushScript = childModel.AddScript(); - - uint[] indices = new uint[geom.vertices.Length]; - for (uint i = 0; i < indices.Length; i++) - indices[i] = i; - - bool isClipMaterial = false; - bool isMissingMaterial = false; - if (geom.meshes.Length == 1 && !LoadCollidersOnly) - { - MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); - if (info != null && (bool)info.Value) - { - StaticModel staticModel = childModel.As(); - var entries = staticModel.Entries; - entries[0].Visible = false; - entries[0].ShadowsMode = ShadowsCastingMode.None; - entries[0].ReceiveDecals = false; - staticModel.Entries = entries; - isClipMaterial = true; - } - - if (geom.meshes[0].material == missingMaterial) - isMissingMaterial = true; - } - - /* - if (!LoadCollidersOnly) - { - StaticModel staticModel = childModel.As(); - var entries = staticModel.Entries; - for (int i=0; i < entries.Length; i++) - entries[i].Visible = false; - staticModel.Entries = entries; - }*/ - - if (!isClipMaterial && !isMissingMaterial && !LoadCollidersOnly) - sdfModels.Add(geom.model); - - CollisionData collisionData = Content.CreateVirtualAsset(); - if (collisionData.CookCollision( - convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, - geom.vertices, - indices)) - { - bool failed = true; - if (convexMesh) - { - // fallback to triangle mesh - failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, - geom.vertices, - indices); - if (!failed) - Console.PrintWarning("Hull brush " + brushIndex + " is not convex"); - } - - if (failed) - throw new Exception("failed to cook final collision"); - } - - MeshCollider meshCollider = childModel.AddChild(); - meshCollider.CollisionData = collisionData; - brushIndex++; - } + Console.Print("Failed to hull brush " + brushIndex + ": " + e.Message); } + + brushIndex++; + } + + sw.Stop(); + Console.Print("Pass 1: triangulation and texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); + + sw.Restart(); + if (vertices.Count > 0) + { + uint[] triangles = new uint[vertices.Count]; + for (uint i = 0; i < vertices.Count; i++) + triangles[i] = i; + + model = Content.CreateVirtualAsset(); + model.SetupLODs(new[] { 1 }); + model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), + null, uvs.ToArray()); + + sdfModels.Add(model); + + StaticModel childModel = worldSpawnActor.AddChild(); + childModel.Name = "MapModel"; + childModel.Model = model; + //childModel.DrawModes = DrawPass.None; + //childModel.SetMaterial(0, missingMaterial); + + string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); + string assetPath = Path.Combine(matBasePath, "dev/dev_128_gray" + ".flax"); + var brushMaterial = Content.Load(assetPath); + if (brushMaterial != null) + childModel.SetMaterial(0, brushMaterial); else - { - // create brush holder actors and collision - brushIndex = 0; - foreach (BrushGeometry geom in brushGeometries) - { - Actor childModel; - if (childModelSdf) - { - StaticModel staticModel = worldSpawnActor.AddChild(); - staticModel.DrawModes = DrawPass.GlobalSDF | DrawPass.GlobalSurfaceAtlas; - staticModel.Model = geom.model; - childModel = staticModel; - } - else - childModel = worldSpawnActor.AddChild(); - childModel.Name = "Brush_" + brushIndex; - //childModel.Model = geom.model; - childModel.Position = geom.offset; + childModel.SetMaterial(0, missingMaterial); - //for (int i = 0; i < geom.meshes.Length; i++) - // childModel.SetMaterial(i, geom.meshes[i].material); - - BrushScript brushScript = childModel.AddScript(); - - uint[] indices = new uint[geom.vertices.Length]; - for (uint i = 0; i < indices.Length; i++) - indices[i] = i; - - bool isClipMaterial = false; - bool isMissingMaterial = false; - if (geom.meshes.Length == 1 && childModelSdf) - { - MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); - if (info != null && (bool)info.Value) - { - var staticModel = childModel as StaticModel; - var entries = staticModel.Entries; - entries[0].Visible = false; - entries[0].ShadowsMode = ShadowsCastingMode.None; - entries[0].ReceiveDecals = false; - staticModel.Entries = entries; - isClipMaterial = true; - } - - if (geom.meshes[0].material == missingMaterial) - isMissingMaterial = true; - } - - /*{ - var entries = childModel.Entries; - for (int i=0; i < entries.Length; i++) - entries[i].Visible = false; - childModel.Entries = entries; - }*/ - - if (childModelSdf && !isClipMaterial && !isMissingMaterial) - sdfModels.Add(geom.model); - - CollisionData collisionData = Content.CreateVirtualAsset(); - if (collisionData.CookCollision( - convexMesh ? CollisionDataType.ConvexMesh : CollisionDataType.TriangleMesh, - geom.vertices, - indices)) - { - bool failed = true; - if (convexMesh) - { - // fallback to triangle mesh - failed = collisionData.CookCollision(CollisionDataType.TriangleMesh, - geom.vertices, - indices); - if (!failed) - Console.PrintWarning("Hull brush " + brushIndex + " is not convex"); - } - - if (failed) - throw new Exception("failed to cook final collision"); - } - - MeshCollider meshCollider = childModel.AddChild(); - meshCollider.CollisionData = collisionData; - brushIndex++; - } - - // collect batches - brushIndex = 0; - Dictionary> batches = new Dictionary>(); - foreach (BrushGeometry geom in brushGeometries) - { - bool isClipMaterial = false; - bool isMissingMaterial = false; - if (geom.meshes.Length == 1) - { - MaterialParameter info = geom.meshes[0].material.GetParameter("IsClipMaterial"); - if (info != null && (bool)info.Value) - isClipMaterial = true; - - if (geom.meshes[0].material == missingMaterial) - isMissingMaterial = true; - } - - var brushMaterial = geom.meshes[0].material; - - if (!isClipMaterial) - { - if (!batches.TryGetValue(brushMaterial, out var batchGeometries)) - { - batchGeometries = new List(); - batches.Add(brushMaterial, batchGeometries); - } - - batchGeometries.Add(geom); - } - - /*{ - var entries = childModel.Entries; - for (int i=0; i < entries.Length; i++) - entries[i].Visible = false; - childModel.Entries = entries; - }*/ - - //if (!isClipMaterial && !isMissingMaterial) - // sdfModels.Add(geom.model); - - brushIndex++; - } - - foreach (var kvp in batches) - { - List normals = new List(); - List uvs = new List(); - List vertices = new List(); - List indices = new List(); - - uint indicesOffset = 0; - foreach (BrushGeometry geom in kvp.Value) - { - for (int i = 0; i < geom.meshes[0].vertices.Count; i++) - { - var v = geom.meshes[0].vertices[i]; - var n = geom.meshes[0].normals[i]; - var uv = geom.meshes[0].uvs[i]; - - vertices.Add(v + geom.offset); - uvs.Add(uv); - normals.Add(n); - - indices.Add(indicesOffset); - indicesOffset++; - } - } - - var batchModel = Content.CreateVirtualAsset(); - batchModel.SetupLODs(new[] { 1 }); - batchModel.SetupMaterialSlots(1); - - batchModel.LODs[0].Meshes[0].UpdateMesh(vertices, indices, normals, - null, uvs); - batchModel.LODs[0].Meshes[0].MaterialSlotIndex = 0; - batchModel.MaterialSlots[0].Material = kvp.Key; - - StaticModel childModel = worldSpawnActor.AddChild(); - childModel.Name = "Batch_" + kvp.Key.Path; - childModel.Model = batchModel; - //childModel.Position = geom.offset; - - if (!childModelSdf) - sdfModels.Add(batchModel); - } - } - - sw.Stop(); - //Console.Print("Pass 3: collision: " + sw.Elapsed.TotalMilliseconds + "ms"); + CollisionData collisionData = Content.CreateVirtualAsset(); + if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(), + triangles.ToArray())) + throw new Exception("failed to cook final collision"); + MeshCollider meshCollider = childModel.AddChild(); + meshCollider.CollisionData = collisionData; } -#if false - else - { - var vertices = new List(); - var uvs = new List(); - var normals = new List(); - sw.Restart(); - int brushIndex = 0; - foreach (MapBrush brush in root.entities[0].brushes) - { - try - { - TriangulateBrush(brush, out var brushVertices); - var brushUvs = new Float2[brushVertices.Length]; - var brushNormals = new Float3[brushVertices.Length]; - - for (int i = 0; i < brushVertices.Length; i += 3) - { - Float3 v1 = brushVertices[i + 0]; - Float3 v2 = brushVertices[i + 1]; - Float3 v3 = brushVertices[i + 2]; - - Float3 normal = -Float3.Cross(v3 - v1, v2 - v1).Normalized; - - // fetch the texture parameters from the plane with matching normal - Float2 uvScale = new Float2(0f); - float uvRotation = 0f; - Float2 uvOffset = new Float2(0f); - bool found = false; - foreach (MapFacePlane brushPlane in brush.planes) - if ((brushPlane.plane.Normal - normal).Length < 0.01f) - { - normal = brushPlane.plane.Normal; // for consistency - uvScale = 1f / brushPlane.scale; - uvRotation = brushPlane.rotation; - uvOffset = brushPlane.offset * brushPlane.scale; - found = true; - break; - } - - if (!found) - Console.Print("no matching plane found, bad geometry?"); - - Float2 uv1, uv2, uv3; - // if quake format - { - // The texture is projected to the surface from three angles, the axis with least - // distortion is chosen here. - - // Attempt to workaround most rounding errors at 45-degree angles which causes bias towards one axis. - // This behaviour is seemingly random in different engines and editors, so let's not bother. - Float3 textureNormal = new Float3((float)Math.Round(normal.X, 4), - (float)Math.Round(normal.Y, 4), (float)Math.Round(normal.Z, 4)); - - float dotX = Math.Abs(Float3.Dot(textureNormal, Float3.Right)); - float dotY = Math.Abs(Float3.Dot(textureNormal, Float3.Up)); - float dotZ = Math.Abs(Float3.Dot(textureNormal, Float3.Forward)); - - Float3 axis; - if (dotY >= dotX && dotY >= dotZ) - axis = -Float3.Up; - else if (dotX >= dotY && dotX >= dotZ) - axis = Float3.Right; - else if (dotZ >= dotX && dotZ >= dotY) - axis = -Float3.Forward; - else - axis = Float3.Right; - - Float3 axisForward = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f - ? -Float3.Forward - : Float3.Up; - Float3 axisForward2 = Mathf.Abs(Float3.Dot(axis, Float3.Up)) > 0.01f - ? Float3.Up - : -Float3.Forward; - - Quaternion rot = Quaternion.Identity; - rot = rot * Quaternion.LookRotation(axis, axisForward); - rot = rot * Quaternion.RotationAxis(-Float3.Forward, - 180f * Mathf.DegreesToRadians); - rot = rot * Quaternion.RotationAxis( - Mathf.Abs(Float3.Dot(axis, Float3.Right)) > 0.01f - ? Float3.Right - : axisForward2, - uvRotation * Mathf.DegreesToRadians); - - uv1 = ((Float2)(v1 * rot) + uvOffset) * uvScale; - uv2 = ((Float2)(v2 * rot) + uvOffset) * uvScale; - uv3 = ((Float2)(v3 * rot) + uvOffset) * uvScale; - } - - brushUvs[i + 0] = uv1; - brushUvs[i + 1] = uv2; - brushUvs[i + 2] = uv3; - - brushNormals[i + 0] = normal; - brushNormals[i + 1] = normal; - brushNormals[i + 2] = normal; - } - - vertices.AddRange(brushVertices); - uvs.AddRange(brushUvs); - normals.AddRange(brushNormals); - } - catch (Exception e) - { - Console.Print("Failed to hull brush " + brushIndex + ": " + e.Message); - } - - brushIndex++; - } - - sw.Stop(); - Console.Print("Pass 1: triangulation and texturing: " + sw.Elapsed.TotalMilliseconds + "ms"); - - sw.Restart(); - if (vertices.Count > 0) - { - uint[] triangles = new uint[vertices.Count]; - for (uint i = 0; i < vertices.Count; i++) - triangles[i] = i; - - model = Content.CreateVirtualAsset(); - model.SetupLODs(new[] { 1 }); - model.LODs[0].Meshes[0].UpdateMesh(vertices.ToArray(), (int[])(object)triangles, normals.ToArray(), - null, uvs.ToArray()); - - sdfModels.Add(model); - - StaticModel childModel = worldSpawnActor.AddChild(); - childModel.Name = "MapModel"; - childModel.Model = model; - //childModel.DrawModes = DrawPass.None; - //childModel.SetMaterial(0, missingMaterial); - - string matBasePath = Path.Combine(AssetManager.ContentPath, "Materials"); - string assetPath = Path.Combine(matBasePath, "dev/dev_128_gray" + ".flax"); - var brushMaterial = Content.Load(assetPath); - if (brushMaterial != null) - childModel.SetMaterial(0, brushMaterial); - else - childModel.SetMaterial(0, missingMaterial); - - CollisionData collisionData = Content.CreateVirtualAsset(); - if (collisionData.CookCollision(CollisionDataType.TriangleMesh, vertices.ToArray(), - triangles.ToArray())) - throw new Exception("failed to cook final collision"); - MeshCollider meshCollider = childModel.AddChild(); - meshCollider.CollisionData = collisionData; - } - - sw.Stop(); - Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms"); - } + sw.Stop(); + Console.Print("Pass 2: model and collision: " + sw.Elapsed.TotalMilliseconds + "ms"); + } #endif - // Handle entities + // Handle entities - { - sw.Restart(); + { + sw.Restart(); - int lightIndex = 0; - int playerSpawnIndex = 0; - foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) - switch (entity.properties["classname"]) - { - case "light": - if (importLights) - ParseLight(entity, ref lightIndex); - lightEnts.Add(entity); - break; - case "info_player_deathmatch": - ParsePlayerSpawn(entity, ref playerSpawnIndex); - break; - } - - //Console.Print("entity parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); - } - - /*for (int i=0; i<10000; i++) - { - Debug.Log($"{i} udfghjosa fuhoag guiha7 2382835yayhahn0 generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); - }*/ - - //Debug.Log($"generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); - if (generateSdf && globalIllumination /*&& Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None*/ && sdfModels.Count > 1) - { - int modelIndex = 0; - - // TODO: read sdf data from texture and dump it to file, and reuse it when generating sdf data -#if USE_NETCORE - string mapHash = SHA1.HashData(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); -#else - /*using*/ - var sha1 = new SHA1Managed(); - string mapHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); -#endif - - foreach (var model in sdfModels.ToList()) + int lightIndex = 0; + int playerSpawnIndex = 0; + foreach (MapEntity entity in root.entities.Where(x => x.properties.ContainsKey("classname"))) + switch (entity.properties["classname"]) { - string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", - $"{mapHash}_brush{modelIndex+1}"); - - /*if (File.Exists(sdfDataPath)) - { - sdfModels.TryTake(out var model_); - - T RawDeserialize(byte[] rawData, int position) - { - int rawsize = Marshal.SizeOf(typeof(T)); - if (rawsize > rawData.Length - position) - throw new ArgumentException("Not enough data to fill struct. Array length from position: " + - (rawData.Length - position) + ", Struct length: " + rawsize); - IntPtr buffer = Marshal.AllocHGlobal(rawsize); - Marshal.Copy(rawData, position, buffer, rawsize); - T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T)); - Marshal.FreeHGlobal(buffer); - return retobj; - } - - - - ModelBase.SDFData sdfData = new ModelBase.SDFData(); - - sdfData.Texture = GPUDevice.Instance.CreateTexture(sdfDataPath); - if (sdfData.Texture.Init(new GPUTextureDescription() { Width = width, Height = height, Depth = depth, Format = format, Flags = GPUTextureFlags.ShaderResource, MipLevels = mips})) - Console.PrintError($"Failed to create SDF texture for {sdfDataPath}"); - - sdfData.LocalToUVWMul = LocalToUVWMul; - sdfData.LocalToUVWAdd = LocalToUVWAdd; - sdfData.WorldUnitsPerVoxel = WorldUnitsPerVoxel; - sdfData.MaxDistance = MaxDistance; - sdfData.LocalBoundsMin = LocalBoundsMin; - sdfData.LocalBoundsMax = LocalBoundsMax; - sdfData.ResolutionScale = ResolutionScale; - sdfData.LOD = LOD; - for (int mipLevel = 0; mipLevel < mips; mipLevel++) - { - - } - - - //sdfData.Texture - //sdfData.Texture - - model.SetSDF(sdfData); - }*/ - modelIndex++; + case "light": + if (importLights) + ParseLight(entity, ref lightIndex); + lightEnts.Add(entity); + break; + case "info_player_deathmatch": + ParsePlayerSpawn(entity, ref playerSpawnIndex); + break; } - - var task = Task.Run(() => - { - Stopwatch sw2 = Stopwatch.StartNew(); - FlaxEngine.Debug.Log($"Generating level SDF ({sdfModels.Count} models)..."); - Console.Print($"Generating level SDF ({sdfModels.Count} models)..."); - - ParallelOptions opts = new ParallelOptions(); - FlaxEngine.Debug.Log("processorcount: " + Environment.ProcessorCount); - float backfacesThreshold = 0.15f; - if (useStaticBatching && !childModelSdf) - { - opts.MaxDegreeOfParallelism = 2; //Environment.ProcessorCount / 2; - //backfacesThreshold = 1f; - } - - Parallel.ForEach(sdfModels, opts, (model, _, index) => - { - if (model.WaitForLoaded()) - throw new Exception("model was not loaded"); - - model.GenerateSDF(0.9f, 6, true, backfacesThreshold); - - /*byte[] RawSerialize(object anything) - { - int rawSize = Marshal.SizeOf(anything); - IntPtr buffer = Marshal.AllocHGlobal(rawSize); - Marshal.StructureToPtr(anything, buffer, false); - byte[] rawDatas = new byte[rawSize]; - Marshal.Copy(buffer, rawDatas, 0, rawSize); - Marshal.FreeHGlobal(buffer); - return rawDatas; - } - - string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", - $"{mapHash}_brush{modelIndex+1}");*/ - - }); - - sw2.Stop(); - FlaxEngine.Debug.Log($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); - Console.Print($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); - }); - } + //Console.Print("entity parsing time: " + sw.Elapsed.TotalMilliseconds + "ms"); } - private void ParseLight(MapEntity entity, ref int lightIndex) + /*for (int i=0; i<10000; i++) { - Actor actor; - Float3? lightTargetPosition = null; + Debug.Log($"{i} udfghjosa fuhoag guiha7 2382835yayhahn0 generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); + }*/ - if (entity.properties.TryGetValue("target", out string targetName)) + //Debug.Log($"generate:{generateSdf}, GI:{Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None}, {sdfModels.Count}"); + if (generateSdf && globalIllumination /*&& Graphics.PostProcessSettings.GlobalIllumination.Mode != GlobalIlluminationMode.None*/ && sdfModels.Count > 1) + { + int modelIndex = 0; + + // TODO: read sdf data from texture and dump it to file, and reuse it when generating sdf data +#if USE_NETCORE + string mapHash = SHA1.HashData(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); +#else + /*using*/ + var sha1 = new SHA1Managed(); + string mapHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(levelScript.MapName + levelScript.MapTimestamp.Ticks.ToString())).ToString(); +#endif + + foreach (var model in sdfModels.ToList()) { - var target = root.entities.FirstOrDefault(x => - x.properties.ContainsKey("targetname") && x.properties["targetname"] == targetName); + string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", + $"{mapHash}_brush{modelIndex+1}"); - if (target != null) - lightTargetPosition = ParseOrigin(target.properties["origin"]); + /*if (File.Exists(sdfDataPath)) + { + sdfModels.TryTake(out var model_); + + T RawDeserialize(byte[] rawData, int position) + { + int rawsize = Marshal.SizeOf(typeof(T)); + if (rawsize > rawData.Length - position) + throw new ArgumentException("Not enough data to fill struct. Array length from position: " + + (rawData.Length - position) + ", Struct length: " + rawsize); + IntPtr buffer = Marshal.AllocHGlobal(rawsize); + Marshal.Copy(rawData, position, buffer, rawsize); + T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T)); + Marshal.FreeHGlobal(buffer); + return retobj; + } + + + + ModelBase.SDFData sdfData = new ModelBase.SDFData(); + + sdfData.Texture = GPUDevice.Instance.CreateTexture(sdfDataPath); + if (sdfData.Texture.Init(new GPUTextureDescription() { Width = width, Height = height, Depth = depth, Format = format, Flags = GPUTextureFlags.ShaderResource, MipLevels = mips})) + Console.PrintError($"Failed to create SDF texture for {sdfDataPath}"); + + sdfData.LocalToUVWMul = LocalToUVWMul; + sdfData.LocalToUVWAdd = LocalToUVWAdd; + sdfData.WorldUnitsPerVoxel = WorldUnitsPerVoxel; + sdfData.MaxDistance = MaxDistance; + sdfData.LocalBoundsMin = LocalBoundsMin; + sdfData.LocalBoundsMax = LocalBoundsMax; + sdfData.ResolutionScale = ResolutionScale; + sdfData.LOD = LOD; + for (int mipLevel = 0; mipLevel < mips; mipLevel++) + { + + } + + + //sdfData.Texture + //sdfData.Texture + + model.SetSDF(sdfData); + }*/ + modelIndex++; } - if (LoadCollidersOnly) - actor = worldSpawnActor.AddChild(); - else if (!lightTargetPosition.HasValue) - actor = worldSpawnActor.AddChild(); - else - actor = worldSpawnActor.AddChild(); - if (!lightTargetPosition.HasValue) - actor.Name = "Light_" + lightIndex; - else - actor.Name = "SpotLight_" + lightIndex; - - //Console.Print("light"); - //PointLight light = worldSpawnActor.AddChild(); - //LightWithShadow light = new PointLight(); - //var light = actor as LightWithShadow; - - actor.IsActive = sceneLighting; - actor.LocalPosition = ParseOrigin(entity.properties["origin"]); - actor.Layer = 1; - - if (lightTargetPosition.HasValue) - actor.Orientation = Quaternion.LookAt(actor.LocalPosition, lightTargetPosition.Value); - - if (actor is LightWithShadow light) + var task = Task.Run(() => { - var pointLight = light as PointLight; - var spotLight = light as SpotLight; + Stopwatch sw2 = Stopwatch.StartNew(); + FlaxEngine.Debug.Log($"Generating level SDF ({sdfModels.Count} models)..."); + Console.Print($"Generating level SDF ({sdfModels.Count} models)..."); - if (entity.properties.TryGetValue("_color", out string colorStr)) - light.Color = ParseColor(colorStr); + ParallelOptions opts = new ParallelOptions(); + FlaxEngine.Debug.Log("processorcount: " + Environment.ProcessorCount); + float backfacesThreshold = 0.15f; + if (useStaticBatching && !childModelSdf) + { + opts.MaxDegreeOfParallelism = 2; //Environment.ProcessorCount / 2; + //backfacesThreshold = 1f; + } - float lightamm = 300f; - if (entity.properties.TryGetValue("light", out string lightStr)) - lightamm = float.Parse(lightStr); + Parallel.ForEach(sdfModels, opts, (model, _, index) => + { + if (model.WaitForLoaded()) + throw new Exception("model was not loaded"); - float radamm = 64f; - if (entity.properties.TryGetValue("radius", out string radStr)) - radamm = float.Parse(radStr); + model.GenerateSDF(0.9f, 6, true, backfacesThreshold); - bool castShadows = true; - if (entity.properties.TryGetValue("castshadows", out string castShadowsStr)) - castShadows = int.Parse(castShadowsStr) != 0; + /*byte[] RawSerialize(object anything) + { + int rawSize = Marshal.SizeOf(anything); + IntPtr buffer = Marshal.AllocHGlobal(rawSize); + Marshal.StructureToPtr(anything, buffer, false); + byte[] rawDatas = new byte[rawSize]; + Marshal.Copy(buffer, rawDatas, 0, rawSize); + Marshal.FreeHGlobal(buffer); + return rawDatas; + } + + string sdfDataPath = Path.Combine(AssetManager.CachePath, "MapSdfData", + $"{mapHash}_brush{modelIndex+1}");*/ + + }); + + sw2.Stop(); + FlaxEngine.Debug.Log($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); + Console.Print($"Generated level SDF in {sw2.Elapsed.TotalMilliseconds}ms"); + }); + } + } + + private void ParseLight(MapEntity entity, ref int lightIndex) + { + Actor actor; + Float3? lightTargetPosition = null; + + if (entity.properties.TryGetValue("target", out string targetName)) + { + var target = root.entities.FirstOrDefault(x => + x.properties.ContainsKey("targetname") && x.properties["targetname"] == targetName); + + if (target != null) + lightTargetPosition = ParseOrigin(target.properties["origin"]); + } + + if (LoadCollidersOnly) + actor = worldSpawnActor.AddChild(); + else if (!lightTargetPosition.HasValue) + actor = worldSpawnActor.AddChild(); + else + actor = worldSpawnActor.AddChild(); + + if (!lightTargetPosition.HasValue) + actor.Name = "Light_" + lightIndex; + else + actor.Name = "SpotLight_" + lightIndex; + + //Console.Print("light"); + //PointLight light = worldSpawnActor.AddChild(); + //LightWithShadow light = new PointLight(); + //var light = actor as LightWithShadow; + + actor.IsActive = sceneLighting; + actor.LocalPosition = ParseOrigin(entity.properties["origin"]); + actor.Layer = 1; + + if (lightTargetPosition.HasValue) + actor.Orientation = Quaternion.LookAt(actor.LocalPosition, lightTargetPosition.Value); + + if (actor is LightWithShadow light) + { + var pointLight = light as PointLight; + var spotLight = light as SpotLight; + + if (entity.properties.TryGetValue("_color", out string colorStr)) + light.Color = ParseColor(colorStr); + + float lightamm = 300f; + if (entity.properties.TryGetValue("light", out string lightStr)) + lightamm = float.Parse(lightStr); + + float radamm = 64f; + if (entity.properties.TryGetValue("radius", out string radStr)) + radamm = float.Parse(radStr); + + bool castShadows = true; + if (entity.properties.TryGetValue("castshadows", out string castShadowsStr)) + castShadows = int.Parse(castShadowsStr) != 0; + + if (pointLight != null) + { + pointLight.UseInverseSquaredFalloff = false; + pointLight.FallOffExponent = 8; + pointLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; + } + if (spotLight != null) + { + spotLight.UseInverseSquaredFalloff = false; + spotLight.FallOffExponent = 8; + spotLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; + spotLight.InnerConeAngle = 65f; + spotLight.OuterConeAngle = 80f; + } + + light.ShadowsDistance = 500f; + light.ShadowsDepthBias = 0.027f;//0.005f; + + int preset = 3; + if (preset == 0) // most accurate?, huge radius and low performance + { + light.Brightness = lightamm / 93f; + + light.ShadowsDepthBias = 0.0565f; + + light.Brightness *= 0.7837f; if (pointLight != null) { - pointLight.UseInverseSquaredFalloff = false; - pointLight.FallOffExponent = 8; - pointLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; + pointLight.Radius = radamm * 12.5f; + pointLight.FallOffExponent = 3.33f; + pointLight.Radius *= 0.83375f; + } + + var hsv = light.Color.ToHSV(); + hsv.Y *= 0.8f; + light.Color = Color.FromHSV(hsv); + } + else if (preset == 1) // + { + if (pointLight != null) + { + pointLight.Radius = 250f; + pointLight.FallOffExponent = 2f; + } + + light.Brightness = (lightamm / 128f) * 1.25f; + } + else if (preset == 2) + { + if (pointLight != null) + { + pointLight.Radius = 200f; + pointLight.FallOffExponent = 2f; + } + + light.Brightness = (lightamm / 128f) * 1.6f; + } + else //if (preset == 3) + { + bool inverse = false; + float finalRadius = radamm * LightRadiusMultiplier; + + + light.Brightness = (lightamm / 128f) * BrightnessMultiplier; + + light.ShadowsNormalOffsetScale = 10f; + + light.ShadowsFadeDistance = 100f; // for debugging + light.ShadowsDistance = 500f; + + var hsv = light.Color.ToHSV(); + hsv.Y *= SaturationMultiplier; + light.Color = Color.FromHSV(hsv); + + light.IndirectLightingIntensity = IndirectLightMultiplier; + + light.ShadowsDepthBias = 0.0565f; + // if low quality shadows + //light.ShadowsDepthBias = 0.2492f; + + if (spotLight != null) + { + // huge aliasing with spot lights for some reason? + light.ShadowsDepthBias = 0.7f; + } + + if (inverse) + { + light.Brightness *= 20000f; + finalRadius *= 0.7f; + } + + if (pointLight != null) + { + pointLight.UseInverseSquaredFalloff = inverse; + pointLight.Radius = finalRadius; + pointLight.FallOffExponent = FallOffExponent; } if (spotLight != null) { - spotLight.UseInverseSquaredFalloff = false; - spotLight.FallOffExponent = 8; - spotLight.ShadowsStrength = sceneShadows && castShadows ? 1.0f : 0.0f; - spotLight.InnerConeAngle = 65f; - spotLight.OuterConeAngle = 80f; - } - - light.ShadowsDistance = 500f; - light.ShadowsDepthBias = 0.027f;//0.005f; - - int preset = 3; - if (preset == 0) // most accurate?, huge radius and low performance - { - light.Brightness = lightamm / 93f; - - light.ShadowsDepthBias = 0.0565f; - - light.Brightness *= 0.7837f; - - if (pointLight != null) - { - pointLight.Radius = radamm * 12.5f; - pointLight.FallOffExponent = 3.33f; - pointLight.Radius *= 0.83375f; - } - - var hsv = light.Color.ToHSV(); - hsv.Y *= 0.8f; - light.Color = Color.FromHSV(hsv); - } - else if (preset == 1) // - { - if (pointLight != null) - { - pointLight.Radius = 250f; - pointLight.FallOffExponent = 2f; - } - - light.Brightness = (lightamm / 128f) * 1.25f; - } - else if (preset == 2) - { - if (pointLight != null) - { - pointLight.Radius = 200f; - pointLight.FallOffExponent = 2f; - } - - light.Brightness = (lightamm / 128f) * 1.6f; - } - else //if (preset == 3) - { - bool inverse = false; - float finalRadius = radamm * LightRadiusMultiplier; - - - light.Brightness = (lightamm / 128f) * BrightnessMultiplier; - - light.ShadowsNormalOffsetScale = 10f; - - light.ShadowsFadeDistance = 100f; // for debugging - light.ShadowsDistance = 500f; - - var hsv = light.Color.ToHSV(); - hsv.Y *= SaturationMultiplier; - light.Color = Color.FromHSV(hsv); - - light.IndirectLightingIntensity = IndirectLightMultiplier; - - light.ShadowsDepthBias = 0.0565f; - // if low quality shadows - //light.ShadowsDepthBias = 0.2492f; - - if (spotLight != null) - { - // huge aliasing with spot lights for some reason? - light.ShadowsDepthBias = 0.7f; - } - - if (inverse) - { - light.Brightness *= 20000f; - finalRadius *= 0.7f; - } - - if (pointLight != null) - { - pointLight.UseInverseSquaredFalloff = inverse; - pointLight.Radius = finalRadius; - pointLight.FallOffExponent = FallOffExponent; - } - if (spotLight != null) - { - spotLight.UseInverseSquaredFalloff = inverse; - spotLight.Radius = finalRadius; - spotLight.FallOffExponent = FallOffExponent; - } + spotLight.UseInverseSquaredFalloff = inverse; + spotLight.Radius = finalRadius; + spotLight.FallOffExponent = FallOffExponent; } } - - lightIndex++; } - private void ParsePlayerSpawn(MapEntity entity, ref int playerSpawnIndex) - { - Actor spawn = worldSpawnActor.AddChild(); - spawn.Name = "PlayerSpawn_" + playerSpawnIndex; - spawn.LocalPosition = ParseOrigin(entity.properties["origin"]); + lightIndex++; + } - string angleStr = entity.properties.ContainsKey("angle") ? entity.properties["angle"] : "0"; - spawn.Orientation = ParseAngle(angleStr); + private void ParsePlayerSpawn(MapEntity entity, ref int playerSpawnIndex) + { + Actor spawn = worldSpawnActor.AddChild(); + spawn.Name = "PlayerSpawn_" + playerSpawnIndex; + spawn.LocalPosition = ParseOrigin(entity.properties["origin"]); - //spawn.Tag = "PlayerSpawn"; + string angleStr = entity.properties.ContainsKey("angle") ? entity.properties["angle"] : "0"; + spawn.Orientation = ParseAngle(angleStr); - playerSpawnIndex++; - } + //spawn.Tag = "PlayerSpawn"; - private static Float3 ParseOrigin(string origin) - { - string[] points = origin.Split(' '); - return new Float3(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture)); - } + playerSpawnIndex++; + } - private static Color ParseColor(string origin) - { - string[] points = origin.Split(' '); - return new Color(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture)); - } + private static Float3 ParseOrigin(string origin) + { + string[] points = origin.Split(' '); + return new Float3(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture)); + } - private static Quaternion ParseAngle(string origin) - { - string[] angles = origin.Split(' '); - //Console.Print("parseangle: " + new Float3(0f, float.Parse(angles[0]) + 45f, 0f).ToString()); - return Quaternion.Euler(new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 90f, 0f)); - } + private static Color ParseColor(string origin) + { + string[] points = origin.Split(' '); + return new Color(float.Parse(points[0], CultureInfo.InvariantCulture), float.Parse(points[1], CultureInfo.InvariantCulture), float.Parse(points[2], CultureInfo.InvariantCulture)); + } - private static Float3 ParseAngleEuler(string origin) - { - string[] angles = origin.Split(' '); - return new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 45f, 0f); - } + private static Quaternion ParseAngle(string origin) + { + string[] angles = origin.Split(' '); + //Console.Print("parseangle: " + new Float3(0f, float.Parse(angles[0]) + 45f, 0f).ToString()); + return Quaternion.Euler(new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 90f, 0f)); + } - public override void OnDestroy() - { - Destroy(ref model); - base.OnDestroy(); - } + private static Float3 ParseAngleEuler(string origin) + { + string[] angles = origin.Split(' '); + return new Float3(0f, float.Parse(angles[0], CultureInfo.InvariantCulture) + 45f, 0f); + } + + public override void OnDestroy() + { + Destroy(ref model); + base.OnDestroy(); } } \ No newline at end of file diff --git a/Source/Game/Level/QuickHull.cs b/Source/Game/Level/QuickHull.cs index 311f4e2..4f4f0ec 100644 --- a/Source/Game/Level/QuickHull.cs +++ b/Source/Game/Level/QuickHull.cs @@ -8,1309 +8,1308 @@ using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.Utilities; -namespace Game +namespace Game; + +public class HalfEdge { - public class HalfEdge - { - public Face face; - - //public Face oppositeFace; - public HalfEdge opposite; - public HalfEdge previous, next; - - public Edge edge; - //public bool horizonVisited; - - public HalfEdge(Edge edge, Face face) - { - this.edge = edge; - this.face = face; - face.halfEdges.Add(this); - } - - public Float3 tail - { - get - { - return edge.v2; - } - set - { - edge.v2 = value; - opposite.edge.v1 = value; - } - } - } - - public struct Edge - { - public Float3 v1, v2; - - public Edge(Float3 v1, Float3 v2) - { - this.v1 = v1; - this.v2 = v2; - } - - public static Edge[] GetEdges(Float3 v1, Float3 v2, Float3 v3) - { - return new[] - { - new Edge(v1, v2), - new Edge(v2, v3), - new Edge(v3, v1), - }; - } - - public override bool Equals(object obj) - { - if (obj is Edge) - { - var other = (Edge) obj; - var d1a = Math.Abs((v1 - other.v1).Length); - var d1b = Math.Abs((v1 - other.v2).Length); - var d2a = Math.Abs((v2 - other.v2).Length); - var d2b = Math.Abs((v2 - other.v1).Length); - - var eps = 1f; - if (d1a < eps && d2a < eps) - return true; - else if (d1b < eps && d2b < eps) - return true; - else - return false; - } - - return base.Equals(obj); - } - - public static bool operator ==(Edge edge, object obj) - { - return edge.Equals(obj); - } - - public static bool operator !=(Edge edge, object obj) - { - return !(edge == obj); - } - } - - public class Face - { - public Float3 v1, v2, v3; - public List halfEdges; - public bool visited; - - public Face(Float3 v1, Float3 v2, Float3 v3) - { - this.v1 = v1; - this.v2 = v2; - this.v3 = v3; - halfEdges = new List(3); - } - - public Edge[] GetEdges() - { - return new[] - { - new Edge(v1, v2), - new Edge(v2, v3), - new Edge(v3, v1), - }; - } - - public float DistanceToPoint(Float3 point) - { - Plane plane = new Plane(v1, v2, v3); - - float distance = (point.X * plane.Normal.X) + (point.Y * plane.Normal.Y) + - (point.Z * plane.Normal.Z) + plane.D; - return distance / (float) Math.Sqrt( - (plane.Normal.X * plane.Normal.X) + (plane.Normal.Y * plane.Normal.Y) + - (plane.Normal.Z * plane.Normal.Z)); - } - - public float DistanceToPlane(Face face) - { - Plane plane = new Plane(v1, v2, v3); - - var center = (face.v1 + face.v2 + face.v3) / 3f; - - return plane.Normal.X * center.X + plane.Normal.Y * center.Y + plane.Normal.Z * center.Z - plane.D; - } - - public float GetArea() - { - HalfEdge areaEdgeStart = halfEdges[0]; - HalfEdge areaEdge = areaEdgeStart.previous; - Float3 areaNorm = Float3.Zero; - int iters = 0; - do - { - if (iters++ > 1000) - throw new Exception("merge infinite loop"); - areaNorm += Float3.Cross(areaEdge.edge.v1 - areaEdgeStart.edge.v1, - areaEdge.next.edge.v1 - areaEdgeStart.edge.v1); - areaEdge = areaEdge.previous; - - } while (areaEdge != areaEdgeStart); - - return areaNorm.Length; - } - } - - public struct Tetrahedron - { - public Float3 v1, v2, v3, v4; - - public Tetrahedron(Float3 v1, Vector3 v2, Vector3 v3, Vector3 v4) - { - this.v1 = v1; - this.v2 = v2; - this.v3 = v3; - this.v4 = v4; - } - - public Face[] GetFaces() - { - return new[] - { - new Face(v1, v2, v3), - new Face(v1, v3, v4), - new Face(v1, v4, v2), - new Face(v2, v4, v3), - }; - } - } - - public class QuickHull - { - const float epsilon = 0.01f; - - private void SortPoints(List points, Vector3 planeNormal) - { - Vector3 center = Float3.Zero; - foreach (var vert in points) - { - center += vert; - } - - if (points.Count > 0) - center /= points.Count; - - points.Sort((v1, v2) => - { - var dot = Float3.Dot(planeNormal, Float3.Cross(v1 - center, v2 - center)); - if (dot > 0) - return 1; - else - return -1; - }); - } - - float PointDistanceFromPlane(Float3 point, Plane plane) - { - float distance = (point.X * plane.Normal.X) + (point.Y * plane.Normal.Y) + - (point.Z * plane.Normal.Z) + plane.D; - return distance / (float) Math.Sqrt( - (plane.Normal.X * plane.Normal.X) + (plane.Normal.Y * plane.Normal.Y) + - (plane.Normal.Z * plane.Normal.Z)); - } - - private Face[] CreateInitialSimplex(Float3[] points) - { - if (false) - { - // TODO: more optimal to find first set of points which are not coplanar? - - // find the longest edge - Vector3 v1 = Float3.Zero; - Vector3 v2 = Float3.Zero; - foreach (var p1 in points) - { - foreach (var p2 in points) - { - if ((p2 - p1).LengthSquared > (v2 - v1).LengthSquared) - { - v1 = p1; - v2 = p2; - } - } - } - - if (v1 == v2) - v1 = v2; - - Assert.IsTrue(v1 != v2, "a1 != a2"); - - // find the furthest point from the edge to form a face - Vector3 v3 = Float3.Zero; - float furthestDist = 0f; - foreach (var point in points) - { - //if (vert == a1 || vert == a2) - // continue; - - var edgeDir = (v2 - v1).Normalized; - var closest = v1 + edgeDir * Float3.Dot(point - v1, edgeDir); - - var dist = (point - closest).Length; - if (dist > furthestDist) - { - v3 = point; - furthestDist = dist; - } - } - - Assert.IsTrue(v3 != v1, "furthest != a1"); - Assert.IsTrue(v3 != v2, "furthest != a2"); - - // find the furthest point from he face - Plane plane = new Plane(v1, v2, v3); - Vector3 v4 = Float3.Zero; - float fourthDist = 0f; - foreach (var point in points) - { - if (point == v1 || point == v2 || point == v3) - continue; - - float distance = PointDistanceFromPlane(point, plane); - if (Math.Abs(distance) > fourthDist) - { - v4 = point; - fourthDist = distance; - } - } - - // make sure the tetrahedron is in counter-clockwise order - if (fourthDist > 0) - { - return new Face[] - { - new Face(v1, v3, v2), - new Face(v1, v4, v3), - new Face(v1, v2, v4), - new Face(v2, v3, v4), - }; - } - else - { - return new Face[] - { - new Face(v1, v2, v3), - new Face(v1, v3, v4), - new Face(v1, v4, v2), - new Face(v2, v4, v3), - }; - } - } - else - { - Vector3 v1 = Float3.Zero, v2 = Float3.Zero, v3 = Float3.Zero, v4 = Float3.Zero; - bool found = false; - - foreach (var p1 in points) - { - foreach (var p2 in points) - { - if (p1 == p2) - continue; - - if (AreCoincident(p1, p2)) - continue; - - - foreach (var p3 in points) - { - if (p3 == p2 || p3 == p1) - continue; - - if (AreCollinear(p1, p2, p3)) - continue; - - foreach (var p4 in points) - { - if (p4 == p1 || p4 == p2 || p4 == p3) - continue; - - if (AreCoplanar(p1, p2, p3, p4)) - continue; - - found = true; - v1 = p1; - v2 = p2; - v3 = p3; - v4 = p4; - break; - } - } - } - } - - if (!found) - throw new Exception("CreateInitialSimplex failed"); - - Plane plane = new Plane(v1, v2, v3); - var fourthDist = PointDistanceFromPlane(v4, plane); - - if (fourthDist > 0) - { - return new Face[] - { - new Face(v1, v3, v2), - new Face(v1, v4, v3), - new Face(v1, v2, v4), - new Face(v2, v3, v4), - }; - } - else - { - return new Face[] - { - new Face(v1, v2, v3), - new Face(v1, v3, v4), - new Face(v1, v4, v2), - new Face(v2, v4, v3), - }; - } - } - } - - - - //http://algolist.ru/maths/geom/convhull/qhull3d.php - - private void PopulateOutsideSet(List> outsideSet, Face[] faces, Vector3[] points) - { - foreach (var point in points) - { - foreach (Face face in faces) - { - float distance = face.DistanceToPoint(point); - /*if (Math.Abs(distance) < epsilon) - { - // point is in the plane, this gets merged - distance = distance; - } - else*/ - if (distance > 0) - { - //side.outsideSet.Add(point); - outsideSet.Add(new Tuple(face, point)); - break; - } - } - } - } - - public List QuickHull2(Float3[] points) - { - Assert.IsTrue(points.Length >= 4, "points.Length >= 4"); - - var tetrahedron = CreateInitialSimplex(points); - - List> outsideSet = new List>(); - PopulateOutsideSet(outsideSet, tetrahedron, points); - - // all points not in side.outsideSet are inside in "inside" set - - // create half-edges - foreach (var face in tetrahedron) - { - var halfEdges = new List(3); - foreach (var edge in face.GetEdges()) - halfEdges.Add(new HalfEdge(edge, face)); - - for (int i = 0; i < halfEdges.Count; i++) - { - halfEdges[i].previous = halfEdges[(i + 2) % 3]; - halfEdges[i].next = halfEdges[(i + 1) % 3]; - } - } - - // verify - { - var tetrapoints = new List(); - foreach (var face in tetrahedron) - { - foreach (var he in face.halfEdges) - { - if (!tetrapoints.Contains(he.edge.v1)) - tetrapoints.Add(he.edge.v1); - } - } - - foreach (var point in tetrapoints) - { - int foundFaces = 0; - - foreach (var face in tetrahedron) - { - if (face.v1 == point) - foundFaces++; - else if (face.v2 == point) - foundFaces++; - else if (face.v3 == point) - foundFaces++; - } - - Assert.IsTrue(foundFaces == 3, "foundFaces == 3"); - } - } - - - foreach (var face in tetrahedron) - { - Assert.IsTrue(face.halfEdges.Count == 3, "side.halfEdges.Count == 3"); - foreach (var halfEdge in face.halfEdges) - { - bool found = false; - foreach (var otherFace in tetrahedron) - { - if (found) - break; - if (face == otherFace) - continue; - - foreach (var otherHalfEdge in otherFace.halfEdges) - { - if (otherHalfEdge.opposite != null) - continue; - - if (halfEdge.edge == otherHalfEdge.edge) - { - halfEdge.opposite = otherHalfEdge; - otherHalfEdge.opposite = halfEdge; - //halfEdge.oppositeFace = otherFace; - //otherHalfEdge.oppositeFace = face; - found = true; - break; - } - } - } - - Assert.IsTrue(halfEdge.previous != null, "halfEdge.previous != null"); - Assert.IsTrue(halfEdge.next != null, "halfEdge.next != null"); - Assert.IsTrue(halfEdge.opposite != null, "halfEdge.opposite != null"); - //Assert.IsTrue(halfEdge.oppositeFace != null, "halfEdge.oppositeFace != null"); - Assert.IsTrue(halfEdge.opposite.face != null, "halfEdge.opposite.face != null"); - //Assert.IsTrue(halfEdge.oppositeFace == halfEdge.opposite.face, "halfEdge.oppositeFace == halfEdge.opposite.face"); - } - } - - - // grow hull - List horizonEdges = new List(); - - List hullFaces = new List(); - hullFaces.AddRange(tetrahedron); - - // stop when none of the faces have any visible outside points - int iterCount = 0; - while (outsideSet.Count > 0) - { - iterCount++; - Tuple pointToAdd = null; - Face pointFace = null; - // get furthest point in outside set - /*for (int sideIndex = 0; sideIndex < sides.Count; sideIndex++) - { - TetrahedronSide side = sides[sideIndex]; - if (side.outsideSet.Count == 0) - continue; - - float furthestDist = 0f; - foreach (var point in side.outsideSet) - { - Assert.IsTrue(point != side.face.v1, "point != side.face.v1"); - Assert.IsTrue(point != side.face.v2, "point != side.face.v2"); - Assert.IsTrue(point != side.face.v3, "point != side.face.v3"); - - float distance = PointDistanceFromPlane(point, side.plane); - if (Math.Abs(distance) > furthestDist) - { - pointToAdd = point; - pointSide = side; - furthestDist = distance; - } - } - }*/ - - float furthestDist = 0f; - foreach (var fp in outsideSet) - { - var face = fp.Item1; - var point = fp.Item2; - - float distance = face.DistanceToPoint(point); - if (Math.Abs(distance) > furthestDist) - //if (distance > furthestDist) - { - pointToAdd = fp; - pointFace = face; - furthestDist = distance; - } - } - - Assert.IsTrue(furthestDist > 0, "furthestDist > 0"); - Assert.IsTrue(pointToAdd != null, "pointToAdd != null"); - - outsideSet.Remove(pointToAdd); - - foreach (var face in hullFaces) - { - face.visited = false; - foreach (var halfEdge in face.halfEdges) - { - Assert.IsTrue(halfEdge.opposite.opposite == halfEdge, "halfEdge.opposite.opposite == halfEdge"); - Assert.IsTrue(hullFaces.Contains(halfEdge.opposite.face), - "hullFaces.Contains(halfEdge.opposite.face)"); - } - } - - var hullFacesNew = new List(); - var unclaimedPoints = new List(); - - AddPointToHull(pointToAdd.Item2, pointFace, unclaimedPoints, outsideSet, horizonEdges, hullFacesNew); - - // remove lit/seen/visited faces, their points were added to unclaimed points - for (int i = 0; i < hullFaces.Count; i++) - { - if (hullFaces[i].visited) - { - hullFaces.RemoveAt(i); - i--; - } - } - - hullFaces.AddRange(hullFacesNew); - - foreach (var face in hullFaces) - { - face.visited = false; - foreach (var halfEdge in face.halfEdges) - { - Assert.IsTrue(halfEdge.opposite.opposite == halfEdge, - "2 halfEdge.opposite.opposite == halfEdge (degenerate face?)"); - Assert.IsTrue(hullFaces.Contains(halfEdge.opposite.face), - "2 hullFaces.Contains(halfEdge.opposite.face)"); - } - } - - foreach (var fb in outsideSet) - unclaimedPoints.Add(fb.Item2); - - outsideSet.Clear(); - PopulateOutsideSet(outsideSet, hullFaces.ToArray(), unclaimedPoints.ToArray()); - - //if (iterCount >= 3) - // break; - - if (hullFaces.Count > 1000 || iterCount > 1000) - Assert.Fail("overflow"); - if (outsideSet.Count > 100000) - Assert.Fail("outsideSet overflow"); - } - - // merge faces with similar normals - List discardedFaces = new List(); - for (int i = 0; i < hullFaces.Count; i++) - { - Face firstFace = hullFaces[i]; - // if visible? - { - while (PostAdjacentMerge(firstFace, discardedFaces, hullFaces)) - { - // - } - } - } - - foreach (var f in discardedFaces) - hullFaces.Remove(f); - - List hullPoints = new List(hullFaces.Count * 3); - foreach (var face in hullFaces) - { - hullPoints.Add(face.v1); - hullPoints.Add(face.v2); - hullPoints.Add(face.v3); - } - - return hullPoints; - } - - private void AddUnique(List list, Vector3 point) - { - foreach (var p in list) - { - if ((point - p).Length < epsilon) - return; - } - list.Add(point); - } - - bool AreCoincident(Float3 a, Vector3 b) - { - return (a - b).Length <= epsilon; - } - - bool AreCollinear(Float3 a, Vector3 b, Vector3 c) - { - return Float3.Cross(c - a, c - b).Length <= epsilon; - } - - bool AreCoplanar(Float3 a, Vector3 b, Vector3 c, Vector3 d) - { - var n1 = Float3.Cross(c - a, c - b); - var n2 = Float3.Cross(d - a, d - b); - - var m1 = n1.Length; - var m2 = n2.Length; - - return m1 > epsilon - && m2 > epsilon - && AreCollinear(Float3.Zero, - (1.0f / m1) * n1, - (1.0f / m2) * n2); - } - - private bool PostAdjacentMerge(Face face, List discardedFaces, List hullFaces) - { - float maxdot_minang = Mathf.Cos(Mathf.DegreesToRadians * 3f); - HalfEdge edge = face.halfEdges[0]; - - do - { - Face oppFace = edge.opposite.face; - - bool merge = false; - Vector3 ni = new Plane(face.v1, face.v2, face.v3).Normal; - Vector3 nj = new Plane(oppFace.v1, oppFace.v2, oppFace.v3).Normal; - float dotP = Float3.Dot(ni, nj); - - if (dotP > maxdot_minang) - { - if (face.GetArea() >= oppFace.GetArea()) - { - // check if we can merge the 2 faces - merge = canMergeFaces(edge, hullFaces); - } - } - - if (merge) - { - // mergeAdjacentFace - if (!MergeAdjacentFaces(edge, face, face, discardedFaces)) - { - throw new Exception("merge failure"); - } - return true; - } - edge = edge.next; - } while (edge != face.halfEdges[0]); - - return false; - } - - private static int asdf = 0; - bool canMergeFaces(HalfEdge he, List hullFaces) - { - asdf++; - if (asdf == 22) - asdf = asdf; - Face face1 = he.face; - Face face2 = he.opposite.face; - - // construct the merged face - List edges = new List(); - Face mergedFace = new Face(new Float3(float.NaN), new Float3(float.NaN), new Float3(float.NaN)); - - // copy the first face edges - HalfEdge heTwin = null; - HalfEdge heCopy = null; - HalfEdge startEdge = (face1.halfEdges[0] != he) ? face1.halfEdges[0] : face1.halfEdges[1]; - HalfEdge copyHe = startEdge; + public Face face; + + //public Face oppositeFace; + public HalfEdge opposite; + public HalfEdge previous, next; + + public Edge edge; + //public bool horizonVisited; + + public HalfEdge(Edge edge, Face face) + { + this.edge = edge; + this.face = face; + face.halfEdges.Add(this); + } + + public Float3 tail + { + get + { + return edge.v2; + } + set + { + edge.v2 = value; + opposite.edge.v1 = value; + } + } +} + +public struct Edge +{ + public Float3 v1, v2; + + public Edge(Float3 v1, Float3 v2) + { + this.v1 = v1; + this.v2 = v2; + } + + public static Edge[] GetEdges(Float3 v1, Float3 v2, Float3 v3) + { + return new[] + { + new Edge(v1, v2), + new Edge(v2, v3), + new Edge(v3, v1), + }; + } + + public override bool Equals(object obj) + { + if (obj is Edge) + { + var other = (Edge) obj; + var d1a = Math.Abs((v1 - other.v1).Length); + var d1b = Math.Abs((v1 - other.v2).Length); + var d2a = Math.Abs((v2 - other.v2).Length); + var d2b = Math.Abs((v2 - other.v1).Length); + + var eps = 1f; + if (d1a < eps && d2a < eps) + return true; + else if (d1b < eps && d2b < eps) + return true; + else + return false; + } + + return base.Equals(obj); + } + + public static bool operator ==(Edge edge, object obj) + { + return edge.Equals(obj); + } + + public static bool operator !=(Edge edge, object obj) + { + return !(edge == obj); + } +} + +public class Face +{ + public Float3 v1, v2, v3; + public List halfEdges; + public bool visited; + + public Face(Float3 v1, Float3 v2, Float3 v3) + { + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + halfEdges = new List(3); + } + + public Edge[] GetEdges() + { + return new[] + { + new Edge(v1, v2), + new Edge(v2, v3), + new Edge(v3, v1), + }; + } + + public float DistanceToPoint(Float3 point) + { + Plane plane = new Plane(v1, v2, v3); + + float distance = (point.X * plane.Normal.X) + (point.Y * plane.Normal.Y) + + (point.Z * plane.Normal.Z) + plane.D; + return distance / (float) Math.Sqrt( + (plane.Normal.X * plane.Normal.X) + (plane.Normal.Y * plane.Normal.Y) + + (plane.Normal.Z * plane.Normal.Z)); + } + + public float DistanceToPlane(Face face) + { + Plane plane = new Plane(v1, v2, v3); + + var center = (face.v1 + face.v2 + face.v3) / 3f; + + return plane.Normal.X * center.X + plane.Normal.Y * center.Y + plane.Normal.Z * center.Z - plane.D; + } + + public float GetArea() + { + HalfEdge areaEdgeStart = halfEdges[0]; + HalfEdge areaEdge = areaEdgeStart.previous; + Float3 areaNorm = Float3.Zero; + int iters = 0; + do + { + if (iters++ > 1000) + throw new Exception("merge infinite loop"); + areaNorm += Float3.Cross(areaEdge.edge.v1 - areaEdgeStart.edge.v1, + areaEdge.next.edge.v1 - areaEdgeStart.edge.v1); + areaEdge = areaEdge.previous; + + } while (areaEdge != areaEdgeStart); + + return areaNorm.Length; + } +} + +public struct Tetrahedron +{ + public Float3 v1, v2, v3, v4; + + public Tetrahedron(Float3 v1, Vector3 v2, Vector3 v3, Vector3 v4) + { + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + } + + public Face[] GetFaces() + { + return new[] + { + new Face(v1, v2, v3), + new Face(v1, v3, v4), + new Face(v1, v4, v2), + new Face(v2, v4, v3), + }; + } +} + +public class QuickHull +{ + const float epsilon = 0.01f; + + private void SortPoints(List points, Vector3 planeNormal) + { + Vector3 center = Float3.Zero; + foreach (var vert in points) + { + center += vert; + } + + if (points.Count > 0) + center /= points.Count; + + points.Sort((v1, v2) => + { + var dot = Float3.Dot(planeNormal, Float3.Cross(v1 - center, v2 - center)); + if (dot > 0) + return 1; + else + return -1; + }); + } + + float PointDistanceFromPlane(Float3 point, Plane plane) + { + float distance = (point.X * plane.Normal.X) + (point.Y * plane.Normal.Y) + + (point.Z * plane.Normal.Z) + plane.D; + return distance / (float) Math.Sqrt( + (plane.Normal.X * plane.Normal.X) + (plane.Normal.Y * plane.Normal.Y) + + (plane.Normal.Z * plane.Normal.Z)); + } + + private Face[] CreateInitialSimplex(Float3[] points) + { + if (false) + { + // TODO: more optimal to find first set of points which are not coplanar? + + // find the longest edge + Vector3 v1 = Float3.Zero; + Vector3 v2 = Float3.Zero; + foreach (var p1 in points) + { + foreach (var p2 in points) + { + if ((p2 - p1).LengthSquared > (v2 - v1).LengthSquared) + { + v1 = p1; + v2 = p2; + } + } + } + + if (v1 == v2) + v1 = v2; + + Assert.IsTrue(v1 != v2, "a1 != a2"); + + // find the furthest point from the edge to form a face + Vector3 v3 = Float3.Zero; + float furthestDist = 0f; + foreach (var point in points) + { + //if (vert == a1 || vert == a2) + // continue; + + var edgeDir = (v2 - v1).Normalized; + var closest = v1 + edgeDir * Float3.Dot(point - v1, edgeDir); + + var dist = (point - closest).Length; + if (dist > furthestDist) + { + v3 = point; + furthestDist = dist; + } + } + + Assert.IsTrue(v3 != v1, "furthest != a1"); + Assert.IsTrue(v3 != v2, "furthest != a2"); + + // find the furthest point from he face + Plane plane = new Plane(v1, v2, v3); + Vector3 v4 = Float3.Zero; + float fourthDist = 0f; + foreach (var point in points) + { + if (point == v1 || point == v2 || point == v3) + continue; + + float distance = PointDistanceFromPlane(point, plane); + if (Math.Abs(distance) > fourthDist) + { + v4 = point; + fourthDist = distance; + } + } + + // make sure the tetrahedron is in counter-clockwise order + if (fourthDist > 0) + { + return new Face[] + { + new Face(v1, v3, v2), + new Face(v1, v4, v3), + new Face(v1, v2, v4), + new Face(v2, v3, v4), + }; + } + else + { + return new Face[] + { + new Face(v1, v2, v3), + new Face(v1, v3, v4), + new Face(v1, v4, v2), + new Face(v2, v4, v3), + }; + } + } + else + { + Vector3 v1 = Float3.Zero, v2 = Float3.Zero, v3 = Float3.Zero, v4 = Float3.Zero; + bool found = false; + + foreach (var p1 in points) + { + foreach (var p2 in points) + { + if (p1 == p2) + continue; + + if (AreCoincident(p1, p2)) + continue; + + + foreach (var p3 in points) + { + if (p3 == p2 || p3 == p1) + continue; + + if (AreCollinear(p1, p2, p3)) + continue; + + foreach (var p4 in points) + { + if (p4 == p1 || p4 == p2 || p4 == p3) + continue; + + if (AreCoplanar(p1, p2, p3, p4)) + continue; + + found = true; + v1 = p1; + v2 = p2; + v3 = p3; + v4 = p4; + break; + } + } + } + } + + if (!found) + throw new Exception("CreateInitialSimplex failed"); + + Plane plane = new Plane(v1, v2, v3); + var fourthDist = PointDistanceFromPlane(v4, plane); + + if (fourthDist > 0) + { + return new Face[] + { + new Face(v1, v3, v2), + new Face(v1, v4, v3), + new Face(v1, v2, v4), + new Face(v2, v3, v4), + }; + } + else + { + return new Face[] + { + new Face(v1, v2, v3), + new Face(v1, v3, v4), + new Face(v1, v4, v2), + new Face(v2, v4, v3), + }; + } + } + } + + + + //http://algolist.ru/maths/geom/convhull/qhull3d.php + + private void PopulateOutsideSet(List> outsideSet, Face[] faces, Vector3[] points) + { + foreach (var point in points) + { + foreach (Face face in faces) + { + float distance = face.DistanceToPoint(point); + /*if (Math.Abs(distance) < epsilon) + { + // point is in the plane, this gets merged + distance = distance; + } + else*/ + if (distance > 0) + { + //side.outsideSet.Add(point); + outsideSet.Add(new Tuple(face, point)); + break; + } + } + } + } + + public List QuickHull2(Float3[] points) + { + Assert.IsTrue(points.Length >= 4, "points.Length >= 4"); + + var tetrahedron = CreateInitialSimplex(points); + + List> outsideSet = new List>(); + PopulateOutsideSet(outsideSet, tetrahedron, points); + + // all points not in side.outsideSet are inside in "inside" set + + // create half-edges + foreach (var face in tetrahedron) + { + var halfEdges = new List(3); + foreach (var edge in face.GetEdges()) + halfEdges.Add(new HalfEdge(edge, face)); + + for (int i = 0; i < halfEdges.Count; i++) + { + halfEdges[i].previous = halfEdges[(i + 2) % 3]; + halfEdges[i].next = halfEdges[(i + 1) % 3]; + } + } + + // verify + { + var tetrapoints = new List(); + foreach (var face in tetrahedron) + { + foreach (var he in face.halfEdges) + { + if (!tetrapoints.Contains(he.edge.v1)) + tetrapoints.Add(he.edge.v1); + } + } + + foreach (var point in tetrapoints) + { + int foundFaces = 0; + + foreach (var face in tetrahedron) + { + if (face.v1 == point) + foundFaces++; + else if (face.v2 == point) + foundFaces++; + else if (face.v3 == point) + foundFaces++; + } + + Assert.IsTrue(foundFaces == 3, "foundFaces == 3"); + } + } + + + foreach (var face in tetrahedron) + { + Assert.IsTrue(face.halfEdges.Count == 3, "side.halfEdges.Count == 3"); + foreach (var halfEdge in face.halfEdges) + { + bool found = false; + foreach (var otherFace in tetrahedron) + { + if (found) + break; + if (face == otherFace) + continue; + + foreach (var otherHalfEdge in otherFace.halfEdges) + { + if (otherHalfEdge.opposite != null) + continue; + + if (halfEdge.edge == otherHalfEdge.edge) + { + halfEdge.opposite = otherHalfEdge; + otherHalfEdge.opposite = halfEdge; + //halfEdge.oppositeFace = otherFace; + //otherHalfEdge.oppositeFace = face; + found = true; + break; + } + } + } + + Assert.IsTrue(halfEdge.previous != null, "halfEdge.previous != null"); + Assert.IsTrue(halfEdge.next != null, "halfEdge.next != null"); + Assert.IsTrue(halfEdge.opposite != null, "halfEdge.opposite != null"); + //Assert.IsTrue(halfEdge.oppositeFace != null, "halfEdge.oppositeFace != null"); + Assert.IsTrue(halfEdge.opposite.face != null, "halfEdge.opposite.face != null"); + //Assert.IsTrue(halfEdge.oppositeFace == halfEdge.opposite.face, "halfEdge.oppositeFace == halfEdge.opposite.face"); + } + } + + + // grow hull + List horizonEdges = new List(); + + List hullFaces = new List(); + hullFaces.AddRange(tetrahedron); + + // stop when none of the faces have any visible outside points + int iterCount = 0; + while (outsideSet.Count > 0) + { + iterCount++; + Tuple pointToAdd = null; + Face pointFace = null; + // get furthest point in outside set + /*for (int sideIndex = 0; sideIndex < sides.Count; sideIndex++) + { + TetrahedronSide side = sides[sideIndex]; + if (side.outsideSet.Count == 0) + continue; + + float furthestDist = 0f; + foreach (var point in side.outsideSet) + { + Assert.IsTrue(point != side.face.v1, "point != side.face.v1"); + Assert.IsTrue(point != side.face.v2, "point != side.face.v2"); + Assert.IsTrue(point != side.face.v3, "point != side.face.v3"); + + float distance = PointDistanceFromPlane(point, side.plane); + if (Math.Abs(distance) > furthestDist) + { + pointToAdd = point; + pointSide = side; + furthestDist = distance; + } + } + }*/ + + float furthestDist = 0f; + foreach (var fp in outsideSet) + { + var face = fp.Item1; + var point = fp.Item2; + + float distance = face.DistanceToPoint(point); + if (Math.Abs(distance) > furthestDist) + //if (distance > furthestDist) + { + pointToAdd = fp; + pointFace = face; + furthestDist = distance; + } + } + + Assert.IsTrue(furthestDist > 0, "furthestDist > 0"); + Assert.IsTrue(pointToAdd != null, "pointToAdd != null"); + + outsideSet.Remove(pointToAdd); + + foreach (var face in hullFaces) + { + face.visited = false; + foreach (var halfEdge in face.halfEdges) + { + Assert.IsTrue(halfEdge.opposite.opposite == halfEdge, "halfEdge.opposite.opposite == halfEdge"); + Assert.IsTrue(hullFaces.Contains(halfEdge.opposite.face), + "hullFaces.Contains(halfEdge.opposite.face)"); + } + } + + var hullFacesNew = new List(); + var unclaimedPoints = new List(); + + AddPointToHull(pointToAdd.Item2, pointFace, unclaimedPoints, outsideSet, horizonEdges, hullFacesNew); + + // remove lit/seen/visited faces, their points were added to unclaimed points + for (int i = 0; i < hullFaces.Count; i++) + { + if (hullFaces[i].visited) + { + hullFaces.RemoveAt(i); + i--; + } + } + + hullFaces.AddRange(hullFacesNew); + + foreach (var face in hullFaces) + { + face.visited = false; + foreach (var halfEdge in face.halfEdges) + { + Assert.IsTrue(halfEdge.opposite.opposite == halfEdge, + "2 halfEdge.opposite.opposite == halfEdge (degenerate face?)"); + Assert.IsTrue(hullFaces.Contains(halfEdge.opposite.face), + "2 hullFaces.Contains(halfEdge.opposite.face)"); + } + } + + foreach (var fb in outsideSet) + unclaimedPoints.Add(fb.Item2); + + outsideSet.Clear(); + PopulateOutsideSet(outsideSet, hullFaces.ToArray(), unclaimedPoints.ToArray()); + + //if (iterCount >= 3) + // break; + + if (hullFaces.Count > 1000 || iterCount > 1000) + Assert.Fail("overflow"); + if (outsideSet.Count > 100000) + Assert.Fail("outsideSet overflow"); + } + + // merge faces with similar normals + List discardedFaces = new List(); + for (int i = 0; i < hullFaces.Count; i++) + { + Face firstFace = hullFaces[i]; + // if visible? + { + while (PostAdjacentMerge(firstFace, discardedFaces, hullFaces)) + { + // + } + } + } + + foreach (var f in discardedFaces) + hullFaces.Remove(f); + + List hullPoints = new List(hullFaces.Count * 3); + foreach (var face in hullFaces) + { + hullPoints.Add(face.v1); + hullPoints.Add(face.v2); + hullPoints.Add(face.v3); + } + + return hullPoints; + } + + private void AddUnique(List list, Vector3 point) + { + foreach (var p in list) + { + if ((point - p).Length < epsilon) + return; + } + list.Add(point); + } + + bool AreCoincident(Float3 a, Vector3 b) + { + return (a - b).Length <= epsilon; + } + + bool AreCollinear(Float3 a, Vector3 b, Vector3 c) + { + return Float3.Cross(c - a, c - b).Length <= epsilon; + } + + bool AreCoplanar(Float3 a, Vector3 b, Vector3 c, Vector3 d) + { + var n1 = Float3.Cross(c - a, c - b); + var n2 = Float3.Cross(d - a, d - b); + + var m1 = n1.Length; + var m2 = n2.Length; + + return m1 > epsilon + && m2 > epsilon + && AreCollinear(Float3.Zero, + (1.0f / m1) * n1, + (1.0f / m2) * n2); + } + + private bool PostAdjacentMerge(Face face, List discardedFaces, List hullFaces) + { + float maxdot_minang = Mathf.Cos(Mathf.DegreesToRadians * 3f); + HalfEdge edge = face.halfEdges[0]; + + do + { + Face oppFace = edge.opposite.face; + + bool merge = false; + Vector3 ni = new Plane(face.v1, face.v2, face.v3).Normal; + Vector3 nj = new Plane(oppFace.v1, oppFace.v2, oppFace.v3).Normal; + float dotP = Float3.Dot(ni, nj); + + if (dotP > maxdot_minang) + { + if (face.GetArea() >= oppFace.GetArea()) + { + // check if we can merge the 2 faces + merge = canMergeFaces(edge, hullFaces); + } + } + + if (merge) + { + // mergeAdjacentFace + if (!MergeAdjacentFaces(edge, face, face, discardedFaces)) + { + throw new Exception("merge failure"); + } + return true; + } + edge = edge.next; + } while (edge != face.halfEdges[0]); + + return false; + } + + private static int asdf = 0; + bool canMergeFaces(HalfEdge he, List hullFaces) + { + asdf++; + if (asdf == 22) + asdf = asdf; + Face face1 = he.face; + Face face2 = he.opposite.face; + + // construct the merged face + List edges = new List(); + Face mergedFace = new Face(new Float3(float.NaN), new Float3(float.NaN), new Float3(float.NaN)); + + // copy the first face edges + HalfEdge heTwin = null; + HalfEdge heCopy = null; + HalfEdge startEdge = (face1.halfEdges[0] != he) ? face1.halfEdges[0] : face1.halfEdges[1]; + HalfEdge copyHe = startEdge; + HalfEdge prevEdge = null; + HalfEdge firstEdge = null; + do + { + HalfEdge newEdge = new HalfEdge(copyHe.edge, mergedFace); + newEdge.opposite = copyHe.opposite; + newEdge.face = mergedFace; + newEdge.tail = copyHe.tail; + if(copyHe == he) + { + heTwin = copyHe.opposite; + heCopy = newEdge; + } + + if (firstEdge == null) + firstEdge = newEdge; + + if (prevEdge != null) + { + prevEdge.next = newEdge; + newEdge.previous = prevEdge; + } + + copyHe = copyHe.next; + prevEdge = newEdge; + } while (copyHe != startEdge); + + if (prevEdge != null) + { + prevEdge.next = firstEdge; + firstEdge.previous = prevEdge; + } + if (heCopy == null) + heCopy = firstEdge; + + // copy the second face edges + prevEdge = null; + firstEdge = null; + copyHe = face2.halfEdges[0]; + do + { + HalfEdge newEdge = new HalfEdge(copyHe.edge, mergedFace); + newEdge.opposite = copyHe.opposite; + newEdge.face = mergedFace; + newEdge.tail = copyHe.tail; + + if (firstEdge == null) + firstEdge = newEdge; + + if (heTwin == copyHe) + heTwin = newEdge; + + if (prevEdge != null) + { + prevEdge.next = newEdge; + newEdge.previous = prevEdge; + } + + copyHe = copyHe.next; + prevEdge = newEdge; + } while (copyHe != face2.halfEdges[0]); + + if (prevEdge != null) + { + prevEdge.next = firstEdge; + firstEdge.previous = prevEdge; + } + if (heTwin == null) + heTwin = firstEdge; + + mergedFace.v1 = mergedFace.halfEdges[0].edge.v1; + mergedFace.v2 = mergedFace.halfEdges[1].edge.v1; + mergedFace.v3 = mergedFace.halfEdges[2].edge.v1; + + if (heCopy == null) + heTwin = heTwin; + + Assert.IsTrue(heTwin != null, "heTwin != null"); + + HalfEdge hedgeAdjPrev = heCopy.previous; + HalfEdge hedgeAdjNext = heCopy.next; + HalfEdge hedgeOppPrev = heTwin.previous; + HalfEdge hedgeOppNext = heTwin.next; + + hedgeOppPrev.next = hedgeAdjNext; + hedgeAdjNext.previous = hedgeOppPrev; + + hedgeAdjPrev.next = hedgeOppNext; + hedgeOppNext.previous = hedgeAdjPrev; + + // compute normal and centroid + //mergedFace.computeNormalAndCentroid(); + + // test the vertex distance + float mTolarenace = epsilon;//-1; + float mPlaneTolerance = epsilon;//-1f; + float maxDist = mPlaneTolerance; + List uniqPoints = new List(); + foreach (var hullFace in hullFaces) + { + AddUnique(uniqPoints, hullFace.v1); + AddUnique(uniqPoints, hullFace.v2); + AddUnique(uniqPoints, hullFace.v3); + } + + foreach (var point in uniqPoints) + { + float dist = mergedFace.DistanceToPoint(point); + if (dist > maxDist) + { + return false; + } + } + + // check the convexity + HalfEdge qhe = mergedFace.halfEdges[0]; + Assert.IsTrue(mergedFace.halfEdges.Count == 3, "mergedFace.halfEdges.Count == 3"); + do + { + Vector3 vertex = qhe.tail; + Vector3 nextVertex = qhe.next.tail; + + Vector3 edgeVector = (nextVertex - vertex).Normalized; + Vector3 outVector = +Float3.Cross(-(new Plane(mergedFace.v1, mergedFace.v2, mergedFace.v3).Normal), edgeVector); + + HalfEdge testHe = qhe.next; + do + { + Vector3 testVertex = testHe.tail; + float dist = Float3.Dot(testVertex - vertex, outVector); + + if (dist > mTolarenace) + return false; + + testHe = testHe.next; + } while (testHe != qhe.next); + + qhe = qhe.next; + } while (qhe != mergedFace.halfEdges[0]); + + + Face oppFace = he.opposite.face; + + HalfEdge hedgeOpp = he.opposite; + + hedgeAdjPrev = he.previous; + hedgeAdjNext = he.next; + hedgeOppPrev = hedgeOpp.previous; + hedgeOppNext = hedgeOpp.next; + + // check if we are lining up with the face in adjPrev dir + while (hedgeAdjPrev.opposite.face == oppFace) + { + hedgeAdjPrev = hedgeAdjPrev.previous; + hedgeOppNext = hedgeOppNext.next; + } + + // check if we are lining up with the face in adjNext dir + while (hedgeAdjNext.opposite.face == oppFace) + { + hedgeOppPrev = hedgeOppPrev.previous; + hedgeAdjNext = hedgeAdjNext.next; + } + + // no redundant merges, just clean merge of 2 neighbour faces + if (hedgeOppPrev.opposite.face == hedgeAdjNext.opposite.face) + { + return false; + } + + if (hedgeAdjPrev.opposite.face == hedgeOppNext.opposite.face) + { + return false; + } + + return true; + } + + private void AddPointToHull(Float3 point, Face face, List unclaimedPoints, + List> outsideSet, + List horizonEdges, List hullFaces) + { + horizonEdges.Clear(); + + CalculateHorizon(face, point, unclaimedPoints, outsideSet, horizonEdges, face.halfEdges[0]); + + // create new faces + if (horizonEdges.Count > 0) + { + List newFaces = new List(); + HalfEdge firstEdge = horizonEdges.First(); HalfEdge prevEdge = null; - HalfEdge firstEdge = null; - do - { - HalfEdge newEdge = new HalfEdge(copyHe.edge, mergedFace); - newEdge.opposite = copyHe.opposite; - newEdge.face = mergedFace; - newEdge.tail = copyHe.tail; - if(copyHe == he) - { - heTwin = copyHe.opposite; - heCopy = newEdge; - } - - if (firstEdge == null) - firstEdge = newEdge; - - if (prevEdge != null) - { - prevEdge.next = newEdge; - newEdge.previous = prevEdge; - } - - copyHe = copyHe.next; - prevEdge = newEdge; - } while (copyHe != startEdge); - - if (prevEdge != null) - { - prevEdge.next = firstEdge; - firstEdge.previous = prevEdge; - } - if (heCopy == null) - heCopy = firstEdge; - - // copy the second face edges - prevEdge = null; - firstEdge = null; - copyHe = face2.halfEdges[0]; - do - { - HalfEdge newEdge = new HalfEdge(copyHe.edge, mergedFace); - newEdge.opposite = copyHe.opposite; - newEdge.face = mergedFace; - newEdge.tail = copyHe.tail; - - if (firstEdge == null) - firstEdge = newEdge; - - if (heTwin == copyHe) - heTwin = newEdge; - - if (prevEdge != null) - { - prevEdge.next = newEdge; - newEdge.previous = prevEdge; - } - - copyHe = copyHe.next; - prevEdge = newEdge; - } while (copyHe != face2.halfEdges[0]); - - if (prevEdge != null) - { - prevEdge.next = firstEdge; - firstEdge.previous = prevEdge; - } - if (heTwin == null) - heTwin = firstEdge; - - mergedFace.v1 = mergedFace.halfEdges[0].edge.v1; - mergedFace.v2 = mergedFace.halfEdges[1].edge.v1; - mergedFace.v3 = mergedFace.halfEdges[2].edge.v1; - - if (heCopy == null) - heTwin = heTwin; - - Assert.IsTrue(heTwin != null, "heTwin != null"); - - HalfEdge hedgeAdjPrev = heCopy.previous; - HalfEdge hedgeAdjNext = heCopy.next; - HalfEdge hedgeOppPrev = heTwin.previous; - HalfEdge hedgeOppNext = heTwin.next; - - hedgeOppPrev.next = hedgeAdjNext; - hedgeAdjNext.previous = hedgeOppPrev; - - hedgeAdjPrev.next = hedgeOppNext; - hedgeOppNext.previous = hedgeAdjPrev; - - // compute normal and centroid - //mergedFace.computeNormalAndCentroid(); - - // test the vertex distance - float mTolarenace = epsilon;//-1; - float mPlaneTolerance = epsilon;//-1f; - float maxDist = mPlaneTolerance; - List uniqPoints = new List(); - foreach (var hullFace in hullFaces) - { - AddUnique(uniqPoints, hullFace.v1); - AddUnique(uniqPoints, hullFace.v2); - AddUnique(uniqPoints, hullFace.v3); - } - - foreach (var point in uniqPoints) - { - float dist = mergedFace.DistanceToPoint(point); - if (dist > maxDist) - { - return false; - } - } - - // check the convexity - HalfEdge qhe = mergedFace.halfEdges[0]; - Assert.IsTrue(mergedFace.halfEdges.Count == 3, "mergedFace.halfEdges.Count == 3"); - do - { - Vector3 vertex = qhe.tail; - Vector3 nextVertex = qhe.next.tail; - - Vector3 edgeVector = (nextVertex - vertex).Normalized; - Vector3 outVector = - Float3.Cross(-(new Plane(mergedFace.v1, mergedFace.v2, mergedFace.v3).Normal), edgeVector); - - HalfEdge testHe = qhe.next; - do - { - Vector3 testVertex = testHe.tail; - float dist = Float3.Dot(testVertex - vertex, outVector); - - if (dist > mTolarenace) - return false; - - testHe = testHe.next; - } while (testHe != qhe.next); - - qhe = qhe.next; - } while (qhe != mergedFace.halfEdges[0]); - - - Face oppFace = he.opposite.face; - - HalfEdge hedgeOpp = he.opposite; - - hedgeAdjPrev = he.previous; - hedgeAdjNext = he.next; - hedgeOppPrev = hedgeOpp.previous; - hedgeOppNext = hedgeOpp.next; - - // check if we are lining up with the face in adjPrev dir - while (hedgeAdjPrev.opposite.face == oppFace) - { - hedgeAdjPrev = hedgeAdjPrev.previous; - hedgeOppNext = hedgeOppNext.next; - } - - // check if we are lining up with the face in adjNext dir - while (hedgeAdjNext.opposite.face == oppFace) - { - hedgeOppPrev = hedgeOppPrev.previous; - hedgeAdjNext = hedgeAdjNext.next; - } - - // no redundant merges, just clean merge of 2 neighbour faces - if (hedgeOppPrev.opposite.face == hedgeAdjNext.opposite.face) - { - return false; - } - - if (hedgeAdjPrev.opposite.face == hedgeOppNext.opposite.face) - { - return false; - } - - return true; - } - - private void AddPointToHull(Float3 point, Face face, List unclaimedPoints, - List> outsideSet, - List horizonEdges, List hullFaces) - { - horizonEdges.Clear(); - - CalculateHorizon(face, point, unclaimedPoints, outsideSet, horizonEdges, face.halfEdges[0]); - - // create new faces - if (horizonEdges.Count > 0) - { - List newFaces = new List(); - HalfEdge firstEdge = horizonEdges.First(); - HalfEdge prevEdge = null; - foreach (var edge in horizonEdges) - { - var newFace = new Face(point, edge.edge.v1, edge.edge.v2); - var newPlane = new Plane(newFace.v1, newFace.v2, newFace.v3); - - var uniqPoints = new List(); - AddUnique(uniqPoints, newFace.v1); - AddUnique(uniqPoints, newFace.v2); - AddUnique(uniqPoints, newFace.v3); - - var fourtPoint = edge.opposite.next.edge.v2; - - AddUnique(uniqPoints, edge.opposite.next.edge.v1); - AddUnique(uniqPoints, edge.opposite.next.edge.v2); - AddUnique(uniqPoints, edge.opposite.previous.edge.v1); - AddUnique(uniqPoints, edge.opposite.previous.edge.v2); - - var distFromPlane = PointDistanceFromPlane(fourtPoint, newPlane); - if (Math.Abs(distFromPlane) < epsilon) - { - // both faces are coplanar, merge them together - - - distFromPlane = distFromPlane; - if (AreCoplanar(newFace.v1, newFace.v2, newFace.v3, fourtPoint)) - distFromPlane = distFromPlane; - } - else if (AreCoplanar(newFace.v1, newFace.v2, newFace.v3, fourtPoint)) - { - distFromPlane = distFromPlane; - } - - - - var newEdges = new List(); - foreach (var ne in newFace.GetEdges()) - newEdges.Add(new HalfEdge(ne, newFace)); - - for (int i = 0; i < newEdges.Count; i++) - { - newEdges[i].previous = newEdges[(i + 2) % 3]; - newEdges[i].next = newEdges[(i + 1) % 3]; - } - - if (prevEdge != null) - { - var prevAdjacentEdge = newFaces.Last().halfEdges.Last(); - var lastAdjacentEdge = newEdges.First(); - lastAdjacentEdge.opposite = prevAdjacentEdge; - prevAdjacentEdge.opposite = lastAdjacentEdge; - } - - //edge.face = newFace; - - newEdges[1].opposite = edge.opposite; - edge.opposite.opposite = newEdges[1]; - - newFaces.Add(newFace); - prevEdge = edge; - } - - if (prevEdge != null) - { - var lastAdjacentEdge = newFaces.Last().halfEdges.Last(); - var firstAdjacentEdge = newFaces.First().halfEdges.First(); - lastAdjacentEdge.opposite = firstAdjacentEdge; - firstAdjacentEdge.opposite = lastAdjacentEdge; - //first.previous.opposite = prev.next; - //prev.next.opposite = first.previous; - } - - // merge NONCONVEX_WRT_LARGER_FACE - - //List discardedFaces = new List(); - if (false) - { - foreach (var newFace in newFaces) - { - // if face visible? - while (AdjacentMerge(point, newFace, unclaimedPoints, outsideSet, true)) - { - // merge until failure - } - - hullFaces.Add(newFace); - } - - foreach (var newFace in newFaces) - { - // if face non-convex? - // mark face as visible? - while (AdjacentMerge(point, newFace, unclaimedPoints, outsideSet, false)) - { - // merge until failure - } - - hullFaces.Add(newFace); - } - } - else - hullFaces.AddRange(newFaces); - - // verify - foreach (var newFace in hullFaces) - { - Assert.IsTrue(newFace.halfEdges.Count == 3, "AddPointToHull: side.halfEdges.Count == 3"); - foreach (var halfEdge in newFace.halfEdges) - { - /*bool found = false; - foreach (var otherFace in hullFaces) - { - if (found) - break; - if (newFace == otherFace) - continue; - - foreach (var otherHalfEdge in otherFace.halfEdges) - { - if (otherHalfEdge.opposite != null) - continue; - - if (halfEdge.edge == otherHalfEdge.edge) - { - halfEdge.opposite = otherHalfEdge; - otherHalfEdge.opposite = halfEdge; - //halfEdge.oppositeFace = otherFace; - //otherHalfEdge.oppositeFace = face; - found = true; - break; - } - } - }*/ - - Assert.IsTrue(halfEdge.previous != null, "AddPointToHull: halfEdge.previous != null"); - Assert.IsTrue(halfEdge.next != null, "AddPointToHull: halfEdge.next != null"); - Assert.IsTrue(halfEdge.next.next.next == halfEdge, "AddPointToHull: halfEdge.next.next.next == halfEdge"); - Assert.IsTrue(halfEdge.previous.previous.previous == halfEdge, "AddPointToHull: halfEdge.previous.previous.previous == halfEdge"); - Assert.IsTrue(halfEdge.opposite != null, "AddPointToHull: halfEdge.opposite != null"); - //Assert.IsTrue(halfEdge.oppositeFace != null, "halfEdge.oppositeFace != null"); - Assert.IsTrue(halfEdge.opposite.face != null, "AddPointToHull: halfEdge.opposite.face != null"); - //Assert.IsTrue(halfEdge.oppositeFace == halfEdge.opposite.face, "halfEdge.oppositeFace == halfEdge.opposite.face"); - } - } - } - } - - private bool AdjacentMerge(Float3 point, Face face, List unclaimedPoints, List> outsideSet, bool mergeWrtLargerFace) - { - const float tolerance = -1f; - - HalfEdge edge = face.halfEdges[0]; - - bool convex = true; - do - { - Face oppositeFace = edge.opposite.face; - bool merge = false; - - var p1 = new Plane(face.v1, face.v2, face.v3); - var p2 = new Plane(oppositeFace.v1, oppositeFace.v2, oppositeFace.v3); - - if (mergeWrtLargerFace) - { - float faceArea = edge.face.GetArea(); - float oppositeArea = edge.opposite.face.GetArea(); - - if (faceArea > oppositeArea) - { - if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance) - merge = true; - else if (edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) - convex = false; - } - else - { - if (edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) - merge = true; - else if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance) - convex = false; - } - } - else - { - if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance || - edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) - { - merge = true; - } - } - - if (merge) - { - List discardedFaces = new List(); - // mergeAdjacentFace - if (!MergeAdjacentFaces(edge, face, face, discardedFaces)) - { - throw new Exception("merge failure"); - } - - foreach (var dface in discardedFaces) - { - for (int i = 0; i 0) - outsideSet[i] = new Tuple(face, outsideSet[i].Item2); - else - unclaimedPoints.Add(outsideSet[i].Item2); - } - } - } - } - } while (edge != face.halfEdges[0]); - - return false; // no merge - } - - private bool MergeAdjacentFaces(HalfEdge edge, Face newFace, Face oldFace, List discardedFaces) - { - Face oppositeFace = edge.opposite.face; - - discardedFaces.Add(oppositeFace); - - HalfEdge prev = edge.previous; - HalfEdge next = edge.next; - HalfEdge oppositePrev = edge.opposite.previous; - HalfEdge oppositeNext = edge.opposite.next; - - HalfEdge breakEdge = prev; - while (prev.opposite.face == oppositeFace) - { - prev = prev.previous; - oppositeNext = oppositeNext.next; - - if (prev == breakEdge) - return false; - } - - breakEdge = next; - while (next.opposite.face == oppositeFace) - { - oppositePrev = oppositePrev.previous; - next = next.next; - - if (next == breakEdge) - return false; - } - - for (HalfEdge e = oppositeNext; e != oppositePrev.next; e = e.next) - e.face = newFace; - - if (edge == oldFace.halfEdges[0]) - oldFace.halfEdges[0] = next; - - Face discardedFace = ConnectHalfEdges(newFace, oppositePrev, next); - Face discardedFace2 = ConnectHalfEdges(newFace, prev, oppositeNext); - - if (discardedFace != null) - discardedFaces.Add(discardedFace); - if (discardedFace2 != null) - discardedFaces.Add(discardedFace2); - - return true; - } - - // merges adjacent faces - private Face ConnectHalfEdges(Face face, HalfEdge prev, HalfEdge edge) - { - Face discardedFace = null; - - if (prev.opposite.face == edge.opposite.face) - { - Face oppFace = edge.opposite.face; - HalfEdge hedgeOpp; - - if (prev == face.halfEdges[0]) - { - face.halfEdges[0] = edge; - } - - bool isDegenerate = false; - { - // is this correct? - HalfEdge s = oppFace.halfEdges[0]; - if (s.next.next.next != s) - isDegenerate = true; - else if (s.previous.previous.previous != s) - isDegenerate = true; - else if (s.next.next == s) - isDegenerate = true; - else if (s.previous.previous == s) - isDegenerate = true; - - HalfEdge ee = s; - int numVerts = 0; - do - { - numVerts++; - ee = ee.next; - } while (ee != s); - - if (numVerts <= 2) - isDegenerate = true; - } - - //if (oppFace.numVertices() == 3) - if (!isDegenerate) - { - // then we can get rid of the - // opposite face altogether - hedgeOpp = edge.opposite.previous.opposite; - - //oppFace.mark = DELETED; - discardedFace = oppFace; - } - else - { - hedgeOpp = edge.opposite.next; - - if (oppFace.halfEdges[0] == hedgeOpp.previous) { - oppFace.halfEdges[0] = hedgeOpp; - } - hedgeOpp.previous = hedgeOpp.previous.previous; - hedgeOpp.previous.next = hedgeOpp; - } - edge.previous = prev.previous; - edge.previous.next = edge; - - edge.opposite = hedgeOpp; - hedgeOpp.opposite = edge; - - // oppFace was modified, so need to recompute - //oppFace.computeNormalAndCentroid(); - } - else - { - prev.next = edge; - edge.previous = prev; - } - - return discardedFace; - } - - // calculates the outermost edges of the geometry seen from the eyePoint - private void CalculateHorizon(Face face, Vector3 eyePoint, List unclaimedPoints, - List> outsideSet, - List horizonEdges, HalfEdge currentEdge) - { - face.visited = true; - - // move outside points of this face to unclaimed points - foreach (var set in outsideSet) - { - if (set.Item1 == face) - unclaimedPoints.Add(set.Item2); - } - - HalfEdge startingEdge = currentEdge; - do - { - Face oppositeFace = currentEdge.opposite.face; - if (!oppositeFace.visited) - { - var dist = oppositeFace.DistanceToPoint(eyePoint); - if (dist > epsilon) - { - // positive distance means this is visible - CalculateHorizon(oppositeFace, eyePoint, unclaimedPoints, outsideSet, horizonEdges, - currentEdge.opposite); - } - /*else if (Math.Abs(dist) <= epsilon) - { - dist = dist; - }*/ - else - { - if (!horizonEdges.Contains(currentEdge)) - horizonEdges.Add(currentEdge); - } - } - - currentEdge = currentEdge.next; - } while (currentEdge != startingEdge); - } - } + foreach (var edge in horizonEdges) + { + var newFace = new Face(point, edge.edge.v1, edge.edge.v2); + var newPlane = new Plane(newFace.v1, newFace.v2, newFace.v3); + + var uniqPoints = new List(); + AddUnique(uniqPoints, newFace.v1); + AddUnique(uniqPoints, newFace.v2); + AddUnique(uniqPoints, newFace.v3); + + var fourtPoint = edge.opposite.next.edge.v2; + + AddUnique(uniqPoints, edge.opposite.next.edge.v1); + AddUnique(uniqPoints, edge.opposite.next.edge.v2); + AddUnique(uniqPoints, edge.opposite.previous.edge.v1); + AddUnique(uniqPoints, edge.opposite.previous.edge.v2); + + var distFromPlane = PointDistanceFromPlane(fourtPoint, newPlane); + if (Math.Abs(distFromPlane) < epsilon) + { + // both faces are coplanar, merge them together + + + distFromPlane = distFromPlane; + if (AreCoplanar(newFace.v1, newFace.v2, newFace.v3, fourtPoint)) + distFromPlane = distFromPlane; + } + else if (AreCoplanar(newFace.v1, newFace.v2, newFace.v3, fourtPoint)) + { + distFromPlane = distFromPlane; + } + + + + var newEdges = new List(); + foreach (var ne in newFace.GetEdges()) + newEdges.Add(new HalfEdge(ne, newFace)); + + for (int i = 0; i < newEdges.Count; i++) + { + newEdges[i].previous = newEdges[(i + 2) % 3]; + newEdges[i].next = newEdges[(i + 1) % 3]; + } + + if (prevEdge != null) + { + var prevAdjacentEdge = newFaces.Last().halfEdges.Last(); + var lastAdjacentEdge = newEdges.First(); + lastAdjacentEdge.opposite = prevAdjacentEdge; + prevAdjacentEdge.opposite = lastAdjacentEdge; + } + + //edge.face = newFace; + + newEdges[1].opposite = edge.opposite; + edge.opposite.opposite = newEdges[1]; + + newFaces.Add(newFace); + prevEdge = edge; + } + + if (prevEdge != null) + { + var lastAdjacentEdge = newFaces.Last().halfEdges.Last(); + var firstAdjacentEdge = newFaces.First().halfEdges.First(); + lastAdjacentEdge.opposite = firstAdjacentEdge; + firstAdjacentEdge.opposite = lastAdjacentEdge; + //first.previous.opposite = prev.next; + //prev.next.opposite = first.previous; + } + + // merge NONCONVEX_WRT_LARGER_FACE + + //List discardedFaces = new List(); + if (false) + { + foreach (var newFace in newFaces) + { + // if face visible? + while (AdjacentMerge(point, newFace, unclaimedPoints, outsideSet, true)) + { + // merge until failure + } + + hullFaces.Add(newFace); + } + + foreach (var newFace in newFaces) + { + // if face non-convex? + // mark face as visible? + while (AdjacentMerge(point, newFace, unclaimedPoints, outsideSet, false)) + { + // merge until failure + } + + hullFaces.Add(newFace); + } + } + else + hullFaces.AddRange(newFaces); + + // verify + foreach (var newFace in hullFaces) + { + Assert.IsTrue(newFace.halfEdges.Count == 3, "AddPointToHull: side.halfEdges.Count == 3"); + foreach (var halfEdge in newFace.halfEdges) + { + /*bool found = false; + foreach (var otherFace in hullFaces) + { + if (found) + break; + if (newFace == otherFace) + continue; + + foreach (var otherHalfEdge in otherFace.halfEdges) + { + if (otherHalfEdge.opposite != null) + continue; + + if (halfEdge.edge == otherHalfEdge.edge) + { + halfEdge.opposite = otherHalfEdge; + otherHalfEdge.opposite = halfEdge; + //halfEdge.oppositeFace = otherFace; + //otherHalfEdge.oppositeFace = face; + found = true; + break; + } + } + }*/ + + Assert.IsTrue(halfEdge.previous != null, "AddPointToHull: halfEdge.previous != null"); + Assert.IsTrue(halfEdge.next != null, "AddPointToHull: halfEdge.next != null"); + Assert.IsTrue(halfEdge.next.next.next == halfEdge, "AddPointToHull: halfEdge.next.next.next == halfEdge"); + Assert.IsTrue(halfEdge.previous.previous.previous == halfEdge, "AddPointToHull: halfEdge.previous.previous.previous == halfEdge"); + Assert.IsTrue(halfEdge.opposite != null, "AddPointToHull: halfEdge.opposite != null"); + //Assert.IsTrue(halfEdge.oppositeFace != null, "halfEdge.oppositeFace != null"); + Assert.IsTrue(halfEdge.opposite.face != null, "AddPointToHull: halfEdge.opposite.face != null"); + //Assert.IsTrue(halfEdge.oppositeFace == halfEdge.opposite.face, "halfEdge.oppositeFace == halfEdge.opposite.face"); + } + } + } + } + + private bool AdjacentMerge(Float3 point, Face face, List unclaimedPoints, List> outsideSet, bool mergeWrtLargerFace) + { + const float tolerance = -1f; + + HalfEdge edge = face.halfEdges[0]; + + bool convex = true; + do + { + Face oppositeFace = edge.opposite.face; + bool merge = false; + + var p1 = new Plane(face.v1, face.v2, face.v3); + var p2 = new Plane(oppositeFace.v1, oppositeFace.v2, oppositeFace.v3); + + if (mergeWrtLargerFace) + { + float faceArea = edge.face.GetArea(); + float oppositeArea = edge.opposite.face.GetArea(); + + if (faceArea > oppositeArea) + { + if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance) + merge = true; + else if (edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) + convex = false; + } + else + { + if (edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) + merge = true; + else if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance) + convex = false; + } + } + else + { + if (edge.face.DistanceToPlane(edge.opposite.face) > -tolerance || + edge.opposite.face.DistanceToPlane(edge.face) > -tolerance) + { + merge = true; + } + } + + if (merge) + { + List discardedFaces = new List(); + // mergeAdjacentFace + if (!MergeAdjacentFaces(edge, face, face, discardedFaces)) + { + throw new Exception("merge failure"); + } + + foreach (var dface in discardedFaces) + { + for (int i = 0; i 0) + outsideSet[i] = new Tuple(face, outsideSet[i].Item2); + else + unclaimedPoints.Add(outsideSet[i].Item2); + } + } + } + } + } while (edge != face.halfEdges[0]); + + return false; // no merge + } + + private bool MergeAdjacentFaces(HalfEdge edge, Face newFace, Face oldFace, List discardedFaces) + { + Face oppositeFace = edge.opposite.face; + + discardedFaces.Add(oppositeFace); + + HalfEdge prev = edge.previous; + HalfEdge next = edge.next; + HalfEdge oppositePrev = edge.opposite.previous; + HalfEdge oppositeNext = edge.opposite.next; + + HalfEdge breakEdge = prev; + while (prev.opposite.face == oppositeFace) + { + prev = prev.previous; + oppositeNext = oppositeNext.next; + + if (prev == breakEdge) + return false; + } + + breakEdge = next; + while (next.opposite.face == oppositeFace) + { + oppositePrev = oppositePrev.previous; + next = next.next; + + if (next == breakEdge) + return false; + } + + for (HalfEdge e = oppositeNext; e != oppositePrev.next; e = e.next) + e.face = newFace; + + if (edge == oldFace.halfEdges[0]) + oldFace.halfEdges[0] = next; + + Face discardedFace = ConnectHalfEdges(newFace, oppositePrev, next); + Face discardedFace2 = ConnectHalfEdges(newFace, prev, oppositeNext); + + if (discardedFace != null) + discardedFaces.Add(discardedFace); + if (discardedFace2 != null) + discardedFaces.Add(discardedFace2); + + return true; + } + + // merges adjacent faces + private Face ConnectHalfEdges(Face face, HalfEdge prev, HalfEdge edge) + { + Face discardedFace = null; + + if (prev.opposite.face == edge.opposite.face) + { + Face oppFace = edge.opposite.face; + HalfEdge hedgeOpp; + + if (prev == face.halfEdges[0]) + { + face.halfEdges[0] = edge; + } + + bool isDegenerate = false; + { + // is this correct? + HalfEdge s = oppFace.halfEdges[0]; + if (s.next.next.next != s) + isDegenerate = true; + else if (s.previous.previous.previous != s) + isDegenerate = true; + else if (s.next.next == s) + isDegenerate = true; + else if (s.previous.previous == s) + isDegenerate = true; + + HalfEdge ee = s; + int numVerts = 0; + do + { + numVerts++; + ee = ee.next; + } while (ee != s); + + if (numVerts <= 2) + isDegenerate = true; + } + + //if (oppFace.numVertices() == 3) + if (!isDegenerate) + { + // then we can get rid of the + // opposite face altogether + hedgeOpp = edge.opposite.previous.opposite; + + //oppFace.mark = DELETED; + discardedFace = oppFace; + } + else + { + hedgeOpp = edge.opposite.next; + + if (oppFace.halfEdges[0] == hedgeOpp.previous) { + oppFace.halfEdges[0] = hedgeOpp; + } + hedgeOpp.previous = hedgeOpp.previous.previous; + hedgeOpp.previous.next = hedgeOpp; + } + edge.previous = prev.previous; + edge.previous.next = edge; + + edge.opposite = hedgeOpp; + hedgeOpp.opposite = edge; + + // oppFace was modified, so need to recompute + //oppFace.computeNormalAndCentroid(); + } + else + { + prev.next = edge; + edge.previous = prev; + } + + return discardedFace; + } + + // calculates the outermost edges of the geometry seen from the eyePoint + private void CalculateHorizon(Face face, Vector3 eyePoint, List unclaimedPoints, + List> outsideSet, + List horizonEdges, HalfEdge currentEdge) + { + face.visited = true; + + // move outside points of this face to unclaimed points + foreach (var set in outsideSet) + { + if (set.Item1 == face) + unclaimedPoints.Add(set.Item2); + } + + HalfEdge startingEdge = currentEdge; + do + { + Face oppositeFace = currentEdge.opposite.face; + if (!oppositeFace.visited) + { + var dist = oppositeFace.DistanceToPoint(eyePoint); + if (dist > epsilon) + { + // positive distance means this is visible + CalculateHorizon(oppositeFace, eyePoint, unclaimedPoints, outsideSet, horizonEdges, + currentEdge.opposite); + } + /*else if (Math.Abs(dist) <= epsilon) + { + dist = dist; + }*/ + else + { + if (!horizonEdges.Contains(currentEdge)) + horizonEdges.Add(currentEdge); + } + } + + currentEdge = currentEdge.next; + } while (currentEdge != startingEdge); + } } #endif \ No newline at end of file diff --git a/Source/Game/Level/QuickHull2.cs b/Source/Game/Level/QuickHull2.cs index 3035cef..1b91405 100644 --- a/Source/Game/Level/QuickHull2.cs +++ b/Source/Game/Level/QuickHull2.cs @@ -51,114 +51,114 @@ namespace Game; /// public class ConvexHullCalculator { - /// - /// Constant representing a point that has yet to be assigned to a - /// face. It's only used immediately after constructing the seed hull. - /// - private const int UNASSIGNED = -2; + /// + /// Constant representing a point that has yet to be assigned to a + /// face. It's only used immediately after constructing the seed hull. + /// + private const int UNASSIGNED = -2; - /// - /// Constant representing a point that is inside the convex hull, and - /// thus is behind all faces. In the openSet array, all points with - /// INSIDE are at the end of the array, with indexes larger - /// openSetTail. - /// - private const int INSIDE = -1; + /// + /// Constant representing a point that is inside the convex hull, and + /// thus is behind all faces. In the openSet array, all points with + /// INSIDE are at the end of the array, with indexes larger + /// openSetTail. + /// + private const int INSIDE = -1; - /// - /// Epsilon value. If the coordinates of the point space are - /// exceptionally close to each other, this value might need to be - /// adjusted. - /// - private const float EPSILON = 0.001f; + /// + /// Epsilon value. If the coordinates of the point space are + /// exceptionally close to each other, this value might need to be + /// adjusted. + /// + private const float EPSILON = 0.001f; - /// - /// When adding a new face to the faces Dictionary, use this for the - /// key and then increment it. - /// - private int faceCount; + /// + /// When adding a new face to the faces Dictionary, use this for the + /// key and then increment it. + /// + private int faceCount; - /// - /// A dictionary storing the faces of the currently generated convex - /// hull. The key is the id of the face, used in the Face, PointFace - /// and HorizonEdge struct. - /// This is a Dictionary, because we need both random access to it, - /// the ability to loop through it, and ability to quickly delete - /// faces (in the ConstructCone method), and Dictionary is the obvious - /// candidate that can do all of those things. - /// I'm wondering if using a Dictionary is best idea, though. It might - /// be better to just have them in a ]]> and mark a face as - /// deleted by adding a field to the Face struct. The downside is that - /// we would need an extra field in the Face struct, and when we're - /// looping through the points in openSet, we would have to loop - /// through all the Faces EVER created in the algorithm, and skip the - /// ones that have been marked as deleted. However, looping through a - /// list is fairly fast, and it might be worth it to avoid Dictionary - /// overhead. - /// TODO test converting to a ]]> instead. - /// - private Dictionary faces; + /// + /// A dictionary storing the faces of the currently generated convex + /// hull. The key is the id of the face, used in the Face, PointFace + /// and HorizonEdge struct. + /// This is a Dictionary, because we need both random access to it, + /// the ability to loop through it, and ability to quickly delete + /// faces (in the ConstructCone method), and Dictionary is the obvious + /// candidate that can do all of those things. + /// I'm wondering if using a Dictionary is best idea, though. It might + /// be better to just have them in a ]]> and mark a face as + /// deleted by adding a field to the Face struct. The downside is that + /// we would need an extra field in the Face struct, and when we're + /// looping through the points in openSet, we would have to loop + /// through all the Faces EVER created in the algorithm, and skip the + /// ones that have been marked as deleted. However, looping through a + /// list is fairly fast, and it might be worth it to avoid Dictionary + /// overhead. + /// TODO test converting to a ]]> instead. + /// + private Dictionary faces; - /// - /// The current horizon. Generated by the FindHorizon() DFS search, - /// and used in ConstructCone to construct new faces. The list of - /// edges are in CCW order. - /// - private List horizon; + /// + /// The current horizon. Generated by the FindHorizon() DFS search, + /// and used in ConstructCone to construct new faces. The list of + /// edges are in CCW order. + /// + private List horizon; - /// - /// If SplitVerts is false, this Dictionary is used to keep track of - /// which points we've added to the final mesh. - /// - private Dictionary hullVerts; + /// + /// If SplitVerts is false, this Dictionary is used to keep track of + /// which points we've added to the final mesh. + /// + private Dictionary hullVerts; - /// - /// Set of faces which are "lit" by the current point in the set. This - /// is used in the FindHorizon() DFS search to keep track of which - /// faces we've already visited, and in the ReassignPoints() method to - /// know which points need to be reassigned. - /// - private HashSet litFaces; + /// + /// Set of faces which are "lit" by the current point in the set. This + /// is used in the FindHorizon() DFS search to keep track of which + /// faces we've already visited, and in the ReassignPoints() method to + /// know which points need to be reassigned. + /// + private HashSet litFaces; - /// - /// The set of points to be processed. "openSet" is a misleading name, - /// because it's both the open set (points which are still outside the - /// convex hull) and the closed set (points that are inside the convex - /// hull). The first part of the array (with ) - /// is the openSet, the last part of the array (with - /// openSetTail]]>) are the closed set, with - /// Face set to INSIDE. The - /// closed set is largely irrelevant to the algorithm, the open set is - /// what matters. - /// Storing the entire open set in one big list has a downside: when - /// we're reassigning points after ConstructCone, we only need to - /// reassign points that belong to the faces that have been removed, - /// but storing it in one array, we have to loop through the entire - /// list, and checking litFaces to determine which we can skip and - /// which need to be reassigned. - /// The alternative here is to give each face in Face array it's own - /// openSet. I don't like that solution, because then you have to - /// juggle so many more heap-allocated 's]]>, we'd have to use - /// object pools and such. It would do a lot more allocation, and it - /// would have worse locality. I should maybe test that solution, but - /// it probably wont be faster enough (if at all) to justify the extra - /// allocations. - /// - private List openSet; + /// + /// The set of points to be processed. "openSet" is a misleading name, + /// because it's both the open set (points which are still outside the + /// convex hull) and the closed set (points that are inside the convex + /// hull). The first part of the array (with ) + /// is the openSet, the last part of the array (with + /// openSetTail]]>) are the closed set, with + /// Face set to INSIDE. The + /// closed set is largely irrelevant to the algorithm, the open set is + /// what matters. + /// Storing the entire open set in one big list has a downside: when + /// we're reassigning points after ConstructCone, we only need to + /// reassign points that belong to the faces that have been removed, + /// but storing it in one array, we have to loop through the entire + /// list, and checking litFaces to determine which we can skip and + /// which need to be reassigned. + /// The alternative here is to give each face in Face array it's own + /// openSet. I don't like that solution, because then you have to + /// juggle so many more heap-allocated 's]]>, we'd have to use + /// object pools and such. It would do a lot more allocation, and it + /// would have worse locality. I should maybe test that solution, but + /// it probably wont be faster enough (if at all) to justify the extra + /// allocations. + /// + private List openSet; - /// - /// The "tail" of the openSet, the last index of a vertex that has - /// been assigned to a face. - /// - private int openSetTail = -1; + /// + /// The "tail" of the openSet, the last index of a vertex that has + /// been assigned to a face. + /// + private int openSetTail = -1; - /// - /// Generate a convex hull from points in points array, and store the - /// mesh in Unity-friendly format in verts and tris. If splitVerts is - /// true, the the verts will be split, if false, the same vert will be - /// used for more than one triangle. - /// - public void GenerateHull( + /// + /// Generate a convex hull from points in points array, and store the + /// mesh in Unity-friendly format in verts and tris. If splitVerts is + /// true, the the verts will be split, if false, the same vert will be + /// used for more than one triangle. + /// + public void GenerateHull( List points, bool splitVerts, ref List verts, @@ -179,11 +179,11 @@ public class ConvexHullCalculator //VerifyMesh(points, ref verts, ref tris); } - /// - /// Make sure all the buffers and variables needed for the algorithm - /// are initialized. - /// - private void Initialize(List points, bool splitVerts) + /// + /// Make sure all the buffers and variables needed for the algorithm + /// are initialized. + /// + private void Initialize(List points, bool splitVerts) { faceCount = 0; openSetTail = -1; @@ -225,10 +225,10 @@ public class ConvexHullCalculator } } - /// - /// Create initial seed hull. - /// - private void GenerateInitialHull(List points) + /// + /// Create initial seed hull. + /// + private void GenerateInitialHull(List points) { // Find points suitable for use as the seed hull. Some varieties of // this algorithm pick extreme points here, but I'm not convinced @@ -356,11 +356,11 @@ public class ConvexHullCalculator VerifyOpenSet(points); } - /// - /// Find four points in the point cloud that are not coplanar for the - /// seed hull - /// - private void FindInitialHullIndices(List points, out int b0, out int b1, out int b2, out int b3) + /// + /// Find four points in the point cloud that are not coplanar for the + /// seed hull + /// + private void FindInitialHullIndices(List points, out int b0, out int b1, out int b2, out int b3) { int count = points.Count; @@ -399,12 +399,12 @@ public class ConvexHullCalculator throw new ArgumentException("Can't generate hull, points are coplanar"); } - /// - /// Grow the hull. This method takes the current hull, and expands it - /// to encompass the point in openSet with the point furthest away - /// from its face. - /// - private void GrowHull(List points) + /// + /// Grow the hull. This method takes the current hull, and expands it + /// to encompass the point in openSet with the point furthest away + /// from its face. + /// + private void GrowHull(List points) { Assert.IsTrue(openSetTail >= 0); Assert.IsTrue(openSet[0].Face != INSIDE); @@ -439,21 +439,21 @@ public class ConvexHullCalculator ReassignPoints(points); } - /// - /// Start the search for the horizon. - /// The search is a DFS search that searches neighboring triangles in - /// a counter-clockwise fashion. When it find a neighbor which is not - /// lit, that edge will be a line on the horizon. If the search always - /// proceeds counter-clockwise, the edges of the horizon will be found - /// in counter-clockwise order. - /// The heart of the search can be found in the recursive - /// SearchHorizon() method, but the the first iteration of the search - /// is special, because it has to visit three neighbors (all the - /// neighbors of the initial triangle), while the rest of the search - /// only has to visit two (because one of them has already been - /// visited, the one you came from). - /// - private void FindHorizon(List points, Float3 point, int fi, Face face) + /// + /// Start the search for the horizon. + /// The search is a DFS search that searches neighboring triangles in + /// a counter-clockwise fashion. When it find a neighbor which is not + /// lit, that edge will be a line on the horizon. If the search always + /// proceeds counter-clockwise, the edges of the horizon will be found + /// in counter-clockwise order. + /// The heart of the search can be found in the recursive + /// SearchHorizon() method, but the the first iteration of the search + /// is special, because it has to visit three neighbors (all the + /// neighbors of the initial triangle), while the rest of the search + /// only has to visit two (because one of them has already been + /// visited, the one you came from). + /// + private void FindHorizon(List points, Float3 point, int fi, Face face) { // TODO should I use epsilon in the PointFaceDistance comparisons? @@ -529,10 +529,10 @@ public class ConvexHullCalculator } } - /// - /// Recursively search to find the horizon or lit set. - /// - private void SearchHorizon(List points, Float3 point, int prevFaceIndex, int faceCount, Face face) + /// + /// Recursively search to find the horizon or lit set. + /// + private void SearchHorizon(List points, Float3 point, int prevFaceIndex, int faceCount, Face face) { Assert.IsTrue(prevFaceIndex >= 0); Assert.IsTrue(litFaces.Contains(prevFaceIndex)); @@ -622,18 +622,18 @@ public class ConvexHullCalculator } } - /// - /// Remove all lit faces and construct new faces from the horizon in a - /// "cone-like" fashion. - /// This is a relatively straight-forward procedure, given that the - /// horizon is handed to it in already sorted counter-clockwise. The - /// neighbors of the new faces are easy to find: they're the previous - /// and next faces to be constructed in the cone, as well as the face - /// on the other side of the horizon. We also have to update the face - /// on the other side of the horizon to reflect it's new neighbor from - /// the cone. - /// - private void ConstructCone(List points, int farthestPoint) + /// + /// Remove all lit faces and construct new faces from the horizon in a + /// "cone-like" fashion. + /// This is a relatively straight-forward procedure, given that the + /// horizon is handed to it in already sorted counter-clockwise. The + /// neighbors of the new faces are easy to find: they're the previous + /// and next faces to be constructed in the cone, as well as the face + /// on the other side of the horizon. We also have to update the face + /// on the other side of the horizon to reflect it's new neighbor from + /// the cone. + /// + private void ConstructCone(List points, int farthestPoint) { foreach (int fi in litFaces) { @@ -713,21 +713,21 @@ public class ConvexHullCalculator } } - /// - /// Reassign points based on the new faces added by ConstructCone(). - /// Only points that were previous assigned to a removed face need to - /// be updated, so check litFaces while looping through the open set. - /// There is a potential optimization here: there's no reason to loop - /// through the entire openSet here. If each face had it's own - /// openSet, we could just loop through the openSets in the removed - /// faces. That would make the loop here shorter. - /// However, to do that, we would have to juggle A LOT more 's]]>, - /// and we would need an object pool to manage them all without - /// generating a whole bunch of garbage. I don't think it's worth - /// doing that to make this loop shorter, a straight for-loop through - /// a list is pretty darn fast. Still, it might be worth trying - /// - private void ReassignPoints(List points) + /// + /// Reassign points based on the new faces added by ConstructCone(). + /// Only points that were previous assigned to a removed face need to + /// be updated, so check litFaces while looping through the open set. + /// There is a potential optimization here: there's no reason to loop + /// through the entire openSet here. If each face had it's own + /// openSet, we could just loop through the openSets in the removed + /// faces. That would make the loop here shorter. + /// However, to do that, we would have to juggle A LOT more 's]]>, + /// and we would need an object pool to manage them all without + /// generating a whole bunch of garbage. I don't think it's worth + /// doing that to make this loop shorter, a straight for-loop through + /// a list is pretty darn fast. Still, it might be worth trying + /// + private void ReassignPoints(List points) { for (int i = 0; i <= openSetTail; i++) { @@ -780,13 +780,13 @@ public class ConvexHullCalculator } } - /// - /// Final step in algorithm, export the faces of the convex hull in a - /// mesh-friendly format. - /// TODO normals calculation for non-split vertices. Right now it just - /// leaves the normal array empty. - /// - private void ExportMesh( + /// + /// Final step in algorithm, export the faces of the convex hull in a + /// mesh-friendly format. + /// TODO normals calculation for non-split vertices. Right now it just + /// leaves the normal array empty. + /// + private void ExportMesh( List points, bool splitVerts, ref List verts, @@ -855,40 +855,40 @@ public class ConvexHullCalculator } } - /// - /// Signed distance from face to point (a positive number means that - /// the point is above the face) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Signed distance from face to point (a positive number means that + /// the point is above the face) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private float PointFaceDistance(Float3 point, Float3 pointOnFace, Face face) { return Dot(face.Normal, point - pointOnFace); } - /// - /// Calculate normal for triangle - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Calculate normal for triangle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private Float3 Normal(Float3 v0, Float3 v1, Float3 v2) { return Cross(v1 - v0, v2 - v0).Normalized; } - /// - /// Dot product, for convenience. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Dot product, for convenience. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float Dot(Float3 a, Float3 b) { return a.X * b.X + a.Y * b.Y + a.Z * b.Z; } - /// - /// Float3.Cross i left-handed, the algorithm is right-handed. Also, - /// i wanna test to see if using aggressive inlining makes any - /// difference here. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Float3.Cross i left-handed, the algorithm is right-handed. Also, + /// i wanna test to see if using aggressive inlining makes any + /// difference here. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Float3 Cross(Float3 a, Float3 b) { return new Float3( @@ -897,28 +897,28 @@ public class ConvexHullCalculator a.X * b.Y - a.Y * b.X); } - /// - /// Check if two points are coincident - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Check if two points are coincident + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool AreCoincident(Float3 a, Float3 b) { return (a - b).Length <= EPSILON; } - /// - /// Check if three points are collinear - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Check if three points are collinear + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool AreCollinear(Float3 a, Float3 b, Float3 c) { return Cross(c - a, c - b).Length <= EPSILON; } - /// - /// Check if four points are coplanar - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Check if four points are coplanar + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool AreCoplanar(Float3 a, Float3 b, Float3 c, Float3 d) { Float3 n1 = Cross(c - a, c - b); @@ -934,12 +934,12 @@ public class ConvexHullCalculator 1.0f / m2 * n2); } - /// - /// Method used for debugging, verifies that the openSet is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - private void VerifyOpenSet(List points) + /// + /// Method used for debugging, verifies that the openSet is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyOpenSet(List points) { for (int i = 0; i < openSet.Count; i++) if (i > openSetTail) @@ -958,12 +958,12 @@ public class ConvexHullCalculator } } - /// - /// Method used for debugging, verifies that the horizon is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - private void VerifyHorizon() + /// + /// Method used for debugging, verifies that the horizon is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyHorizon() { for (int i = 0; i < horizon.Count; i++) { @@ -974,12 +974,12 @@ public class ConvexHullCalculator } } - /// - /// Method used for debugging, verifies that the faces array is in a - /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if - /// defined. - /// - private void VerifyFaces(List points) + /// + /// Method used for debugging, verifies that the faces array is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + private void VerifyFaces(List points) { foreach (var kvp in faces) { @@ -1009,12 +1009,12 @@ public class ConvexHullCalculator } } - /// - /// Method used for debugging, verifies that the final mesh is - /// actually a convex hull of all the points. Conditionally compiled - /// if DEBUG_QUICKHULL if defined. - /// - private void VerifyMesh(List points, ref List verts, ref List tris) + /// + /// Method used for debugging, verifies that the final mesh is + /// actually a convex hull of all the points. Conditionally compiled + /// if DEBUG_QUICKHULL if defined. + /// + private void VerifyMesh(List points, ref List verts, ref List tris) { Assert.IsTrue(tris.Count % 3 == 0); @@ -1032,28 +1032,28 @@ public class ConvexHullCalculator } } - /// - /// Does face f have a face with vertexes e0 and e1? Used only for - /// debugging. - /// - private bool HasEdge(Face f, int e0, int e1) + /// + /// Does face f have a face with vertexes e0 and e1? Used only for + /// debugging. + /// + private bool HasEdge(Face f, int e0, int e1) { return (f.Vertex0 == e0 && f.Vertex1 == e1) || (f.Vertex1 == e0 && f.Vertex2 == e1) || (f.Vertex2 == e0 && f.Vertex0 == e1); } - /// - /// Struct representing a single face. - /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They - /// acutal points are stored in the points array, these are just - /// indexes into that array. - /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which - /// share an edge with this face. Opposite0 is the face opposite - /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. - /// Normal is (unsurprisingly) the normal of the triangle. - /// - private struct Face + /// + /// Struct representing a single face. + /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They + /// acutal points are stored in the points array, these are just + /// indexes into that array. + /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which + /// share an edge with this face. Opposite0 is the face opposite + /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. + /// Normal is (unsurprisingly) the normal of the triangle. + /// + private struct Face { public readonly int Vertex0; public readonly int Vertex1; @@ -1088,14 +1088,14 @@ public class ConvexHullCalculator } } - /// - /// Struct representing a mapping between a point and a face. These - /// are used in the openSet array. - /// Point is the index of the point in the points array, Face is the - /// key of the face in the Key dictionary, Distance is the distance - /// from the face to the point. - /// - private struct PointFace + /// + /// Struct representing a mapping between a point and a face. These + /// are used in the openSet array. + /// Point is the index of the point in the points array, Face is the + /// key of the face in the Key dictionary, Distance is the distance + /// from the face to the point. + /// + private struct PointFace { public readonly int Point; public int Face; @@ -1109,14 +1109,14 @@ public class ConvexHullCalculator } } - /// - /// Struct representing a single edge in the horizon. - /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the - /// face on the other side of the horizon. - /// TODO Edge1 isn't actually needed, you can just index the next item - /// in the horizon array. - /// - private struct HorizonEdge + /// + /// Struct representing a single edge in the horizon. + /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the + /// face on the other side of the horizon. + /// TODO Edge1 isn't actually needed, you can just index the next item + /// in the horizon array. + /// + private struct HorizonEdge { public int Face; public int Edge0; diff --git a/Source/Game/Player/InputManager.cs b/Source/Game/Player/InputManager.cs index 7c75c2c..ebc33a3 100644 --- a/Source/Game/Player/InputManager.cs +++ b/Source/Game/Player/InputManager.cs @@ -1,28 +1,27 @@ using FlaxEngine; -namespace Game +namespace Game; + +public static class InputManager { - public static class InputManager + public static bool GetAction(string name) { - public static bool GetAction(string name) - { - if (Console.IsOpen) - return false; - return Input.GetAction(name); - } + if (Console.IsOpen) + return false; + return Input.GetAction(name); + } - public static float GetAxis(string name) - { - if (Console.IsOpen) - return 0.0f; - return Input.GetAxis(name); - } + public static float GetAxis(string name) + { + if (Console.IsOpen) + return 0.0f; + return Input.GetAxis(name); + } - public static float GetAxisRaw(string name) - { - if (Console.IsOpen) - return 0.0f; - return Input.GetAxisRaw(name); - } + public static float GetAxisRaw(string name) + { + if (Console.IsOpen) + return 0.0f; + return Input.GetAxisRaw(name); } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInput.cs b/Source/Game/Player/PlayerInput.cs index f8c0360..29f0959 100644 --- a/Source/Game/Player/PlayerInput.cs +++ b/Source/Game/Player/PlayerInput.cs @@ -2,173 +2,172 @@ using FlaxEngine; using FlaxEngine.Networking; -namespace Game +namespace Game; + +[StructLayout(LayoutKind.Sequential)] +public struct PlayerInputState { - [StructLayout(LayoutKind.Sequential)] - public struct PlayerInputState + /*static PlayerInputState() { - /*static PlayerInputState() - { - NetworkReplicator.AddSerializer(typeof(PlayerInputState), - (ptr, streamPtr) => - { - - }, - (ptr, streamPtr) => - { - - }); - }*/ - - #if false - public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight, - bool attacking, bool jumping) - { - this.frame = frame; - this.viewDeltaX = viewDeltaX; - this.viewDeltaY = viewDeltaY; - this.moveForward = moveForward; - this.moveRight = moveRight; - this.attacking = attacking; - this.jumping = jumping; - /*this.verificationPosition = verificationPosition; - this.verificationVelocity = verificationVelocity; - this.verificationViewAngles = verificationViewAngles; - this.verificationOrientation = verificationOrientation;*/ - } - - public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight, - bool attacking, bool jumping, Float3 verificationPosition, Float3 verificationVelocity, - Float3 verificationViewAngles, Quaternion verificationOrientation) - { - this.frame = frame; - this.viewDeltaX = viewDeltaX; - this.viewDeltaY = viewDeltaY; - this.moveForward = moveForward; - this.moveRight = moveRight; - this.attacking = attacking; - this.jumping = jumping; - this.verificationPosition = verificationPosition; - this.verificationVelocity = verificationVelocity; - this.verificationViewAngles = verificationViewAngles; - this.verificationOrientation = verificationOrientation; - } - #endif - - public ulong frame; - public float viewDeltaX, viewDeltaY; - public float moveForward; - public float moveRight; - public bool attacking; - public bool jumping; - - public Float3 verificationPosition; - public Float3 verificationVelocity; - public Float3 verificationViewAngles; - public Quaternion verificationOrientation; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PlayerActorState - { - public Float3 position; - public Float3 velocity; - public Quaternion orientation; - public Float3 viewAngles; // yaw, pitch, roll - public float lastJumpTime; - public int numJumps; - public bool jumped; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PlayerState - { - public PlayerInputState input; - public PlayerActorState actor; - } - - public class PlayerInput - { - public const byte DemoVer = 1; - public const int MaxPlayerStates = 120; - - public PlayerState currentState; - public ulong frame; - //public ulong oldestFrame; - - private PlayerState[] states = new PlayerState[MaxPlayerStates]; - - public virtual bool Predict => false; - - public virtual void OnUpdate() - { - } - - public virtual void OnFixedUpdate() - { - } - - public virtual void OnEndFrame() - { - //Console.Print("recorded frame " + frame); - - states[frame % MaxPlayerStates] = currentState; - - /*ulong oldest = ulong.MaxValue; - for (int i = 0; i < 120; i++) - oldest = states[i].input.frame < oldest ? states[i].input.frame : oldest; - oldestFrame = oldest;*/ - - frame++; - currentState.input.frame = frame; - } - - public virtual void RecordCurrentActorState(PlayerActorState actorState) - { - if (actorState.position.Length <= 0.01) - Console.Print("wrong recorded position?"); - currentState.actor = actorState; - } - - public bool GetState(ulong frame, out PlayerInputState inputState, out PlayerActorState actorState) - { - int frameIndex = (int)frame % MaxPlayerStates; - if (states[frameIndex].input.frame != frame) + NetworkReplicator.AddSerializer(typeof(PlayerInputState), + (ptr, streamPtr) => { - inputState = default; - actorState = default; - return false; - } - inputState = states[frameIndex].input; - actorState = states[frameIndex].actor; - return true; - } + }, + (ptr, streamPtr) => + { - public void SetState(ulong frame, PlayerInputState inputState) + }); + }*/ + + #if false + public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight, + bool attacking, bool jumping) + { + this.frame = frame; + this.viewDeltaX = viewDeltaX; + this.viewDeltaY = viewDeltaY; + this.moveForward = moveForward; + this.moveRight = moveRight; + this.attacking = attacking; + this.jumping = jumping; + /*this.verificationPosition = verificationPosition; + this.verificationVelocity = verificationVelocity; + this.verificationViewAngles = verificationViewAngles; + this.verificationOrientation = verificationOrientation;*/ + } + + public PlayerInputState(ulong frame, float viewDeltaX, float viewDeltaY, float moveForward, float moveRight, + bool attacking, bool jumping, Float3 verificationPosition, Float3 verificationVelocity, + Float3 verificationViewAngles, Quaternion verificationOrientation) + { + this.frame = frame; + this.viewDeltaX = viewDeltaX; + this.viewDeltaY = viewDeltaY; + this.moveForward = moveForward; + this.moveRight = moveRight; + this.attacking = attacking; + this.jumping = jumping; + this.verificationPosition = verificationPosition; + this.verificationVelocity = verificationVelocity; + this.verificationViewAngles = verificationViewAngles; + this.verificationOrientation = verificationOrientation; + } + #endif + + public ulong frame; + public float viewDeltaX, viewDeltaY; + public float moveForward; + public float moveRight; + public bool attacking; + public bool jumping; + + public Float3 verificationPosition; + public Float3 verificationVelocity; + public Float3 verificationViewAngles; + public Quaternion verificationOrientation; +} + +[StructLayout(LayoutKind.Sequential)] +public struct PlayerActorState +{ + public Float3 position; + public Float3 velocity; + public Quaternion orientation; + public Float3 viewAngles; // yaw, pitch, roll + public float lastJumpTime; + public int numJumps; + public bool jumped; +} + +[StructLayout(LayoutKind.Sequential)] +public struct PlayerState +{ + public PlayerInputState input; + public PlayerActorState actor; +} + +public class PlayerInput +{ + public const byte DemoVer = 1; + public const int MaxPlayerStates = 120; + + public PlayerState currentState; + public ulong frame; + //public ulong oldestFrame; + + private PlayerState[] states = new PlayerState[MaxPlayerStates]; + + public virtual bool Predict => false; + + public virtual void OnUpdate() + { + } + + public virtual void OnFixedUpdate() + { + } + + public virtual void OnEndFrame() + { + //Console.Print("recorded frame " + frame); + + states[frame % MaxPlayerStates] = currentState; + + /*ulong oldest = ulong.MaxValue; + for (int i = 0; i < 120; i++) + oldest = states[i].input.frame < oldest ? states[i].input.frame : oldest; + oldestFrame = oldest;*/ + + frame++; + currentState.input.frame = frame; + } + + public virtual void RecordCurrentActorState(PlayerActorState actorState) + { + if (actorState.position.Length <= 0.01) + Console.Print("wrong recorded position?"); + currentState.actor = actorState; + } + + public bool GetState(ulong frame, out PlayerInputState inputState, out PlayerActorState actorState) + { + int frameIndex = (int)frame % MaxPlayerStates; + if (states[frameIndex].input.frame != frame) { - int frameIndex = (int)frame % MaxPlayerStates; - states[frameIndex].input = inputState; - states[frameIndex].input.frame = frame; - states[frameIndex].actor = new PlayerActorState(); + inputState = default; + actorState = default; + return false; } - public void SetState(ulong frame, PlayerInputState inputState, PlayerActorState actorState) - { - int frameIndex = (int)frame % MaxPlayerStates; - states[frameIndex].input = inputState; - states[frameIndex].input.frame = frame; - states[frameIndex].actor = actorState; - } + inputState = states[frameIndex].input; + actorState = states[frameIndex].actor; + return true; + } - public PlayerInputState GetCurrentInputState() - { - return currentState.input; - } + public void SetState(ulong frame, PlayerInputState inputState) + { + int frameIndex = (int)frame % MaxPlayerStates; + states[frameIndex].input = inputState; + states[frameIndex].input.frame = frame; + states[frameIndex].actor = new PlayerActorState(); + } - public PlayerActorState GetCurrentActorState() - { - return currentState.actor; - } + public void SetState(ulong frame, PlayerInputState inputState, PlayerActorState actorState) + { + int frameIndex = (int)frame % MaxPlayerStates; + states[frameIndex].input = inputState; + states[frameIndex].input.frame = frame; + states[frameIndex].actor = actorState; + } + + public PlayerInputState GetCurrentInputState() + { + return currentState.input; + } + + public PlayerActorState GetCurrentActorState() + { + return currentState.actor; } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputDemo.cs b/Source/Game/Player/PlayerInputDemo.cs index 190ed6c..303304b 100644 --- a/Source/Game/Player/PlayerInputDemo.cs +++ b/Source/Game/Player/PlayerInputDemo.cs @@ -8,95 +8,94 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Console = Game.Console; -namespace Game +namespace Game; + +public class PlayerInputDemo : PlayerInput { - public class PlayerInputDemo : PlayerInput + protected List buffer = new List(); + protected IEnumerator bufferEnumerable; + + public bool IsPlaying { - protected List buffer = new List(); - protected IEnumerator bufferEnumerable; - - public bool IsPlaying + get { - get - { - return bufferEnumerable != null; - } - } - - public PlayerInputDemo(string demoPath) - { - if (!File.Exists(demoPath)) - return; - - int expectedPlayerInputStateSize = Unsafe.SizeOf(); - - Stopwatch sw = Stopwatch.StartNew(); - - using FileStream fileStream = File.OpenRead(demoPath); - using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress); - - int ver = stream.ReadByte(); - int inputStateSize = stream.ReadByte(); - if (ver != DemoVer || inputStateSize != expectedPlayerInputStateSize) - { - Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " + - inputStateSize + " != " + Unsafe.SizeOf()); - stream.Close(); - return; - } - - Span b = stackalloc byte[expectedPlayerInputStateSize]; - while (true) - { - int bytesLeftInBuffer = expectedPlayerInputStateSize; - do - { - int readBytes = stream.Read(b.Slice(expectedPlayerInputStateSize - bytesLeftInBuffer, bytesLeftInBuffer)); - if (readBytes == 0) - break; - bytesLeftInBuffer -= readBytes; - } while (bytesLeftInBuffer > 0); - - if (bytesLeftInBuffer > 0) - break; // EOF; - - buffer.Add(MemoryMarshal.Read(b)); - } - - sw.Stop(); - - bufferEnumerable = buffer.GetEnumerator(); - - Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} "); - - OnEndFrame(); // advances to first frame - } - - public override void OnEndFrame() - { - // TODO: check if the current state frame matches the current frame number before advancing - - /*asdf++; - if (asdf < 8) - return;*/ - - if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) - { - if (buffer.Any()) - { - bufferEnumerable.Dispose(); - bufferEnumerable = null; - buffer.Clear(); - Console.Print("Demo ended"); - } - - return; - } - - //var actorState = currentState.actor; - currentState.input = bufferEnumerable.Current; - //frame++; - //currentState.actor = actorState; + return bufferEnumerable != null; } } + + public PlayerInputDemo(string demoPath) + { + if (!File.Exists(demoPath)) + return; + + int expectedPlayerInputStateSize = Unsafe.SizeOf(); + + Stopwatch sw = Stopwatch.StartNew(); + + using FileStream fileStream = File.OpenRead(demoPath); + using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress); + + int ver = stream.ReadByte(); + int inputStateSize = stream.ReadByte(); + if (ver != DemoVer || inputStateSize != expectedPlayerInputStateSize) + { + Console.Print("demover mismatch: version " + ver + " != " + DemoVer + ", inputStateSize " + + inputStateSize + " != " + Unsafe.SizeOf()); + stream.Close(); + return; + } + + Span b = stackalloc byte[expectedPlayerInputStateSize]; + while (true) + { + int bytesLeftInBuffer = expectedPlayerInputStateSize; + do + { + int readBytes = stream.Read(b.Slice(expectedPlayerInputStateSize - bytesLeftInBuffer, bytesLeftInBuffer)); + if (readBytes == 0) + break; + bytesLeftInBuffer -= readBytes; + } while (bytesLeftInBuffer > 0); + + if (bytesLeftInBuffer > 0) + break; // EOF; + + buffer.Add(MemoryMarshal.Read(b)); + } + + sw.Stop(); + + bufferEnumerable = buffer.GetEnumerator(); + + Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} "); + + OnEndFrame(); // advances to first frame + } + + public override void OnEndFrame() + { + // TODO: check if the current state frame matches the current frame number before advancing + + /*asdf++; + if (asdf < 8) + return;*/ + + if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) + { + if (buffer.Any()) + { + bufferEnumerable.Dispose(); + bufferEnumerable = null; + buffer.Clear(); + Console.Print("Demo ended"); + } + + return; + } + + //var actorState = currentState.actor; + currentState.input = bufferEnumerable.Current; + //frame++; + //currentState.actor = actorState; + } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputLocal.cs b/Source/Game/Player/PlayerInputLocal.cs index 33b9811..bd8c6bf 100644 --- a/Source/Game/Player/PlayerInputLocal.cs +++ b/Source/Game/Player/PlayerInputLocal.cs @@ -9,142 +9,141 @@ using FlaxEngine; using FlaxEngine.Networking; using Console = Game.Console; -namespace Game +namespace Game; + +public class PlayerInputLocal : PlayerInput { - public class PlayerInputLocal : PlayerInput + protected List buffer = new List(); + protected GZipStream demoStream; + protected FileStream demoFileStream; + + private PlayerActor playerActor; + public bool IsNetworked => NetworkManager.client != null; + private long flushedFrames = 0; + + /*public PlayerInputLocal() { - protected List buffer = new List(); - protected GZipStream demoStream; - protected FileStream demoFileStream; + }*/ - private PlayerActor playerActor; - public bool IsNetworked => NetworkManager.client != null; - private long flushedFrames = 0; + public override bool Predict => true; - /*public PlayerInputLocal() + public PlayerInputLocal(PlayerActor playerActor, string demoPath = null) + { + this.playerActor = playerActor; + if (demoPath == null) + return; + var demoFolder = Directory.GetParent(demoPath); + if (!demoFolder.Exists) + Directory.CreateDirectory(demoFolder.FullName); + + demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); + demoStream = new GZipStream(demoFileStream, CompressionMode.Compress); + demoStream.WriteByte(DemoVer); + demoStream.WriteByte((byte)Unsafe.SizeOf()); + } + + public bool IsRecording => demoStream != null; + + public override void OnUpdate() + { + // Collect all input here + // All axis values here should be accumulated + float sensitivity = 1.0f / 8.0f; + sensitivity = 1.0f; + + //var asf = InputManager.GetAxisRaw("Mouse X"); + //if (asf != 0.0f) + // Console.Print(InputManager.GetAxisRaw("Mouse X").ToString("G9", System.Globalization.CultureInfo.InvariantCulture)); + + currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X") * sensitivity; + currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y") * sensitivity; + currentState.input.viewDeltaX += InputManager.GetAxisRaw("LookRight") * Time.DeltaTime * 100; + currentState.input.viewDeltaY += -InputManager.GetAxisRaw("LookUp") * Time.DeltaTime * 100; + + currentState.input.moveForward = InputManager.GetAxis("Vertical"); + currentState.input.moveRight = InputManager.GetAxis("Horizontal"); + currentState.input.attacking = InputManager.GetAction("Attack"); + currentState.input.jumping = InputManager.GetAction("Jump"); + + + } + + public override void OnFixedUpdate() + { + } + + public override void OnEndFrame() + { + currentState.input.frame = frame; + if (IsRecording) { - }*/ + currentState.input.verificationPosition = currentState.actor.position; + currentState.input.verificationVelocity = currentState.actor.velocity; + currentState.input.verificationViewAngles = currentState.actor.viewAngles; + currentState.input.verificationOrientation = currentState.actor.orientation; - public override bool Predict => true; - - public PlayerInputLocal(PlayerActor playerActor, string demoPath = null) - { - this.playerActor = playerActor; - if (demoPath == null) - return; - var demoFolder = Directory.GetParent(demoPath); - if (!demoFolder.Exists) - Directory.CreateDirectory(demoFolder.FullName); - - demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); - demoStream = new GZipStream(demoFileStream, CompressionMode.Compress); - demoStream.WriteByte(DemoVer); - demoStream.WriteByte((byte)Unsafe.SizeOf()); + buffer.Add(currentState.input); } - public bool IsRecording => demoStream != null; - - public override void OnUpdate() + if (playerActor != null) { - // Collect all input here - // All axis values here should be accumulated - float sensitivity = 1.0f / 8.0f; - sensitivity = 1.0f; - - //var asf = InputManager.GetAxisRaw("Mouse X"); - //if (asf != 0.0f) - // Console.Print(InputManager.GetAxisRaw("Mouse X").ToString("G9", System.Globalization.CultureInfo.InvariantCulture)); - - currentState.input.viewDeltaX += InputManager.GetAxisRaw("Mouse X") * sensitivity; - currentState.input.viewDeltaY += InputManager.GetAxisRaw("Mouse Y") * sensitivity; - currentState.input.viewDeltaX += InputManager.GetAxisRaw("LookRight") * Time.DeltaTime * 100; - currentState.input.viewDeltaY += -InputManager.GetAxisRaw("LookUp") * Time.DeltaTime * 100; - - currentState.input.moveForward = InputManager.GetAxis("Vertical"); - currentState.input.moveRight = InputManager.GetAxis("Horizontal"); - currentState.input.attacking = InputManager.GetAction("Attack"); - currentState.input.jumping = InputManager.GetAction("Jump"); - - - } - - public override void OnFixedUpdate() - { - } - - public override void OnEndFrame() - { - currentState.input.frame = frame; - if (IsRecording) - { - currentState.input.verificationPosition = currentState.actor.position; - currentState.input.verificationVelocity = currentState.actor.velocity; - currentState.input.verificationViewAngles = currentState.actor.viewAngles; - currentState.input.verificationOrientation = currentState.actor.orientation; - - buffer.Add(currentState.input); - } - - if (playerActor != null) - { - //playerActor.UpdateNetworkInput(currentState.input.frame, new Float4(currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight), currentState.input.attacking, currentState.input.jumping); - - } - //playerActor.UpdateNetworkInput(currentState.input.frame, currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight, currentState.input.attacking, currentState.input.jumping, currentState.input.verificationPosition, currentState.input.verificationVelocity, currentState.input.verificationViewAngles, currentState.input.verificationOrientation); - - if (IsNetworked) - { - var message = NetworkManager.ClientBeginSendMessage(); - message.WriteByte((byte)GameModeMessageType.PlayerInput); - message.WriteUInt64(currentState.input.frame); - message.WriteSingle(currentState.input.viewDeltaX); - message.WriteSingle(currentState.input.viewDeltaY); - message.WriteSingle(currentState.input.moveForward); - message.WriteSingle(currentState.input.moveRight); - message.WriteBoolean(currentState.input.attacking); - message.WriteBoolean(currentState.input.jumping); - NetworkManager.ClientEndSendMessage(ref message); - } - - base.OnEndFrame(); - - // Reset anything accumulatable here - currentState.input.viewDeltaX = 0; - currentState.input.viewDeltaY = 0; - } - - public void FlushDemo() - { - if (!IsRecording) - return; - - Stopwatch sw = Stopwatch.StartNew(); - - Span bytes = stackalloc byte[Unsafe.SizeOf()]; - foreach (ref PlayerInputState state in CollectionsMarshal.AsSpan(buffer)) - { - MemoryMarshal.Write(bytes, state); - demoStream.Write(bytes); - } - - sw.Stop(); + //playerActor.UpdateNetworkInput(currentState.input.frame, new Float4(currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight), currentState.input.attacking, currentState.input.jumping); - flushedFrames += buffer.Count; - buffer.Clear(); - - FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); } + //playerActor.UpdateNetworkInput(currentState.input.frame, currentState.input.viewDeltaX, currentState.input.viewDeltaY, currentState.input.moveForward, currentState.input.moveRight, currentState.input.attacking, currentState.input.jumping, currentState.input.verificationPosition, currentState.input.verificationVelocity, currentState.input.verificationViewAngles, currentState.input.verificationOrientation); - public void StopRecording() + if (IsNetworked) { - if (!IsRecording) - return; - - FlushDemo(); - demoStream.Close(); - demoStream = null; - demoFileStream.Close(); - demoFileStream = null; + var message = NetworkManager.ClientBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerInput); + message.WriteUInt64(currentState.input.frame); + message.WriteSingle(currentState.input.viewDeltaX); + message.WriteSingle(currentState.input.viewDeltaY); + message.WriteSingle(currentState.input.moveForward); + message.WriteSingle(currentState.input.moveRight); + message.WriteBoolean(currentState.input.attacking); + message.WriteBoolean(currentState.input.jumping); + NetworkManager.ClientEndSendMessage(ref message); } + + base.OnEndFrame(); + + // Reset anything accumulatable here + currentState.input.viewDeltaX = 0; + currentState.input.viewDeltaY = 0; + } + + public void FlushDemo() + { + if (!IsRecording) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + Span bytes = stackalloc byte[Unsafe.SizeOf()]; + foreach (ref PlayerInputState state in CollectionsMarshal.AsSpan(buffer)) + { + MemoryMarshal.Write(bytes, state); + demoStream.Write(bytes); + } + + sw.Stop(); + + flushedFrames += buffer.Count; + buffer.Clear(); + + FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); + } + + public void StopRecording() + { + if (!IsRecording) + return; + + FlushDemo(); + demoStream.Close(); + demoStream = null; + demoFileStream.Close(); + demoFileStream = null; } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerInputNetwork.cs b/Source/Game/Player/PlayerInputNetwork.cs index b18e5f1..73ff415 100644 --- a/Source/Game/Player/PlayerInputNetwork.cs +++ b/Source/Game/Player/PlayerInputNetwork.cs @@ -1,13 +1,12 @@ -namespace Game -{ - public class PlayerInputNetwork : PlayerInput - { - public override bool Predict => true; +namespace Game; - public override void OnEndFrame() - { - currentState.input.frame = frame; - base.OnEndFrame(); - } +public class PlayerInputNetwork : PlayerInput +{ + public override bool Predict => true; + + public override void OnEndFrame() + { + currentState.input.frame = frame; + base.OnEndFrame(); } } \ No newline at end of file diff --git a/Source/Game/Player/PlayerMovement.cs b/Source/Game/Player/PlayerMovement.cs index f919042..b10a93a 100644 --- a/Source/Game/Player/PlayerMovement.cs +++ b/Source/Game/Player/PlayerMovement.cs @@ -9,361 +9,470 @@ using FlaxEngine.Assertions; using FlaxEngine.Networking; using Console = Game.Console; -namespace Game +namespace Game; + +public class PlayerMovementParameters { - public class PlayerMovementParameters + // FIXME, should be much smaller but needed to avoid issues with box collider edges against brush edges diagonally + public float collisionMargin { get; } = 0.031f * 1.666f * 1.85f; + public float slopeNormal { get; } = 0.7f; + + public float moveSpeed { get; } = 320f; + public Float3 gravity { get; } = new Float3(0, -800.0f, 0f); + /* + // QW + public float friction = 4f; + public float stopspeed = 100f; + public float accelerationGround = 10f; + public float jumpVelocity = 270f; + + public float maxAirSpeed = 320f; + public float maxAirStrafeSpeed = 30f; //Q2+ + public float airAcceleration = 0f; //Q2+ + public float airStopAcceleration = 0f; //Q2+ + public float airStrafeAcceleration = 0f; //CPM? + public float strafeAcceleration = 10f; //QW + public float airControl = 0f; //CPM + public float stepSize = 16f; + public float autoJumpTime = 0.4f; + + public int airStep = 0; + */ + + // GOA + public float friction { get; } = 6f; + public float stopspeed { get; } = 100f; + public float accelerationGround { get; } = 12f; + public float jumpVelocity { get; } = 270f; + public float jumpBoostTime { get; } = 0.5f; + public float jumpBoostVelocity { get; } = 100f; + public int jumpBoostMaxJumps { get; } = 2; + public bool jumpStairBehavior { get; } = true; + public bool jumpAdditive { get; } = true; + + public float maxAirSpeed { get; } = 320f; + public float maxAirStrafeSpeed { get; } = 30f; + public float airAcceleration { get; } = 0.4f; + public float airStopAcceleration { get; } = 2.5f; + public float airStrafeAcceleration { get; } = 0f; + public float strafeAcceleration { get; } = 10f; + public float airControl { get; } = 0f; + public float stepHeight { get; } = 16f; + public float autoJumpTime { get; } = 0.4f; + + public int airStep { get; } = 2; +} + +public struct PlayerMovementState +{ + public Float3 position; + public Float3 currentVelocity; + //public Quaternion orientation; + public Float3 viewAngles; // yaw, pitch, roll + public float lastJumped = -1f; + public float lastLanded = -1f; + public int numJumps; + public bool jumped; + public bool onGround; + + + public PlayerMovementState() { - // FIXME, should be much smaller but needed to avoid issues with box collider edges against brush edges diagonally - public float collisionMargin { get; } = 0.031f * 1.666f * 1.85f; - public float slopeNormal { get; } = 0.7f; + } +} - public float moveSpeed { get; } = 320f; - public Float3 gravity { get; } = new Float3(0, -800.0f, 0f); - /* - // QW - public float friction = 4f; - public float stopspeed = 100f; - public float accelerationGround = 10f; - public float jumpVelocity = 270f; +public struct TraceInfo +{ + public RayCastHit[] hitInfos; + public bool startSolid; - public float maxAirSpeed = 320f; - public float maxAirStrafeSpeed = 30f; //Q2+ - public float airAcceleration = 0f; //Q2+ - public float airStopAcceleration = 0f; //Q2+ - public float airStrafeAcceleration = 0f; //CPM? - public float strafeAcceleration = 10f; //QW - public float airControl = 0f; //CPM - public float stepSize = 16f; - public float autoJumpTime = 0.4f; + // closest hit + public float fraction; + public Float3 endPosition; + public Float3 hitNormal; + public Float3 hitPosition; - public int airStep = 0; - */ + // furthest hit + //public float maxFraction; + //public Float3 maxHitNormal; + //public Float3 maxEndPosition; +} - // GOA - public float friction { get; } = 6f; - public float stopspeed { get; } = 100f; - public float accelerationGround { get; } = 12f; - public float jumpVelocity { get; } = 270f; - public float jumpBoostTime { get; } = 0.5f; - public float jumpBoostVelocity { get; } = 100f; - public int jumpBoostMaxJumps { get; } = 2; - public bool jumpStairBehavior { get; } = true; - public bool jumpAdditive { get; } = true; +public class PlayerMovement : Script +{ + private PlayerMovementParameters movementParameters = new PlayerMovementParameters(); + public PlayerMovementState movementState = new PlayerMovementState(); - public float maxAirSpeed { get; } = 320f; - public float maxAirStrafeSpeed { get; } = 30f; - public float airAcceleration { get; } = 0.4f; - public float airStopAcceleration { get; } = 2.5f; - public float airStrafeAcceleration { get; } = 0f; - public float strafeAcceleration { get; } = 10f; - public float airControl { get; } = 0f; - public float stepHeight { get; } = 16f; - public float autoJumpTime { get; } = 0.4f; + private readonly bool demoDeltasCorrect = true; + private readonly bool demoDeltasVerify = true; - public int airStep { get; } = 2; + + private WorldStateManager worldStateManager; + + private bool predicting = false; + + private readonly List touchingActors = new List(); + //public int currentInputFrame; + //private int currentInputFrame2; + + + public PlayerInput input; + + //private bool physicsInteractions = false; + + public bool OnGround => movementState.onGround; + + //private int lastInputFrame; + + private float simulationTime = 0f; + + + private RigidBody rigidBody; + private Actor cameraHolder; + + private PlayerActor playerActor; + public Actor rootActor; + + public Float3 viewAngles; + private Float3 viewAnglesLastFrame; + + public uint PlayerId = 0; + + [ReadOnly] + public float CurrentVelocity + { + get => movementState.currentVelocity.Length; + set { } } - public struct PlayerMovementState + [ReadOnly] + public float UPS { - public Float3 position; - public Float3 currentVelocity; - //public Quaternion orientation; - public Float3 viewAngles; // yaw, pitch, roll - public float lastJumped = -1f; - public float lastLanded = -1f; - public int numJumps; - public bool jumped; - public bool onGround; - - - public PlayerMovementState() + get { + Float3 horizontalSpeed = movementState.currentVelocity; + horizontalSpeed.Y = 0f; + return horizontalSpeed.Length; } + set { } } - public struct TraceInfo + public override void OnAwake() { - public RayCastHit[] hitInfos; - public bool startSolid; + base.OnAwake(); - // closest hit - public float fraction; - public Float3 endPosition; - public Float3 hitNormal; - public Float3 hitPosition; + Console.Print("player awake, playerid: " + PlayerId); + rootActor = Actor.GetChild("RootActor"); + rigidBody = Actor.As(); + playerActor = Actor.As(); + cameraHolder = rootActor.GetChild("CameraHolder"); - // furthest hit - //public float maxFraction; - //public Float3 maxHitNormal; - //public Float3 maxEndPosition; + // Setup input with no controller + //SetInput(NetworkReplicator.GetObjectOwnerClientId(this.Parent)); + + + //rigidBody.CollisionEnter += OnCollisionEnter; + //rigidBody.TriggerEnter += OnTriggerEnter; + //rigidBody.TriggerExit += OnTriggerExit; } - public class PlayerMovement : Script + public void SetInput(uint playerId) { - private PlayerMovementParameters movementParameters = new PlayerMovementParameters(); - public PlayerMovementState movementState = new PlayerMovementState(); + //if (playerId == 0) + // input = new PlayerInput(); + Assert.IsTrue(playerId != uint.MaxValue); - private readonly bool demoDeltasCorrect = true; - private readonly bool demoDeltasVerify = true; - - - private WorldStateManager worldStateManager; - - private bool predicting = false; - - private readonly List touchingActors = new List(); - //public int currentInputFrame; - //private int currentInputFrame2; - - - public PlayerInput input; - - //private bool physicsInteractions = false; - - public bool OnGround => movementState.onGround; - - //private int lastInputFrame; - - private float simulationTime = 0f; - - - private RigidBody rigidBody; - private Actor cameraHolder; - - private PlayerActor playerActor; - public Actor rootActor; - - public Float3 viewAngles; - private Float3 viewAnglesLastFrame; - - public uint PlayerId = 0; - - [ReadOnly] - public float CurrentVelocity + PlayerId = playerId; + if (PlayerId == NetworkManager.LocalPlayerClientId)//if (NetworkReplicator.GetObjectRole(this.Parent) == NetworkObjectRole.OwnedAuthoritative)// if (playerId == NetworkManager.LocalPlayerClientId) { - get => movementState.currentVelocity.Length; - set { } + Console.Print("local player?: " + playerId.ToString()); + //string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}.gdem"); + //input = new PlayerInputLocal(playerActor, demoPath); // TODO: support recording + input = new PlayerInputLocal(playerActor); + worldStateManager = NetworkManager.serverWorldStateManager; } - - [ReadOnly] - public float UPS + else { - get + Console.Print("network player: " + playerId.ToString()); + input = new PlayerInputNetwork(); + worldStateManager = NetworkManager.clientWorldStateManager; + } + Assert.IsTrue(worldStateManager != null); + } + + public void SetInput(string demoFile) + { + input = new PlayerInputDemo(demoFile); + + /*movementState.position = input.GetCurrentActorState().position; + currentVelocity = input.GetCurrentActorState().velocity; f + SetCameraEulerAngles(input.GetCurrentActorState().viewAngles);*/ + movementState.position = input.GetCurrentInputState().verificationPosition; + Actor.Position = movementState.position; + //rootActor.Orientation = input.GetCurrentInputState().verificationOrientation; + movementState.currentVelocity = input.GetCurrentInputState().verificationVelocity; + SetCameraEulerAngles(input.GetCurrentInputState().verificationViewAngles); + } + + public override void OnEnable() + { + //var playerId = NetworkReplicator.GetObjectOwnerClientId(this.Parent); + //SetInput(playerId); + //Console.Print("hai: " + playerActor.hai); + //SetInput(playerActor.PlayerId); + } + public override void OnDisable() + { + base.OnDisable(); + + if (input != null && input is PlayerInputLocal) // FIXME + (input as PlayerInputLocal).StopRecording(); + } + + private void OnTriggerEnter(PhysicsColliderActor colliderActor) + { + //if (colliderActor.AttachedRigidBody == null) + // return; + touchingActors.Add(colliderActor); + Console.Print("trogger: "); + } + + private void OnTriggerExit(PhysicsColliderActor colliderActor) + { + //if (colliderActor.AttachedRigidBody == null) + // return; + + touchingActors.Remove(colliderActor); + Console.Print("untrogger: "); + } + + private void OnCollisionEnter(Collision collision) + { + //Console.Print("collision: "); + } + + public override void OnDestroy() + { + base.OnDestroy(); + } + + public override void OnStart() + { + ResetRotation(Actor.Orientation.EulerAngles); + } + + public void ResetRotation(Float3 eulerAngles) + { + //viewAngles = eulerAngles; + //viewAnglesLastFrame = eulerAngles; + + SetCameraEulerAngles(new Float3(eulerAngles.Y, eulerAngles.X, eulerAngles.Z)); + viewAnglesLastFrame = viewAngles; + } + + public override void OnUpdate() + { + //input.OnUpdate(); + + + if (input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/) + return; + + input.OnUpdate(); + + /*if (input.frame > 0) + { + PlayerActorState actorState = input.GetCurrentActorState(); + movementState.position = actorState.position; + currentVelocity = actorState.velocity; + viewYaw = actorState.viewYaw; + viewPitch = actorState.viewPitch; + viewRoll = actorState.viewRoll; + }*/ + + viewAngles = viewAnglesLastFrame; + + if (input is not PlayerInputNetwork) + { + + PlayerInputState inputState = input.GetCurrentInputState(); + + //if (inputState.viewDeltaX != 0 || inputState.viewDeltaY != 0) + // inputState = inputState; + + + ApplyInputToCamera(inputState); + + input.RecordCurrentActorState(new PlayerActorState { - Float3 horizontalSpeed = movementState.currentVelocity; - horizontalSpeed.Y = 0f; - return horizontalSpeed.Length; - } - set { } + position = movementState.position, + velocity = movementState.currentVelocity, + orientation = rootActor.Orientation, + viewAngles = viewAngles, + lastJumpTime = movementState.lastJumped, + numJumps = movementState.numJumps, + jumped = movementState.jumped, + //viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z) + }); } - public override void OnAwake() + /*input.RecordCurrentActorState(new PlayerActorState() { - base.OnAwake(); + position = movementState.position, + velocity = currentVelocity, + orientation = rootActor.Orientation, + viewYaw = viewYaw, + viewPitch = viewPitch, + viewRoll = viewRoll + });*/ + //currentInputFrame2++; + } - Console.Print("player awake, playerid: " + PlayerId); - rootActor = Actor.GetChild("RootActor"); - rigidBody = Actor.As(); - playerActor = Actor.As(); - cameraHolder = rootActor.GetChild("CameraHolder"); + public override void OnFixedUpdate() + { + float timeDeltaDiff = Time.DeltaTime - 1.0f / Time.PhysicsFPS; + if (Time.PhysicsFPS > 0 && Math.Abs(timeDeltaDiff) > 0.0001f) + Console.Print("Time.DeltaTime is not stable: " + timeDeltaDiff); - // Setup input with no controller - //SetInput(NetworkReplicator.GetObjectOwnerClientId(this.Parent)); + input.OnFixedUpdate(); + PlayerInputState inputState = input.GetCurrentInputState(); - - //rigidBody.CollisionEnter += OnCollisionEnter; - //rigidBody.TriggerEnter += OnTriggerEnter; - //rigidBody.TriggerExit += OnTriggerExit; - } - - public void SetInput(uint playerId) + if (input is PlayerInputNetwork) { - //if (playerId == 0) - // input = new PlayerInput(); - Assert.IsTrue(playerId != uint.MaxValue); - - PlayerId = playerId; - if (PlayerId == NetworkManager.LocalPlayerClientId)//if (NetworkReplicator.GetObjectRole(this.Parent) == NetworkObjectRole.OwnedAuthoritative)// if (playerId == NetworkManager.LocalPlayerClientId) +#if false + bool canpredict = true; + if (false && input.Predict && GameModeManager.ClientFrame > 0 && GameModeManager.ServerFrame > 0) { - Console.Print("local player?: " + playerId.ToString()); - //string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}.gdem"); - //input = new PlayerInputLocal(playerActor, demoPath); // TODO: support recording - input = new PlayerInputLocal(playerActor); - worldStateManager = NetworkManager.serverWorldStateManager; + ulong maxFrame = /*NetworkManager.IsServer ? GameModeManager.playerLastReceivedFrames[PlayerId] :*/ GameModeManager.ClientFrame; + ulong currentFrame = GameModeManager.ServerFrame; + for (; currentFrame <= maxFrame; currentFrame++) + { + if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) + { + //Console.Print($"not predicting"); + //canpredict = false; + break; + } + } + + ulong lastFrame = currentFrame; + if (input is PlayerInputNetwork) + { + //canpredict = true; + //lastFrame = GameModeManager.ServerFrame+1; + } + + predicting = true; + currentFrame = GameModeManager.ServerFrame; + for (; currentFrame < lastFrame; currentFrame++) + { + if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) + { + Console.Print($"unexpected predict failure: {currentFrame}"); + break; + } + + if (currentFrame == inputState.frame) + jumped = jumped; + + if (currentFrame == GameModeManager.ServerFrame) + { + movementState.position = pastActorState.position; + currentVelocity = pastActorState.velocity; + lastJumped = pastActorState.lastJumpTime; + numJumps = pastActorState.numJumps; + jumped = pastActorState.jumped; + SetCameraEulerAngles(pastActorState.viewAngles, true); + + if (movementState.position.Length < 0.1) + jumped = jumped; + + continue; + //rootActor.Orientation = pastActorState.orientation; + //viewAngles = pastActorState.viewAngles; + //viewAngles = new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z); + } + else + ApplyInputToCamera(pastInputState, true); + //SetCameraEulerAngles(pastActorState.viewAngles, true); + + //if (currentVelocity.Length > 0) + // currentVelocity = currentVelocity; + + //else + // ApplyInputToCamera(pastInputState, true); + SimulatePlayerMovement(pastInputState); + + break; + + /*if ((movementState.position - pastActorState.position).Length > 0.001) + Console.Print($"mispredicted position"); + if ((currentVelocity - pastActorState.velocity).Length > 0.001) + Console.Print($"mispredicted velocity"); + if ((viewAngles - pastActorState.viewAngles).Length > 0.001) + Console.Print($"mispredicted viewangles: {viewAngles - oldAngles}");*/ + + //Console.Print($"predicted: {currentFrame}"); + } + + /*if (input is PlayerInputNetwork) + { + currentFrame = lastFrame - 1; + if (input.GetState(currentFrame, out var lastInputState, out var lastActorState)) + { + for (; currentFrame < GameModeManager.ClientFrame; currentFrame++) + { + ApplyInputToCamera(lastInputState, true); + SimulatePlayerMovement(lastInputState); + } + } + }*/ + + predicting = false; } else { - Console.Print("network player: " + playerId.ToString()); - input = new PlayerInputNetwork(); - worldStateManager = NetworkManager.clientWorldStateManager; + //ApplyInputToCamera(inputState, true); + //SimulatePlayerMovement(inputState); } - Assert.IsTrue(worldStateManager != null); +#endif } - - public void SetInput(string demoFile) + else { - input = new PlayerInputDemo(demoFile); + if (movementState.position.Length < 0.1) + movementState.jumped = movementState.jumped; - /*movementState.position = input.GetCurrentActorState().position; - currentVelocity = input.GetCurrentActorState().velocity; f - SetCameraEulerAngles(input.GetCurrentActorState().viewAngles);*/ - movementState.position = input.GetCurrentInputState().verificationPosition; - Actor.Position = movementState.position; - //rootActor.Orientation = input.GetCurrentInputState().verificationOrientation; - movementState.currentVelocity = input.GetCurrentInputState().verificationVelocity; - SetCameraEulerAngles(input.GetCurrentInputState().verificationViewAngles); - } + //viewAngles = viewAnglesLastFrame; + //ApplyInputToCamera(inputState); - public override void OnEnable() - { - //var playerId = NetworkReplicator.GetObjectOwnerClientId(this.Parent); - //SetInput(playerId); - //Console.Print("hai: " + playerActor.hai); - //SetInput(playerActor.PlayerId); - } - public override void OnDisable() - { - base.OnDisable(); - - if (input != null && input is PlayerInputLocal) // FIXME - (input as PlayerInputLocal).StopRecording(); - } - - private void OnTriggerEnter(PhysicsColliderActor colliderActor) - { - //if (colliderActor.AttachedRigidBody == null) - // return; - touchingActors.Add(colliderActor); - Console.Print("trogger: "); - } - - private void OnTriggerExit(PhysicsColliderActor colliderActor) - { - //if (colliderActor.AttachedRigidBody == null) - // return; - - touchingActors.Remove(colliderActor); - Console.Print("untrogger: "); - } - - private void OnCollisionEnter(Collision collision) - { - //Console.Print("collision: "); - } - - public override void OnDestroy() - { - base.OnDestroy(); - } - - public override void OnStart() - { - ResetRotation(Actor.Orientation.EulerAngles); - } - - public void ResetRotation(Float3 eulerAngles) - { - //viewAngles = eulerAngles; - //viewAnglesLastFrame = eulerAngles; - - SetCameraEulerAngles(new Float3(eulerAngles.Y, eulerAngles.X, eulerAngles.Z)); - viewAnglesLastFrame = viewAngles; - } - - public override void OnUpdate() - { - //input.OnUpdate(); - - - if (input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/) - return; - - input.OnUpdate(); - - /*if (input.frame > 0) + //viewAngles = viewAnglesLastFrame; + bool canpredict = true; + if (true && input.Predict /*&& !NetworkManager.IsDemoPlaying*/ && worldStateManager.ClientFrame > 0 && worldStateManager.ServerFrame > 0) { - PlayerActorState actorState = input.GetCurrentActorState(); - movementState.position = actorState.position; - currentVelocity = actorState.velocity; - viewYaw = actorState.viewYaw; - viewPitch = actorState.viewPitch; - viewRoll = actorState.viewRoll; - }*/ - - viewAngles = viewAnglesLastFrame; - - if (input is not PlayerInputNetwork) - { - - PlayerInputState inputState = input.GetCurrentInputState(); - - //if (inputState.viewDeltaX != 0 || inputState.viewDeltaY != 0) - // inputState = inputState; - - - ApplyInputToCamera(inputState); - - input.RecordCurrentActorState(new PlayerActorState + ulong currentFrame = worldStateManager.ServerFrame; + for (; currentFrame < worldStateManager.ClientFrame; currentFrame++) { - position = movementState.position, - velocity = movementState.currentVelocity, - orientation = rootActor.Orientation, - viewAngles = viewAngles, - lastJumpTime = movementState.lastJumped, - numJumps = movementState.numJumps, - jumped = movementState.jumped, - //viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z) - }); - } + if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) + { + //Console.Print($"not predicting"); + canpredict = false; + break; + } + } - /*input.RecordCurrentActorState(new PlayerActorState() - { - position = movementState.position, - velocity = currentVelocity, - orientation = rootActor.Orientation, - viewYaw = viewYaw, - viewPitch = viewPitch, - viewRoll = viewRoll - });*/ - //currentInputFrame2++; - } + ulong lastFrame = currentFrame; - public override void OnFixedUpdate() - { - float timeDeltaDiff = Time.DeltaTime - 1.0f / Time.PhysicsFPS; - if (Time.PhysicsFPS > 0 && Math.Abs(timeDeltaDiff) > 0.0001f) - Console.Print("Time.DeltaTime is not stable: " + timeDeltaDiff); - input.OnFixedUpdate(); - PlayerInputState inputState = input.GetCurrentInputState(); - - if (input is PlayerInputNetwork) - { -#if false - bool canpredict = true; - if (false && input.Predict && GameModeManager.ClientFrame > 0 && GameModeManager.ServerFrame > 0) + if (canpredict) { - ulong maxFrame = /*NetworkManager.IsServer ? GameModeManager.playerLastReceivedFrames[PlayerId] :*/ GameModeManager.ClientFrame; - ulong currentFrame = GameModeManager.ServerFrame; - for (; currentFrame <= maxFrame; currentFrame++) - { - if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) - { - //Console.Print($"not predicting"); - //canpredict = false; - break; - } - } + var oldAngles = viewAngles; + var oldPos = movementState.position; + var oldVel = movementState.currentVelocity; - ulong lastFrame = currentFrame; - if (input is PlayerInputNetwork) - { - //canpredict = true; - //lastFrame = GameModeManager.ServerFrame+1; - } + predicting = true; - currentFrame = GameModeManager.ServerFrame; + currentFrame = worldStateManager.ServerFrame; for (; currentFrame < lastFrame; currentFrame++) { if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) @@ -373,19 +482,25 @@ namespace Game } if (currentFrame == inputState.frame) - jumped = jumped; + movementState.jumped = movementState.jumped; - if (currentFrame == GameModeManager.ServerFrame) + if (currentFrame == worldStateManager.ServerFrame) { movementState.position = pastActorState.position; - currentVelocity = pastActorState.velocity; - lastJumped = pastActorState.lastJumpTime; - numJumps = pastActorState.numJumps; - jumped = pastActorState.jumped; + movementState.currentVelocity = pastActorState.velocity; + movementState.lastJumped = pastActorState.lastJumpTime; + movementState.numJumps = pastActorState.numJumps; + movementState.jumped = pastActorState.jumped; + Actor.Position = movementState.position; SetCameraEulerAngles(pastActorState.viewAngles, true); + //cameraHolder.Orientation = Quaternion.Euler(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z); + //ApplyInputToCamera(pastInputState, true); + + //if (pastActorState.viewAngles != new Float3(90f, 0f, 0f)) + // Console.Print($"moved server frame: {currentFrame}, {pastActorState.viewAngles}"); if (movementState.position.Length < 0.1) - jumped = jumped; + movementState.jumped = movementState.jumped; continue; //rootActor.Orientation = pastActorState.orientation; @@ -403,8 +518,6 @@ namespace Game // ApplyInputToCamera(pastInputState, true); SimulatePlayerMovement(pastInputState); - break; - /*if ((movementState.position - pastActorState.position).Length > 0.001) Console.Print($"mispredicted position"); if ((currentVelocity - pastActorState.velocity).Length > 0.001) @@ -415,1054 +528,940 @@ namespace Game //Console.Print($"predicted: {currentFrame}"); } - /*if (input is PlayerInputNetwork) - { - currentFrame = lastFrame - 1; - if (input.GetState(currentFrame, out var lastInputState, out var lastActorState)) - { - for (; currentFrame < GameModeManager.ClientFrame; currentFrame++) - { - ApplyInputToCamera(lastInputState, true); - SimulatePlayerMovement(lastInputState); - } - } - }*/ - predicting = false; + + var posDelta = (movementState.position - oldPos); + var velDelta = (movementState.currentVelocity - oldVel); + if (posDelta.Length > 0.001) + Console.Print($"mispredicted final position"); + if (velDelta.Length > 0.001) + Console.Print($"mispredicted final velocity"); + + + //if (input is not PlayerInputNetwork) + { + /*if ((movementState.position - oldPos).Length > 0.001) + Console.Print($"mispredicted final position"); + if ((currentVelocity - oldVel).Length > 0.001) + Console.Print($"mispredicted final velocity");*/ + + ApplyInputToCamera(inputState, true); + + // Ensure orientation is always up-to-date after predicting + //rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0); + cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z); + + SimulatePlayerMovement(inputState); + } + + var viewDelta = (viewAngles - oldAngles); + if (viewDelta.Length > 0.001) + Console.Print($"mispredicted final viewangles: {viewAngles} <- {oldAngles}"); + + //if (viewAngles != new Float3(90f, 0f, 0f)) + // Console.Print($"moved client frame: {GameModeManager.ClientFrame}, {viewAngles}"); + + //Console.Print($"current: {inputState.frame}"); + //if (GameModeManager.ClientFrame - GameModeManager.ServerFrame > 0) + // Console.Print($"current diff: {GameModeManager.ClientFrame - GameModeManager.ServerFrame}"); } - else - { - //ApplyInputToCamera(inputState, true); - //SimulatePlayerMovement(inputState); - } -#endif } else + canpredict = false; + + if (!canpredict) { - if (movementState.position.Length < 0.1) - movementState.jumped = movementState.jumped; - - //viewAngles = viewAnglesLastFrame; - //ApplyInputToCamera(inputState); - - //viewAngles = viewAnglesLastFrame; - bool canpredict = true; - if (true && input.Predict /*&& !NetworkManager.IsDemoPlaying*/ && worldStateManager.ClientFrame > 0 && worldStateManager.ServerFrame > 0) - { - ulong currentFrame = worldStateManager.ServerFrame; - for (; currentFrame < worldStateManager.ClientFrame; currentFrame++) - { - if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) - { - //Console.Print($"not predicting"); - canpredict = false; - break; - } - } - - ulong lastFrame = currentFrame; - - - if (canpredict) - { - var oldAngles = viewAngles; - var oldPos = movementState.position; - var oldVel = movementState.currentVelocity; - - - - predicting = true; - currentFrame = worldStateManager.ServerFrame; - for (; currentFrame < lastFrame; currentFrame++) - { - if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState)) - { - Console.Print($"unexpected predict failure: {currentFrame}"); - break; - } - - if (currentFrame == inputState.frame) - movementState.jumped = movementState.jumped; - - if (currentFrame == worldStateManager.ServerFrame) - { - movementState.position = pastActorState.position; - movementState.currentVelocity = pastActorState.velocity; - movementState.lastJumped = pastActorState.lastJumpTime; - movementState.numJumps = pastActorState.numJumps; - movementState.jumped = pastActorState.jumped; - Actor.Position = movementState.position; - SetCameraEulerAngles(pastActorState.viewAngles, true); - //cameraHolder.Orientation = Quaternion.Euler(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z); - //ApplyInputToCamera(pastInputState, true); - - //if (pastActorState.viewAngles != new Float3(90f, 0f, 0f)) - // Console.Print($"moved server frame: {currentFrame}, {pastActorState.viewAngles}"); - - if (movementState.position.Length < 0.1) - movementState.jumped = movementState.jumped; - - continue; - //rootActor.Orientation = pastActorState.orientation; - //viewAngles = pastActorState.viewAngles; - //viewAngles = new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z); - } - else - ApplyInputToCamera(pastInputState, true); - //SetCameraEulerAngles(pastActorState.viewAngles, true); - - //if (currentVelocity.Length > 0) - // currentVelocity = currentVelocity; - - //else - // ApplyInputToCamera(pastInputState, true); - SimulatePlayerMovement(pastInputState); - - /*if ((movementState.position - pastActorState.position).Length > 0.001) - Console.Print($"mispredicted position"); - if ((currentVelocity - pastActorState.velocity).Length > 0.001) - Console.Print($"mispredicted velocity"); - if ((viewAngles - pastActorState.viewAngles).Length > 0.001) - Console.Print($"mispredicted viewangles: {viewAngles - oldAngles}");*/ - - //Console.Print($"predicted: {currentFrame}"); - } - - predicting = false; - - var posDelta = (movementState.position - oldPos); - var velDelta = (movementState.currentVelocity - oldVel); - if (posDelta.Length > 0.001) - Console.Print($"mispredicted final position"); - if (velDelta.Length > 0.001) - Console.Print($"mispredicted final velocity"); - - - //if (input is not PlayerInputNetwork) - { - /*if ((movementState.position - oldPos).Length > 0.001) - Console.Print($"mispredicted final position"); - if ((currentVelocity - oldVel).Length > 0.001) - Console.Print($"mispredicted final velocity");*/ - - ApplyInputToCamera(inputState, true); - - // Ensure orientation is always up-to-date after predicting - //rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0); - cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z); - - SimulatePlayerMovement(inputState); - } - - var viewDelta = (viewAngles - oldAngles); - if (viewDelta.Length > 0.001) - Console.Print($"mispredicted final viewangles: {viewAngles} <- {oldAngles}"); - - //if (viewAngles != new Float3(90f, 0f, 0f)) - // Console.Print($"moved client frame: {GameModeManager.ClientFrame}, {viewAngles}"); - - //Console.Print($"current: {inputState.frame}"); - //if (GameModeManager.ClientFrame - GameModeManager.ServerFrame > 0) - // Console.Print($"current diff: {GameModeManager.ClientFrame - GameModeManager.ServerFrame}"); - } - } - else - canpredict = false; - - if (!canpredict) - { - SetCameraEulerAngles(viewAnglesLastFrame, true); - ApplyInputToCamera(inputState, true); - SimulatePlayerMovement(inputState); - } - - if (movementState.position.Length < 0.1) - movementState.jumped = movementState.jumped; - - //if (currentVelocity.Length > 0) - // Console.Print($"velocity at frame {GameModeManager.ClientFrame}"); - - /*if (input.GetState(GameModeManager.ClientFrame - 1, out var pastInputState2, out var pastActorState2)) - { - movementState.position = pastActorState2.position; - currentVelocity = pastActorState2.velocity; - rootActor.Orientation = pastActorState2.orientation; - viewAngles = new Float3(pastActorState2.viewAngles.Y, pastActorState2.viewAngles.X, pastActorState2.viewAngles.Z); - //viewAngles = viewAnglesLastFrame; - } - else - Console.Print($"poop");*/ - - //viewAngles = oldAngles;' - //viewAngles = viewAnglesLastFrame; - //ApplyInputToCamera(inputState, true); - //SetCameraEulerAngles(new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z), true); - //SetCameraEulerAngles(new Float3(viewAnglesLastFrame.Y, viewAnglesLastFrame.X, viewAnglesLastFrame.Z), true); - //SimulatePlayerMovement(inputState); - - input.RecordCurrentActorState(new PlayerActorState - { - position = movementState.position, - velocity = movementState.currentVelocity, - orientation = rootActor.Orientation, - viewAngles = viewAngles, - lastJumpTime = movementState.lastJumped, - numJumps = movementState.numJumps, - jumped = movementState.jumped, - //viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z) - }); - - //Console.Print($"recording frame {input.frame}, client: {GameModeManager.ClientFrame}, server: {GameModeManager.ServerFrame}"); - input.OnEndFrame(); + SetCameraEulerAngles(viewAnglesLastFrame, true); + ApplyInputToCamera(inputState, true); + SimulatePlayerMovement(inputState); } if (movementState.position.Length < 0.1) movementState.jumped = movementState.jumped; - if (input is PlayerInputNetwork) - movementState.jumped = movementState.jumped; + //if (currentVelocity.Length > 0) + // Console.Print($"velocity at frame {GameModeManager.ClientFrame}"); - - - - //lastInputFrame = currentInputFrame; - //currentInputFrame++; - - viewAnglesLastFrame = viewAngles; - - /*if (input.GetState(GameModeManager.ServerFrame, out var pastInputState2, out var pastActorState2)) + /*if (input.GetState(GameModeManager.ClientFrame - 1, out var pastInputState2, out var pastActorState2)) { movementState.position = pastActorState2.position; currentVelocity = pastActorState2.velocity; - SetCameraEulerAngles(pastActorState2.viewAngles, true); + rootActor.Orientation = pastActorState2.orientation; + viewAngles = new Float3(pastActorState2.viewAngles.Y, pastActorState2.viewAngles.X, pastActorState2.viewAngles.Z); + //viewAngles = viewAnglesLastFrame; + } + else + Console.Print($"poop");*/ + + //viewAngles = oldAngles;' + //viewAngles = viewAnglesLastFrame; + //ApplyInputToCamera(inputState, true); + //SetCameraEulerAngles(new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z), true); + //SetCameraEulerAngles(new Float3(viewAnglesLastFrame.Y, viewAnglesLastFrame.X, viewAnglesLastFrame.Z), true); + //SimulatePlayerMovement(inputState); + + input.RecordCurrentActorState(new PlayerActorState + { + position = movementState.position, + velocity = movementState.currentVelocity, + orientation = rootActor.Orientation, + viewAngles = viewAngles, + lastJumpTime = movementState.lastJumped, + numJumps = movementState.numJumps, + jumped = movementState.jumped, + //viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z) + }); + + //Console.Print($"recording frame {input.frame}, client: {GameModeManager.ClientFrame}, server: {GameModeManager.ServerFrame}"); + input.OnEndFrame(); + } + + if (movementState.position.Length < 0.1) + movementState.jumped = movementState.jumped; + + if (input is PlayerInputNetwork) + movementState.jumped = movementState.jumped; + + + + + //lastInputFrame = currentInputFrame; + //currentInputFrame++; + + viewAnglesLastFrame = viewAngles; + + /*if (input.GetState(GameModeManager.ServerFrame, out var pastInputState2, out var pastActorState2)) + { + movementState.position = pastActorState2.position; + currentVelocity = pastActorState2.velocity; + SetCameraEulerAngles(pastActorState2.viewAngles, true); + }*/ + } + + public void ApplyInputToCamera(PlayerInputState inputState, bool wrapAround = true) + { + if (inputState.viewDeltaX == 0.0f && inputState.viewDeltaY == 0.0f) + return; + + float viewPitch = Mathf.Clamp(viewAngles.Y + inputState.viewDeltaY, -90.0f, 90.0f); + float viewYaw = viewAngles.X + inputState.viewDeltaX; + SetCameraEulerAngles(new Float3(viewYaw, viewPitch, viewAngles.Z), wrapAround); + } + + public void SetCameraEulerAngles(Float3 angles, bool wrapAround = true) + { + if (viewAngles == angles) + return; + + // Very slight drift + if (wrapAround) + { + angles.X = Mathf.Mod(angles.X, 360.0f); + angles.Y = Mathf.Mod(angles.Y, 360.0f); + angles.Z = Mathf.Mod(angles.Z, 360.0f); + } + + // Root orientation must be set first + rootActor.Orientation = Quaternion.Euler(0, angles.X, 0); + if (!predicting) + { + cameraHolder.Orientation = Quaternion.Euler(angles.Y, angles.X, angles.Z); + } + //Console.Print(angles.X.ToString()); + + viewAngles = angles;//new Float3(angles.Y, angles.X, angles.Z); + //viewAnglesLastFrame = angles; + } + + private static bool SweepPlayerCollider(PlayerActor actor, Float3 start, Vector3 end, out RayCastHit[] hits) + { + Vector3 delta = end - start; + float distance = delta.Length; + Vector3 direction = delta.Normalized; + + if (distance < 0.00000001f) + { + hits = new RayCastHit[0];//Array.Empty(); + return false; + } + + bool collided = false; + CapsuleCollider capsuleCollider = actor.capsuleCollider; + BoxCollider boxCollider = actor.boxCollider; + MeshCollider meshCollider = actor.meshCollider; + if (capsuleCollider && capsuleCollider.IsActive) + collided = Physics.CapsuleCastAll(start, + capsuleCollider.Radius, capsuleCollider.Height, + direction, out hits, capsuleCollider.Orientation, distance, + uint.MaxValue, + false); + else if (meshCollider && meshCollider.IsActive) + collided = Physics.ConvexCastAll(start, + meshCollider.CollisionData, meshCollider.Scale, + direction, out hits, meshCollider.Orientation, distance, + uint.MaxValue, + false); + else if (boxCollider && boxCollider.IsActive) + collided = Physics.BoxCastAll(start, + boxCollider.OrientedBox.Extents, + direction, out hits, boxCollider.Orientation, distance, + uint.MaxValue, + false); + else + throw new Exception("Player does not have a collider"); + + + + return collided; + } + + private bool SweepPlayerCollider(Float3 position, out PhysicsColliderActor[] colliders) + { + bool collided = false; + CapsuleCollider capsuleCollider = Actor.GetChild(); + BoxCollider boxCollider = Actor.GetChild(); + MeshCollider meshCollider = Actor.GetChild(); + PhysicsColliderActor colliderActor = null; + if (capsuleCollider && capsuleCollider.IsActive) + { + colliderActor = capsuleCollider; + collided = Physics.OverlapCapsule(position, + capsuleCollider.Radius, capsuleCollider.Height, + out colliders, capsuleCollider.Orientation, + uint.MaxValue, + false); + } + else if (meshCollider && meshCollider.IsActive) + { + colliderActor = meshCollider; + collided = Physics.OverlapConvex(position, + meshCollider.CollisionData, meshCollider.Scale, + out colliders, meshCollider.Orientation, + uint.MaxValue, + false); + } + else if (boxCollider && boxCollider.IsActive) + { + colliderActor = boxCollider; + collided = Physics.OverlapBox(position, + boxCollider.OrientedBox.Extents, + out colliders, boxCollider.Orientation, + uint.MaxValue, + false); + } + else + { + throw new Exception("Player does not have a collider"); + } + + if (collided) + { + var collidersFiltered = new List(); + foreach (PhysicsColliderActor collider in colliders) + { + if (collider.Parent == Actor) + continue; + + collidersFiltered.Add(collider); + } + + colliders = collidersFiltered.ToArray(); + if (colliders.Length == 0) + collided = false; + } + + return collided; + } + + /// + /// Sweeps the player rigidbody in world and returns geometry which was hit during the trace. + /// + /// Player actor + /// Start position + /// End position + /// + private static TraceInfo TracePlayer(PlayerActor actor, Vector3 start, Vector3 end) + { + TraceInfo traceInfo = new TraceInfo(); + + Vector3 delta = end - start; + float maxDistance = delta.Length; + Vector3 direction = delta.Normalized; + + bool collided = SweepPlayerCollider(actor, start, end, out traceInfo.hitInfos); + if (collided) + { + var hitInfosFiltered = new List(); + RayCastHit closest = new RayCastHit(); + closest.Distance = float.MaxValue; + foreach (RayCastHit hitInfo in traceInfo.hitInfos) + { + //if (hitInfo.Collider == colliderActor) + // continue; + if (hitInfo.Collider.Parent == actor) + continue; + + hitInfosFiltered.Add(hitInfo); + + if (hitInfo.Distance < closest.Distance && hitInfo.Distance != 0.0f) + closest = hitInfo; + } + + if (hitInfosFiltered.Count == 0 /*|| closest.Distance == float.MaxValue*/) + { + collided = false; // self-collision? + } + /*else if (closest.Distance == float.MaxValue) + { + bool startSolid = SweepPlayerCollider(start, out PhysicsColliderActor[] colliders); + if (startSolid) + { + traceInfo.hitInfos = hitInfosFiltered.ToArray(); + + traceInfo.fraction = 0f; + traceInfo.hitNormal = Float3.Zero; + traceInfo.hitPosition = start; + traceInfo.endPosition = start; + traceInfo.startSolid = true; + + if (delta.Y >= 0f) + Console.Print("ovr: " + colliders[0].Parent.Name); + } + else + collided = false; + }*/ + else //if (closest.Distance > 0f) + { + if (closest.Distance == float.MaxValue) + foreach (RayCastHit hitInfo in hitInfosFiltered) + if (hitInfo.Distance < closest.Distance) + closest = hitInfo; + + traceInfo.hitInfos = hitInfosFiltered.ToArray(); + + traceInfo.fraction = closest.Distance / maxDistance; + traceInfo.hitNormal = closest.Normal; + traceInfo.hitPosition = closest.Point; + traceInfo.endPosition = start + delta * traceInfo.fraction; + + if (traceInfo.fraction == 0f && maxDistance > 0f) + traceInfo.startSolid = true; + + //if (delta.Y >= 0f) + // Console.Print("col: " + closest.Collider.Parent.Name + ", " + closest.Distance.ToString("G9")); + } + /*else + { + traceInfo.startSolid = true; + traceInfo.fraction = 0f; }*/ } - public void ApplyInputToCamera(PlayerInputState inputState, bool wrapAround = true) + if (!collided) { - if (inputState.viewDeltaX == 0.0f && inputState.viewDeltaY == 0.0f) - return; - - float viewPitch = Mathf.Clamp(viewAngles.Y + inputState.viewDeltaY, -90.0f, 90.0f); - float viewYaw = viewAngles.X + inputState.viewDeltaX; - SetCameraEulerAngles(new Float3(viewYaw, viewPitch, viewAngles.Z), wrapAround); + traceInfo.hitInfos = new RayCastHit[0]; + traceInfo.fraction = 1f; + traceInfo.endPosition = end; } - public void SetCameraEulerAngles(Float3 angles, bool wrapAround = true) - { - if (viewAngles == angles) - return; - - // Very slight drift - if (wrapAround) - { - angles.X = Mathf.Mod(angles.X, 360.0f); - angles.Y = Mathf.Mod(angles.Y, 360.0f); - angles.Z = Mathf.Mod(angles.Z, 360.0f); - } - - // Root orientation must be set first - rootActor.Orientation = Quaternion.Euler(0, angles.X, 0); - if (!predicting) - { - cameraHolder.Orientation = Quaternion.Euler(angles.Y, angles.X, angles.Z); - } - //Console.Print(angles.X.ToString()); - - viewAngles = angles;//new Float3(angles.Y, angles.X, angles.Z); - //viewAnglesLastFrame = angles; - } - - private static bool SweepPlayerCollider(PlayerActor actor, Float3 start, Vector3 end, out RayCastHit[] hits) - { - Vector3 delta = end - start; - float distance = delta.Length; - Vector3 direction = delta.Normalized; - - if (distance < 0.00000001f) - { - hits = new RayCastHit[0];//Array.Empty(); - return false; - } - - bool collided = false; - CapsuleCollider capsuleCollider = actor.capsuleCollider; - BoxCollider boxCollider = actor.boxCollider; - MeshCollider meshCollider = actor.meshCollider; - if (capsuleCollider && capsuleCollider.IsActive) - collided = Physics.CapsuleCastAll(start, - capsuleCollider.Radius, capsuleCollider.Height, - direction, out hits, capsuleCollider.Orientation, distance, - uint.MaxValue, - false); - else if (meshCollider && meshCollider.IsActive) - collided = Physics.ConvexCastAll(start, - meshCollider.CollisionData, meshCollider.Scale, - direction, out hits, meshCollider.Orientation, distance, - uint.MaxValue, - false); - else if (boxCollider && boxCollider.IsActive) - collided = Physics.BoxCastAll(start, - boxCollider.OrientedBox.Extents, - direction, out hits, boxCollider.Orientation, distance, - uint.MaxValue, - false); - else - throw new Exception("Player does not have a collider"); - - - - return collided; - } - - private bool SweepPlayerCollider(Float3 position, out PhysicsColliderActor[] colliders) - { - bool collided = false; - CapsuleCollider capsuleCollider = Actor.GetChild(); - BoxCollider boxCollider = Actor.GetChild(); - MeshCollider meshCollider = Actor.GetChild(); - PhysicsColliderActor colliderActor = null; - if (capsuleCollider && capsuleCollider.IsActive) - { - colliderActor = capsuleCollider; - collided = Physics.OverlapCapsule(position, - capsuleCollider.Radius, capsuleCollider.Height, - out colliders, capsuleCollider.Orientation, - uint.MaxValue, - false); - } - else if (meshCollider && meshCollider.IsActive) - { - colliderActor = meshCollider; - collided = Physics.OverlapConvex(position, - meshCollider.CollisionData, meshCollider.Scale, - out colliders, meshCollider.Orientation, - uint.MaxValue, - false); - } - else if (boxCollider && boxCollider.IsActive) - { - colliderActor = boxCollider; - collided = Physics.OverlapBox(position, - boxCollider.OrientedBox.Extents, - out colliders, boxCollider.Orientation, - uint.MaxValue, - false); - } - else - { - throw new Exception("Player does not have a collider"); - } - - if (collided) - { - var collidersFiltered = new List(); - foreach (PhysicsColliderActor collider in colliders) - { - if (collider.Parent == Actor) - continue; - - collidersFiltered.Add(collider); - } - - colliders = collidersFiltered.ToArray(); - if (colliders.Length == 0) - collided = false; - } - - return collided; - } - - /// - /// Sweeps the player rigidbody in world and returns geometry which was hit during the trace. - /// - /// Player actor - /// Start position - /// End position - /// - private static TraceInfo TracePlayer(PlayerActor actor, Vector3 start, Vector3 end) - { - TraceInfo traceInfo = new TraceInfo(); - - Vector3 delta = end - start; - float maxDistance = delta.Length; - Vector3 direction = delta.Normalized; - - bool collided = SweepPlayerCollider(actor, start, end, out traceInfo.hitInfos); - if (collided) - { - var hitInfosFiltered = new List(); - RayCastHit closest = new RayCastHit(); - closest.Distance = float.MaxValue; - foreach (RayCastHit hitInfo in traceInfo.hitInfos) - { - //if (hitInfo.Collider == colliderActor) - // continue; - if (hitInfo.Collider.Parent == actor) - continue; - - hitInfosFiltered.Add(hitInfo); - - if (hitInfo.Distance < closest.Distance && hitInfo.Distance != 0.0f) - closest = hitInfo; - } - - if (hitInfosFiltered.Count == 0 /*|| closest.Distance == float.MaxValue*/) - { - collided = false; // self-collision? - } - /*else if (closest.Distance == float.MaxValue) - { - bool startSolid = SweepPlayerCollider(start, out PhysicsColliderActor[] colliders); - if (startSolid) - { - traceInfo.hitInfos = hitInfosFiltered.ToArray(); - - traceInfo.fraction = 0f; - traceInfo.hitNormal = Float3.Zero; - traceInfo.hitPosition = start; - traceInfo.endPosition = start; - traceInfo.startSolid = true; - - if (delta.Y >= 0f) - Console.Print("ovr: " + colliders[0].Parent.Name); - } - else - collided = false; - }*/ - else //if (closest.Distance > 0f) - { - if (closest.Distance == float.MaxValue) - foreach (RayCastHit hitInfo in hitInfosFiltered) - if (hitInfo.Distance < closest.Distance) - closest = hitInfo; - - traceInfo.hitInfos = hitInfosFiltered.ToArray(); - - traceInfo.fraction = closest.Distance / maxDistance; - traceInfo.hitNormal = closest.Normal; - traceInfo.hitPosition = closest.Point; - traceInfo.endPosition = start + delta * traceInfo.fraction; - - if (traceInfo.fraction == 0f && maxDistance > 0f) - traceInfo.startSolid = true; - - //if (delta.Y >= 0f) - // Console.Print("col: " + closest.Collider.Parent.Name + ", " + closest.Distance.ToString("G9")); - } - /*else - { - traceInfo.startSolid = true; - traceInfo.fraction = 0f; - }*/ - } - - if (!collided) - { - traceInfo.hitInfos = new RayCastHit[0]; - traceInfo.fraction = 1f; - traceInfo.endPosition = end; - } - - return traceInfo; - } + return traceInfo; + } #if FLAX_EDITOR - public override void OnDebugDraw() + public override void OnDebugDraw() + { + base.OnDebugDraw(); + + CapsuleCollider capsuleCollider = Actor.GetChild(); + BoxCollider boxCollider = Actor.GetChild(); + MeshCollider meshCollider = Actor.GetChild(); + if (capsuleCollider && capsuleCollider.IsActive) { - base.OnDebugDraw(); - - CapsuleCollider capsuleCollider = Actor.GetChild(); - BoxCollider boxCollider = Actor.GetChild(); - MeshCollider meshCollider = Actor.GetChild(); - if (capsuleCollider && capsuleCollider.IsActive) - { - Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); - DebugDraw.DrawWireTube(capsuleCollider.Position, rotation, capsuleCollider.Radius, - capsuleCollider.Height, Color.GreenYellow * 0.8f); - } - else if (meshCollider && meshCollider.IsActive) - { - //Quaternion rotation = meshCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); - DebugDraw.DrawWireCylinder(meshCollider.Position, meshCollider.Orientation, capsuleCollider.Radius, - capsuleCollider.Height + capsuleCollider.Radius * 2, Color.GreenYellow * 0.8f); - //DebugDraw.DrawWireTube(meshCollider.Position, rotation, meshCollider.Radius, meshCollider.Height, Color.GreenYellow * 0.8f); - } - else if (boxCollider && boxCollider.IsActive) - { - var clientBbox = boxCollider.OrientedBox.GetBoundingBox(); - - if (false) - { - if (worldStateManager.ServerFrame > 0 && worldStateManager.ClientFrame > 0) - for (ulong frame = worldStateManager.ServerFrame; frame < worldStateManager.ClientFrame; frame++) - { - if (!input.GetState(frame, out var pastInputState, out var pastActorState)) - continue; - - var bbox = clientBbox; - bbox.Center = pastActorState.position; - - Float4 color1 = new Float4(Color.Red.R, Color.Red.G, Color.Red.B, Color.Red.A); - Float4 color2 = new Float4(Color.Blue.R, Color.Blue.G, Color.Blue.B, Color.Blue.A); - Float4 color3 = Float4.Lerp(color1, color2, (float)(frame - worldStateManager.ServerFrame) / (float)(worldStateManager.ClientFrame - worldStateManager.ServerFrame)); - Color color = new Color(color3.X, color3.Y, color3.Z, color3.W); - DebugDraw.DrawBox(bbox, color * 1f); - } - } - else - { - var serverBbox = boxCollider.OrientedBox.GetBoundingBox(); - if (input.GetState(worldStateManager.ServerFrame, out var serverInputState, out var serverActorState)) - serverBbox.Center = serverActorState.position; - - if (serverBbox.Center == clientBbox.Center) - DebugDraw.DrawBox(clientBbox, Color.Magenta * 0.6f); - else - { - DebugDraw.DrawBox(serverBbox, Color.Red * 0.6f); - DebugDraw.DrawBox(clientBbox, Color.Blue * 0.6f); - } - } - } + Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); + DebugDraw.DrawWireTube(capsuleCollider.Position, rotation, capsuleCollider.Radius, + capsuleCollider.Height, Color.GreenYellow * 0.8f); } -#endif - - private static SlideMoveHit StepSlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity, - bool onGround) + else if (meshCollider && meshCollider.IsActive) { - if (velocity.IsZero) - return SlideMoveHit.Nothing; - - Vector3 gravityDirection = movementParameters.gravity.Normalized; - - Vector3 originalPosition = position; - Vector3 originalVelocity = velocity; - - SlideMoveHit slideMoveHit = SlideMove(actor, movementParameters, ref position, ref velocity); - if (slideMoveHit == SlideMoveHit.Nothing) - // TODO: step down here - return slideMoveHit; - - // hit something, try to step up - float effectiveStepHeight = movementParameters.stepHeight; - if (!onGround) - { - // TODO: implement clipping here - /*if (pmove.jump_time > 0 && pmove.jump_time <= movevars.cliptime) - { - float zvel = pmove.velocity[2]; - VectorCopy(originalvel, pmove.velocity); - pmove.velocity[2] = min(pmove.velocity[2], zvel); // nullifies vertical clipping - }*/ - - if (!slideMoveHit.HasFlag(SlideMoveHit.Step)) - return slideMoveHit; - - if (movementParameters.airStep < 2) - { - //effectiveStepHeight = ? - } - } - - - Vector3 stepDelta = -gravityDirection * effectiveStepHeight; - - Vector3 slidePosition = position; - Vector3 slideVelocity = velocity; - position = originalPosition; - velocity = originalVelocity; - - // step up - Vector3 stepUp = position + stepDelta; - TraceInfo traceUp = TracePlayer(actor, position, stepUp); - if (traceUp.fraction > 0f) - position = traceUp.endPosition; - - // try moving from step up position - SlideMoveHit slideMoveStepHit = SlideMove(actor, movementParameters, ref position, ref velocity); - - // step down - Vector3 stepDown = position - stepDelta; - TraceInfo traceDown = TracePlayer(actor, position, stepDown); - if (traceDown.fraction < 1f && -Float3.Dot(gravityDirection, traceDown.hitNormal) < movementParameters.slopeNormal) - { - // can't step down, slide move like normally - Console.Print("no stepping 1, frac: " + traceDown.fraction + ", dot: " + - -Float3.Dot(gravityDirection, traceDown.hitNormal) + - ", norm: " + traceDown.hitNormal); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - - if (traceDown.fraction > 0f) - position = traceDown.endPosition; - - // add some margin from the ground in order to avoid getting stuck after stepping up - if (traceDown.fraction < 1f) - position.Y += movementParameters.collisionMargin; - - // ?? - float d1 = -Float3.Dot(gravityDirection, position); - float d2 = -Float3.Dot(gravityDirection, originalPosition); - if (d1 < d2) - { - //Console.Print("no stepping 2, " + d1 + " < " + d2); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - - Vector3 slidePosition2 = slidePosition; //down - Vector3 stepPosition2 = position; //up - - // FIXME, negate movementParameters.gravity - slidePosition2.Y = 0f; - stepPosition2.Y = 0f; - - // take the slide movement results if furthest away from original position - //if ((stepPosition2 - originalPosition).Length < (slidePosition2 - originalPosition).Length) - if ((slidePosition2 - originalPosition).Length >= (stepPosition2 - originalPosition).Length) - { - //Console.Print("no stepping 3"); - position = slidePosition; - velocity = slideVelocity; - return slideMoveHit; - } - - //return slideMoveStepHit; - return slideMoveHit; + //Quaternion rotation = meshCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); + DebugDraw.DrawWireCylinder(meshCollider.Position, meshCollider.Orientation, capsuleCollider.Radius, + capsuleCollider.Height + capsuleCollider.Radius * 2, Color.GreenYellow * 0.8f); + //DebugDraw.DrawWireTube(meshCollider.Position, rotation, meshCollider.Radius, meshCollider.Height, Color.GreenYellow * 0.8f); } - - private static SlideMoveHit SlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity) + else if (boxCollider && boxCollider.IsActive) { - if (velocity.IsZero) - return SlideMoveHit.Nothing; + var clientBbox = boxCollider.OrientedBox.GetBoundingBox(); - Vector3 originalPosition = position; - Vector3 originalVelocity = velocity; - SlideMoveHit slideMoveHit = SlideMoveHit.Nothing; - - float timeleft = Time.DeltaTime; - - var hitNormals = new List(); - - for (int bump = 0; bump < 4; bump++) + if (false) { - Vector3 startPos = position; - Vector3 endPos = position + velocity * timeleft; - - TraceInfo trace = TracePlayer(actor, startPos, endPos); - // TODO: handle portals here - - float fraction = trace.fraction; - Vector3 hitNormal = trace.hitNormal; - - if (trace.startSolid) - { - velocity = Float3.Zero; - break; - } - - if (fraction > 0f) - { - position = trace.endPosition; - hitNormals.Clear(); // this is present in some forks, not in Q3 - } - - if (fraction >= 1f) - break; - - timeleft *= 1.0f - fraction; - - if (trace.hitNormal.Y > movementParameters.slopeNormal) - slideMoveHit |= SlideMoveHit.Floor; - else if (Math.Abs(trace.hitNormal.Y) < 0.0001f) - slideMoveHit |= SlideMoveHit.Step; - else - slideMoveHit |= SlideMoveHit.Other; - - // this doesn't seem to do anything, we never have any hitNormals stored here - bool hitPreviousNormal = false; - foreach (Float3 normal in hitNormals) - if (Float3.Dot(hitNormal, normal) > 0.99) + if (worldStateManager.ServerFrame > 0 && worldStateManager.ClientFrame > 0) + for (ulong frame = worldStateManager.ServerFrame; frame < worldStateManager.ClientFrame; frame++) { - // nudge away from the same wall we hit earlier and try again - velocity += hitNormal; - hitPreviousNormal = true; - break; - } - - if (hitPreviousNormal) - continue; - - hitNormals.Add(hitNormal); - if (hitNormals.Count != 1) - Console.Print("hitNormals: " + hitNormals.Count); - - int plane; - Vector3 normalMargin = Float3.Zero; - for (plane = 0; plane < hitNormals.Count; plane++) - { - Vector3 normal = hitNormals[plane]; - - // clip velocity - velocity -= normal * Float3.Dot(velocity, normal); - //velocity = Float3.ProjectOnPlane(velocity, normal); - - //traceOffset = normal * 1f; - normalMargin += normal; - //position += normal * 0.031f; - - int plane2; - for (plane2 = 0; plane2 < hitNormals.Count; plane2++) - { - if (plane == plane2) + if (!input.GetState(frame, out var pastInputState, out var pastActorState)) continue; - if (Float3.Dot(velocity, hitNormals[plane2]) < 0f) - break; + var bbox = clientBbox; + bbox.Center = pastActorState.position; + + Float4 color1 = new Float4(Color.Red.R, Color.Red.G, Color.Red.B, Color.Red.A); + Float4 color2 = new Float4(Color.Blue.R, Color.Blue.G, Color.Blue.B, Color.Blue.A); + Float4 color3 = Float4.Lerp(color1, color2, (float)(frame - worldStateManager.ServerFrame) / (float)(worldStateManager.ClientFrame - worldStateManager.ServerFrame)); + Color color = new Color(color3.X, color3.Y, color3.Z, color3.W); + DebugDraw.DrawBox(bbox, color * 1f); } + } + else + { + var serverBbox = boxCollider.OrientedBox.GetBoundingBox(); + if (input.GetState(worldStateManager.ServerFrame, out var serverInputState, out var serverActorState)) + serverBbox.Center = serverActorState.position; - if (plane2 == hitNormals.Count) - break; - } - - // push off slightly away from the walls to not get stuck - position += normalMargin.Normalized * movementParameters.collisionMargin; - //Console.Print("pushin"); - - if (plane == hitNormals.Count) + if (serverBbox.Center == clientBbox.Center) + DebugDraw.DrawBox(clientBbox, Color.Magenta * 0.6f); + else { - if (hitNormals.Count == 2) - { - Vector3 dir = Float3.Cross(hitNormals[0], hitNormals[1]); - //dir.Normalize(); - float dist = Float3.Dot(dir, velocity); - velocity = dist * dir; - } - else - { - velocity = Float3.Zero; - break; - } + DebugDraw.DrawBox(serverBbox, Color.Red * 0.6f); + DebugDraw.DrawBox(clientBbox, Color.Blue * 0.6f); + } + } + } + } +#endif + + private static SlideMoveHit StepSlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity, + bool onGround) + { + if (velocity.IsZero) + return SlideMoveHit.Nothing; + + Vector3 gravityDirection = movementParameters.gravity.Normalized; + + Vector3 originalPosition = position; + Vector3 originalVelocity = velocity; + + SlideMoveHit slideMoveHit = SlideMove(actor, movementParameters, ref position, ref velocity); + if (slideMoveHit == SlideMoveHit.Nothing) + // TODO: step down here + return slideMoveHit; + + // hit something, try to step up + float effectiveStepHeight = movementParameters.stepHeight; + if (!onGround) + { + // TODO: implement clipping here + /*if (pmove.jump_time > 0 && pmove.jump_time <= movevars.cliptime) + { + float zvel = pmove.velocity[2]; + VectorCopy(originalvel, pmove.velocity); + pmove.velocity[2] = min(pmove.velocity[2], zvel); // nullifies vertical clipping + }*/ + + if (!slideMoveHit.HasFlag(SlideMoveHit.Step)) + return slideMoveHit; + + if (movementParameters.airStep < 2) + { + //effectiveStepHeight = ? + } + } + + + Vector3 stepDelta = -gravityDirection * effectiveStepHeight; + + Vector3 slidePosition = position; + Vector3 slideVelocity = velocity; + position = originalPosition; + velocity = originalVelocity; + + // step up + Vector3 stepUp = position + stepDelta; + TraceInfo traceUp = TracePlayer(actor, position, stepUp); + if (traceUp.fraction > 0f) + position = traceUp.endPosition; + + // try moving from step up position + SlideMoveHit slideMoveStepHit = SlideMove(actor, movementParameters, ref position, ref velocity); + + // step down + Vector3 stepDown = position - stepDelta; + TraceInfo traceDown = TracePlayer(actor, position, stepDown); + if (traceDown.fraction < 1f && -Float3.Dot(gravityDirection, traceDown.hitNormal) < movementParameters.slopeNormal) + { + // can't step down, slide move like normally + Console.Print("no stepping 1, frac: " + traceDown.fraction + ", dot: " + + -Float3.Dot(gravityDirection, traceDown.hitNormal) + + ", norm: " + traceDown.hitNormal); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + if (traceDown.fraction > 0f) + position = traceDown.endPosition; + + // add some margin from the ground in order to avoid getting stuck after stepping up + if (traceDown.fraction < 1f) + position.Y += movementParameters.collisionMargin; + + // ?? + float d1 = -Float3.Dot(gravityDirection, position); + float d2 = -Float3.Dot(gravityDirection, originalPosition); + if (d1 < d2) + { + //Console.Print("no stepping 2, " + d1 + " < " + d2); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + Vector3 slidePosition2 = slidePosition; //down + Vector3 stepPosition2 = position; //up + + // FIXME, negate movementParameters.gravity + slidePosition2.Y = 0f; + stepPosition2.Y = 0f; + + // take the slide movement results if furthest away from original position + //if ((stepPosition2 - originalPosition).Length < (slidePosition2 - originalPosition).Length) + if ((slidePosition2 - originalPosition).Length >= (stepPosition2 - originalPosition).Length) + { + //Console.Print("no stepping 3"); + position = slidePosition; + velocity = slideVelocity; + return slideMoveHit; + } + + //return slideMoveStepHit; + return slideMoveHit; + } + + private static SlideMoveHit SlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity) + { + if (velocity.IsZero) + return SlideMoveHit.Nothing; + + Vector3 originalPosition = position; + Vector3 originalVelocity = velocity; + SlideMoveHit slideMoveHit = SlideMoveHit.Nothing; + + float timeleft = Time.DeltaTime; + + var hitNormals = new List(); + + for (int bump = 0; bump < 4; bump++) + { + Vector3 startPos = position; + Vector3 endPos = position + velocity * timeleft; + + TraceInfo trace = TracePlayer(actor, startPos, endPos); + // TODO: handle portals here + + float fraction = trace.fraction; + Vector3 hitNormal = trace.hitNormal; + + if (trace.startSolid) + { + velocity = Float3.Zero; + break; + } + + if (fraction > 0f) + { + position = trace.endPosition; + hitNormals.Clear(); // this is present in some forks, not in Q3 + } + + if (fraction >= 1f) + break; + + timeleft *= 1.0f - fraction; + + if (trace.hitNormal.Y > movementParameters.slopeNormal) + slideMoveHit |= SlideMoveHit.Floor; + else if (Math.Abs(trace.hitNormal.Y) < 0.0001f) + slideMoveHit |= SlideMoveHit.Step; + else + slideMoveHit |= SlideMoveHit.Other; + + // this doesn't seem to do anything, we never have any hitNormals stored here + bool hitPreviousNormal = false; + foreach (Float3 normal in hitNormals) + if (Float3.Dot(hitNormal, normal) > 0.99) + { + // nudge away from the same wall we hit earlier and try again + velocity += hitNormal; + hitPreviousNormal = true; + break; } - // prevents bouncing against the wall - if ( /*velocity.Length > 0f && */Float3.Dot(velocity, originalVelocity) <= 0f) + if (hitPreviousNormal) + continue; + + hitNormals.Add(hitNormal); + if (hitNormals.Count != 1) + Console.Print("hitNormals: " + hitNormals.Count); + + int plane; + Vector3 normalMargin = Float3.Zero; + for (plane = 0; plane < hitNormals.Count; plane++) + { + Vector3 normal = hitNormals[plane]; + + // clip velocity + velocity -= normal * Float3.Dot(velocity, normal); + //velocity = Float3.ProjectOnPlane(velocity, normal); + + //traceOffset = normal * 1f; + normalMargin += normal; + //position += normal * 0.031f; + + int plane2; + for (plane2 = 0; plane2 < hitNormals.Count; plane2++) + { + if (plane == plane2) + continue; + + if (Float3.Dot(velocity, hitNormals[plane2]) < 0f) + break; + } + + if (plane2 == hitNormals.Count) + break; + } + + // push off slightly away from the walls to not get stuck + position += normalMargin.Normalized * movementParameters.collisionMargin; + //Console.Print("pushin"); + + if (plane == hitNormals.Count) + { + if (hitNormals.Count == 2) + { + Vector3 dir = Float3.Cross(hitNormals[0], hitNormals[1]); + //dir.Normalize(); + float dist = Float3.Dot(dir, velocity); + velocity = dist * dir; + } + else { velocity = Float3.Zero; break; } } - return slideMoveHit; + // prevents bouncing against the wall + if ( /*velocity.Length > 0f && */Float3.Dot(velocity, originalVelocity) <= 0f) + { + velocity = Float3.Zero; + break; + } } - public void SimulatePlayerMovement(PlayerInputState inputState) + return slideMoveHit; + } + + public void SimulatePlayerMovement(PlayerInputState inputState) + { + simulationTime = worldStateManager.ClientTime + (inputState.frame * (1.0f / Time.PhysicsFPS)); + + Vector3 inputDirection = + new Float3(inputState.moveRight, 0.0f, inputState.moveForward); + Vector3 moveDirection = rootActor.Transform.TransformDirection(inputDirection); + Vector3 position = movementState.position; + Vector3 velocity = movementState.currentVelocity; //rigidBody.LinearVelocity; + Vector3 wishVelocity = !inputDirection.IsZero ? moveDirection.Normalized * movementParameters.moveSpeed : Vector3.Zero; + + if (position != rigidBody.Position) + Console.Print("PlayerMovement: rigidbody position does not match with movement state position"); + + // categorize position + Vector3 lastVelocity = velocity; + movementState.onGround = true; + + TraceInfo traceGround = CategorizePosition(position, ref velocity); + + bool jumpAction = inputState.jumping; + + if (movementState.jumped && !jumpAction) + movementState.jumped = false; // jump released + else if (movementState.jumped && simulationTime - movementState.lastJumped >= movementParameters.autoJumpTime) + movementState.jumped = false; // jump timeout + + // jump + if (movementState.onGround && jumpAction && !movementState.jumped) + if (OnJump(traceGround, ref position, ref velocity)) + { + //Console.Print($"{inputState.frame} jumped " + ", predicting: " + predicting + ", vel: " + velocity.Y); + movementState.jumped = true; + movementState.lastJumped = simulationTime; + movementState.numJumps++; + } + + if (simulationTime - movementState.lastJumped > movementParameters.jumpBoostTime) + movementState.numJumps = 0; + + //if (/*onGround && */lastGround != onGround) + if (movementState.onGround) { - simulationTime = worldStateManager.ClientTime + (inputState.frame * (1.0f / Time.PhysicsFPS)); + // ground friction + ApplyFriction(movementParameters, ref velocity); - Vector3 inputDirection = - new Float3(inputState.moveRight, 0.0f, inputState.moveForward); - Vector3 moveDirection = rootActor.Transform.TransformDirection(inputDirection); - Vector3 position = movementState.position; - Vector3 velocity = movementState.currentVelocity; //rigidBody.LinearVelocity; - Vector3 wishVelocity = !inputDirection.IsZero ? moveDirection.Normalized * movementParameters.moveSpeed : Vector3.Zero; - - if (position != rigidBody.Position) - Console.Print("PlayerMovement: rigidbody position does not match with movement state position"); - - // categorize position - Vector3 lastVelocity = velocity; - movementState.onGround = true; - - TraceInfo traceGround = CategorizePosition(position, ref velocity); - - bool jumpAction = inputState.jumping; - - if (movementState.jumped && !jumpAction) - movementState.jumped = false; // jump released - else if (movementState.jumped && simulationTime - movementState.lastJumped >= movementParameters.autoJumpTime) - movementState.jumped = false; // jump timeout - - // jump - if (movementState.onGround && jumpAction && !movementState.jumped) - if (OnJump(traceGround, ref position, ref velocity)) - { - //Console.Print($"{inputState.frame} jumped " + ", predicting: " + predicting + ", vel: " + velocity.Y); - movementState.jumped = true; - movementState.lastJumped = simulationTime; - movementState.numJumps++; - } - - if (simulationTime - movementState.lastJumped > movementParameters.jumpBoostTime) - movementState.numJumps = 0; - - //if (/*onGround && */lastGround != onGround) - if (movementState.onGround) - { - // ground friction - ApplyFriction(movementParameters, ref velocity); - - // ground acceleration - ApplyAcceleration(ref velocity, wishVelocity.Normalized, wishVelocity.Length, float.MaxValue, - movementParameters.accelerationGround); - } - else // air movement - { - ApplyAirAcceleration(movementParameters, ref velocity, wishVelocity); - } - - StepSlideMove(Actor as PlayerActor, movementParameters, ref position, ref velocity, movementState.onGround); - - - TraceInfo traceGround2 = CategorizePosition(position, ref velocity); - - movementState.position = position; - rigidBody.Position = position; - movementState.currentVelocity = velocity; - //rigidBody.LinearVelocity = velocity; - - const float landingVelocityThreshold = 120f; - const float landingHardVelocityThreshold = 500f; - if (movementState.currentVelocity.Y - lastVelocity.Y > landingVelocityThreshold) - if (simulationTime - movementState.lastJumped > 0.01) - { - bool hardLanding = movementState.currentVelocity.Y - lastVelocity.Y > landingHardVelocityThreshold; - OnLanded(movementState.currentVelocity - lastVelocity, hardLanding); - movementState.lastLanded = simulationTime; - } + // ground acceleration + ApplyAcceleration(ref velocity, wishVelocity.Normalized, wishVelocity.Length, float.MaxValue, + movementParameters.accelerationGround); + } + else // air movement + { + ApplyAirAcceleration(movementParameters, ref velocity, wishVelocity); } - private TraceInfo CategorizePosition(Float3 position, ref Vector3 velocity) - { - Vector3 groundDelta = movementParameters.gravity.Normalized; //movementParameters.gravity.Normalized * (collisionMargin * 2); - //if (velocity.Y < 0f) - // groundDelta = movementParameters.gravity.Normalized * velocity.Y * Time.DeltaTime; - TraceInfo traceGround = TracePlayer(Actor as PlayerActor, position, position + groundDelta); - //Console.PrintDebug(1, true, "startSolid: " + traceGround.startSolid); + StepSlideMove(Actor as PlayerActor, movementParameters, ref position, ref velocity, movementState.onGround); - if (!traceGround.startSolid && traceGround.fraction < 1f && - -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal) + + TraceInfo traceGround2 = CategorizePosition(position, ref velocity); + + movementState.position = position; + rigidBody.Position = position; + movementState.currentVelocity = velocity; + //rigidBody.LinearVelocity = velocity; + + const float landingVelocityThreshold = 120f; + const float landingHardVelocityThreshold = 500f; + if (movementState.currentVelocity.Y - lastVelocity.Y > landingVelocityThreshold) + if (simulationTime - movementState.lastJumped > 0.01) { - // clip velocity - - Vector3 bounce = groundDelta; - //Vector3 velocityProjected = Float3.ProjectOnPlane(velocity, normal); - float backoff = Float3.Dot(bounce, traceGround.hitNormal) * 2f; - bounce -= traceGround.hitNormal * backoff; - //velocity = velocityProjected; - - Vector3 point = position + groundDelta + - (1f - traceGround.fraction) * bounce; - - Console.Print("backoff: " + backoff); - - // retrace - traceGround = TracePlayer(Actor as PlayerActor, position, position + point); + bool hardLanding = movementState.currentVelocity.Y - lastVelocity.Y > landingHardVelocityThreshold; + OnLanded(movementState.currentVelocity - lastVelocity, hardLanding); + movementState.lastLanded = simulationTime; } + } - if (!traceGround.startSolid && (traceGround.fraction >= 1f || - -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal)) - // falling or sliding down a slope - movementState.onGround = false; - //Console.PrintDebug(1, true, "fall or slide"); - else - //if (onGround != !traceGround.startSolid) - // Console.Print("slidefrac: " + traceGround.fraction); - movementState.onGround = !traceGround.startSolid; - //Console.PrintDebug(1, true, "issolid? :" + traceGround.startSolid); + private TraceInfo CategorizePosition(Float3 position, ref Vector3 velocity) + { + Vector3 groundDelta = movementParameters.gravity.Normalized; //movementParameters.gravity.Normalized * (collisionMargin * 2); + //if (velocity.Y < 0f) + // groundDelta = movementParameters.gravity.Normalized * velocity.Y * Time.DeltaTime; + TraceInfo traceGround = TracePlayer(Actor as PlayerActor, position, position + groundDelta); + //Console.PrintDebug(1, true, "startSolid: " + traceGround.startSolid); - //if (onGround && !slope) - // velocity.Y = 0f; + if (!traceGround.startSolid && traceGround.fraction < 1f && + -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal) + { + // clip velocity - // TODO: snap to ground here? - return traceGround; + Vector3 bounce = groundDelta; + //Vector3 velocityProjected = Float3.ProjectOnPlane(velocity, normal); + float backoff = Float3.Dot(bounce, traceGround.hitNormal) * 2f; + bounce -= traceGround.hitNormal * backoff; + //velocity = velocityProjected; + + Vector3 point = position + groundDelta + + (1f - traceGround.fraction) * bounce; + + Console.Print("backoff: " + backoff); + + // retrace + traceGround = TracePlayer(Actor as PlayerActor, position, position + point); } - private bool OnJump(TraceInfo traceGround, ref Vector3 position, ref Vector3 velocity) - { - float jumpVel = movementParameters.jumpVelocity; - if (simulationTime - movementState.lastJumped < movementParameters.jumpBoostTime) - jumpVel += movementParameters.jumpBoostVelocity; - - // Reset velocity from movementParameters.gravity - if (-Float3.Dot(movementParameters.gravity.Normalized, velocity) < 0 && - Float3.Dot(velocity, traceGround.hitNormal) < -0.1) - { - velocity = Float3.ProjectOnPlane(velocity, traceGround.hitNormal); - } - - if (movementParameters.jumpAdditive) - { - velocity += Float3.Up * jumpVel; - if (velocity.Y < jumpVel) - velocity.Y = jumpVel; - } - else - velocity = Float3.Up * jumpVel + new Float3(1, 0, 1) * velocity; - + if (!traceGround.startSolid && (traceGround.fraction >= 1f || + -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal)) + // falling or sliding down a slope movementState.onGround = false; + //Console.PrintDebug(1, true, "fall or slide"); + else + //if (onGround != !traceGround.startSolid) + // Console.Print("slidefrac: " + traceGround.fraction); + movementState.onGround = !traceGround.startSolid; + //Console.PrintDebug(1, true, "issolid? :" + traceGround.startSolid); - // Allow stairs to eat the first jump to allow easy stair jumps - if (movementParameters.jumpStairBehavior && movementParameters.jumpBoostMaxJumps >= 2 && movementState.numJumps == 0 && - -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) > 0.85) - { - // Try stepping into stairs without vertical velocity - Vector3 stairCheckPosition = position; - Vector3 stairCheckVelocity = velocity.Normalized * (movementParameters.stepHeight / Time.DeltaTime); - stairCheckVelocity.Y = 0f; + //if (onGround && !slope) + // velocity.Y = 0f; - SlideMoveHit blocked = StepSlideMove(Actor as PlayerActor, movementParameters, ref stairCheckPosition, ref stairCheckVelocity, true); - float movedUp = stairCheckPosition.Y - position.Y; + // TODO: snap to ground here? + return traceGround; + } - if (movedUp > 0 && blocked.HasFlag(SlideMoveHit.Step)) - { - velocity.Y = 0f; - movementState.onGround = true; - } - } + private bool OnJump(TraceInfo traceGround, ref Vector3 position, ref Vector3 velocity) + { + float jumpVel = movementParameters.jumpVelocity; + if (simulationTime - movementState.lastJumped < movementParameters.jumpBoostTime) + jumpVel += movementParameters.jumpBoostVelocity; - if (!predicting) - // Avoid overlapping with recent landing sound - if (simulationTime - movementState.lastLanded > 0.3) - PlayJumpLandSound(false, false); - - return true; + // Reset velocity from movementParameters.gravity + if (-Float3.Dot(movementParameters.gravity.Normalized, velocity) < 0 && + Float3.Dot(velocity, traceGround.hitNormal) < -0.1) + { + velocity = Float3.ProjectOnPlane(velocity, traceGround.hitNormal); } - private void OnLanded(Float3 landingVelocity, bool hardLanding) + if (movementParameters.jumpAdditive) { - if (!predicting) - PlayJumpLandSound(true, hardLanding); + velocity += Float3.Up * jumpVel; + if (velocity.Y < jumpVel) + velocity.Y = jumpVel; } + else + velocity = Float3.Up * jumpVel + new Float3(1, 0, 1) * velocity; - private void PlayJumpLandSound(bool landing, bool hardLanding) + movementState.onGround = false; + + // Allow stairs to eat the first jump to allow easy stair jumps + if (movementParameters.jumpStairBehavior && movementParameters.jumpBoostMaxJumps >= 2 && movementState.numJumps == 0 && + -Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) > 0.85) { - if (!landing) - movementState.lastLanded = -1; // Reset so double jumps have double sounds + // Try stepping into stairs without vertical velocity + Vector3 stairCheckPosition = position; + Vector3 stairCheckVelocity = velocity.Normalized * (movementParameters.stepHeight / Time.DeltaTime); + stairCheckVelocity.Y = 0f; - float volume1 = 0.8f; - float volume2 = volume1; - Float2 pitchRange = new Float2(0.9f, 1.05f); - Float2 secondStepDelayRange = new Float2(0.031f, 0.059f); + SlideMoveHit blocked = StepSlideMove(Actor as PlayerActor, movementParameters, ref stairCheckPosition, ref stairCheckVelocity, true); + float movedUp = stairCheckPosition.Y - position.Y; - if (landing) - volume2 *= 0.6f; - else - volume1 *= 0.6f; - - AudioManager.PlaySound("jumpland", Actor, 0, AudioFlags.None, rootActor.Position, volume1, pitchRange); - if (landing) + if (movedUp > 0 && blocked.HasFlag(SlideMoveHit.Step)) { - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - /*AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange); - AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, - pitchRange);*/ + velocity.Y = 0f; + movementState.onGround = true; } } - private static void ApplyFriction(PlayerMovementParameters movementParameters, ref Vector3 velocity) + if (!predicting) + // Avoid overlapping with recent landing sound + if (simulationTime - movementState.lastLanded > 0.3) + PlayJumpLandSound(false, false); + + return true; + } + + private void OnLanded(Float3 landingVelocity, bool hardLanding) + { + if (!predicting) + PlayJumpLandSound(true, hardLanding); + } + + private void PlayJumpLandSound(bool landing, bool hardLanding) + { + if (!landing) + movementState.lastLanded = -1; // Reset so double jumps have double sounds + + float volume1 = 0.8f; + float volume2 = volume1; + Float2 pitchRange = new Float2(0.9f, 1.05f); + Float2 secondStepDelayRange = new Float2(0.031f, 0.059f); + + if (landing) + volume2 *= 0.6f; + else + volume1 *= 0.6f; + + AudioManager.PlaySound("jumpland", Actor, 0, AudioFlags.None, rootActor.Position, volume1, pitchRange); + if (landing) { - float currentSpeed = velocity.Length; - - float control = currentSpeed < movementParameters.stopspeed ? movementParameters.stopspeed : currentSpeed; - float drop = control * movementParameters.friction * Time.DeltaTime; - - float newspeed = currentSpeed - drop; - if (newspeed < 0) - newspeed = 0; - - if (currentSpeed < 0.0001f) - velocity *= 0; - else - velocity *= newspeed / currentSpeed; - } - - private static void ApplyAirAcceleration(PlayerMovementParameters movementParameters, ref Vector3 velocity, Vector3 wishVelocity) - { - float wishspeed = wishVelocity.Length; - if (wishspeed > movementParameters.maxAirSpeed) - wishspeed = movementParameters.maxAirSpeed; - - Vector3 wishDir = wishVelocity.Normalized; - float wishspeedAirControl = wishspeed; - - if (movementParameters.airAcceleration != 0) - { - // Q2+ air acceleration - float accel = movementParameters.airAcceleration; - if (Float3.Dot(velocity, wishDir) < 0) - accel = movementParameters.airStopAcceleration; - - if (movementParameters.airStrafeAcceleration != 0 && Mathf.Abs(wishVelocity.X) > 0 && wishVelocity.Y == 0) - { - // only strafe movement - if (wishspeed > movementParameters.maxAirStrafeSpeed) - wishspeed = movementParameters.maxAirStrafeSpeed; - - accel = movementParameters.airStrafeAcceleration; - } - - ApplyAcceleration(ref velocity, wishDir, wishspeed, float.MaxValue, accel); - } - - // QW air acceleration - if (movementParameters.strafeAcceleration != 0f) - { - ApplyAcceleration(ref velocity, wishDir, wishspeed, movementParameters.maxAirStrafeSpeed, - movementParameters.strafeAcceleration); - } - - // air control while holding forward/back buttons - //if (airControl != 0 && moveDirection.X == 0 && Mathf.Abs(moveDirection.Y) > 0) - // PM_Aircontrol(wishdir, wishspeedAirControl); - - // apply movementParameters.gravity - velocity += movementParameters.gravity * Time.DeltaTime; - //Console.Print(Time.DeltaTime.ToString()); - } - - private static void ApplyAcceleration(ref Vector3 velocity, Vector3 wishDir, float wishspeed, - float maxWishspeed, float acceleration) - { - float wishspeedOrig = wishspeed; - if (wishspeed > maxWishspeed) - wishspeed = maxWishspeed; - - float currentSpeed = Float3.Dot(velocity, wishDir); - float addSpeed = wishspeed - currentSpeed; - if (addSpeed <= 0f) - return; - - float accelSpeed = acceleration * wishspeedOrig * Time.DeltaTime; - if (accelSpeed > addSpeed) - accelSpeed = addSpeed; - - velocity += accelSpeed * wishDir; - } - - [Flags] - private enum SlideMoveHit - { - Nothing = 0, - Step = 1, - Floor = 2, - Other = 4 + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + /*AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange); + AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2, + pitchRange);*/ } } + + private static void ApplyFriction(PlayerMovementParameters movementParameters, ref Vector3 velocity) + { + float currentSpeed = velocity.Length; + + float control = currentSpeed < movementParameters.stopspeed ? movementParameters.stopspeed : currentSpeed; + float drop = control * movementParameters.friction * Time.DeltaTime; + + float newspeed = currentSpeed - drop; + if (newspeed < 0) + newspeed = 0; + + if (currentSpeed < 0.0001f) + velocity *= 0; + else + velocity *= newspeed / currentSpeed; + } + + private static void ApplyAirAcceleration(PlayerMovementParameters movementParameters, ref Vector3 velocity, Vector3 wishVelocity) + { + float wishspeed = wishVelocity.Length; + if (wishspeed > movementParameters.maxAirSpeed) + wishspeed = movementParameters.maxAirSpeed; + + Vector3 wishDir = wishVelocity.Normalized; + float wishspeedAirControl = wishspeed; + + if (movementParameters.airAcceleration != 0) + { + // Q2+ air acceleration + float accel = movementParameters.airAcceleration; + if (Float3.Dot(velocity, wishDir) < 0) + accel = movementParameters.airStopAcceleration; + + if (movementParameters.airStrafeAcceleration != 0 && Mathf.Abs(wishVelocity.X) > 0 && wishVelocity.Y == 0) + { + // only strafe movement + if (wishspeed > movementParameters.maxAirStrafeSpeed) + wishspeed = movementParameters.maxAirStrafeSpeed; + + accel = movementParameters.airStrafeAcceleration; + } + + ApplyAcceleration(ref velocity, wishDir, wishspeed, float.MaxValue, accel); + } + + // QW air acceleration + if (movementParameters.strafeAcceleration != 0f) + { + ApplyAcceleration(ref velocity, wishDir, wishspeed, movementParameters.maxAirStrafeSpeed, + movementParameters.strafeAcceleration); + } + + // air control while holding forward/back buttons + //if (airControl != 0 && moveDirection.X == 0 && Mathf.Abs(moveDirection.Y) > 0) + // PM_Aircontrol(wishdir, wishspeedAirControl); + + // apply movementParameters.gravity + velocity += movementParameters.gravity * Time.DeltaTime; + //Console.Print(Time.DeltaTime.ToString()); + } + + private static void ApplyAcceleration(ref Vector3 velocity, Vector3 wishDir, float wishspeed, + float maxWishspeed, float acceleration) + { + float wishspeedOrig = wishspeed; + if (wishspeed > maxWishspeed) + wishspeed = maxWishspeed; + + float currentSpeed = Float3.Dot(velocity, wishDir); + float addSpeed = wishspeed - currentSpeed; + if (addSpeed <= 0f) + return; + + float accelSpeed = acceleration * wishspeedOrig * Time.DeltaTime; + if (accelSpeed > addSpeed) + accelSpeed = addSpeed; + + velocity += accelSpeed * wishDir; + } + + [Flags] + private enum SlideMoveHit + { + Nothing = 0, + Step = 1, + Floor = 2, + Other = 4 + } } diff --git a/Source/Game/Utility/AssetManager.cs b/Source/Game/Utility/AssetManager.cs index 0ac4a50..0f5879a 100644 --- a/Source/Game/Utility/AssetManager.cs +++ b/Source/Game/Utility/AssetManager.cs @@ -1,27 +1,26 @@ using System.IO; using FlaxEngine; -namespace Game +namespace Game; + +public static class AssetManager { - public static class AssetManager + public static string ContentPath { get; private set; } = + Path.Combine(Directory.GetCurrentDirectory(), "Content"); + + public static string DemoPath { get; private set; } = + Path.Combine(Directory.GetCurrentDirectory(), "Demos"); + + public static string CachePath { get; private set; } = + Path.Combine(Directory.GetCurrentDirectory(), "Cache"); + + public static GameplayGlobals Globals { get; private set; } + public static Config Config { get; private set; } + + //public static void Init() + static AssetManager() { - public static string ContentPath { get; private set; } = - Path.Combine(Directory.GetCurrentDirectory(), "Content"); - - public static string DemoPath { get; private set; } = - Path.Combine(Directory.GetCurrentDirectory(), "Demos"); - - public static string CachePath { get; private set; } = - Path.Combine(Directory.GetCurrentDirectory(), "Cache"); - - public static GameplayGlobals Globals { get; private set; } - public static Config Config { get; private set; } - - //public static void Init() - static AssetManager() - { - Globals = Content.Load(Path.Combine(ContentPath, "Settings", "GameSettings", "GameplayGlobals.flax")); - Config = ConfigParser.ParseFile(Path.Combine(ContentPath, "config.cfg")); - } + Globals = Content.Load(Path.Combine(ContentPath, "Settings", "GameSettings", "GameplayGlobals.flax")); + Config = ConfigParser.ParseFile(Path.Combine(ContentPath, "config.cfg")); } } \ No newline at end of file