diff --git a/.clang-format b/.clang-format index e15b43f0..afd0049f 100644 --- a/.clang-format +++ b/.clang-format @@ -3,7 +3,7 @@ BasedOnStyle: Microsoft AccessModifierOffset: '-4' AlignAfterOpenBracket: DontAlign AlignEscapedNewlines: Left -AllowShortFunctionsOnASingleLine: Inline +AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: Never AlwaysBreakAfterReturnType: None AlwaysBreakTemplateDeclarations: 'Yes' diff --git a/.gitignore b/.gitignore index 301f476b..368ebc31 100644 --- a/.gitignore +++ b/.gitignore @@ -387,9 +387,16 @@ vcpkg_installed/ [Ss]ource/Mocha.Common/Glue/UnmanagedArgs.cs cvars.json [Ss]amples/mocha-minimal/code/*.csproj +[Ss]amples/mocha-minimal/code/Properties # Content meta files ![Cc]ontent/**/*.meta # Cmake build files [Cc]ompile/ + +# Server configuration +server.cfg + +# JetBrains IDEs +Source/.idea diff --git a/.gitmodules b/.gitmodules index c841e76c..3025f2f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,15 @@ -[submodule "Source/Host/thirdparty/imgui"] - path = Source/Mocha.Host/thirdparty/imgui +[submodule "Source/Host/Thirdparty/imgui"] + path = Source/Mocha.Host/Thirdparty/imgui url = https://github.com/ocornut/imgui/ -[submodule "Source/Host/thirdparty/JoltPhysics"] - path = Source/Mocha.Host/thirdparty/JoltPhysics +[submodule "Source/Host/Thirdparty/JoltPhysics"] + path = Source/Mocha.Host/Thirdparty/JoltPhysics url = https://github.com/jrouwe/JoltPhysics/ -[submodule "Source/Host/thirdparty/vk-bootstrap"] - path = Source/Mocha.Host/thirdparty/vk-bootstrap +[submodule "Source/Host/Thirdparty/vk-bootstrap"] + path = Source/Mocha.Host/Thirdparty/vk-bootstrap url = https://github.com/charles-lunarg/vk-bootstrap -[submodule "Source/Host/thirdparty/implot"] - path = Source/Mocha.Host/thirdparty/implot +[submodule "Source/Host/Thirdparty/implot"] + path = Source/Mocha.Host/Thirdparty/implot url = https://github.com/epezent/implot -[submodule "Source/Host/thirdparty/volk"] - path = Source/Mocha.Host/thirdparty/volk +[submodule "Source/Host/Thirdparty/volk"] + path = Source/Mocha.Host/Thirdparty/volk url = https://github.com/zeux/volk diff --git a/Content/core/materials/dev/dev_floor.mmat b/Content/core/materials/dev/dev_floor.mmat index 28895dbf..676fec4e 100644 --- a/Content/core/materials/dev/dev_floor.mmat +++ b/Content/core/materials/dev/dev_floor.mmat @@ -1 +1 @@ -{"DiffuseTexture":"textures/dev/dev_floor.mtex","NormalTexture":null,"AmbientOcclusionTexture":null,"MetalnessTexture":null,"RoughnessTexture":null} \ No newline at end of file +{"DiffuseTexture":"textures/dev/floor_basecolor.mtex","NormalTexture":"textures/dev/floor_normal.mtex","AmbientOcclusionTexture":"textures/dev/floor_ambientocclusion.mtex","MetalnessTexture":"textures/dev/floor_metallic.mtex","RoughnessTexture":"textures/dev/floor_roughness.mtex"} \ No newline at end of file diff --git a/Content/core/materials/dev/dev_wall.mmat b/Content/core/materials/dev/dev_wall.mmat index 7780e62e..6117f6ff 100644 --- a/Content/core/materials/dev/dev_wall.mmat +++ b/Content/core/materials/dev/dev_wall.mmat @@ -1,3 +1 @@ -{ - "DiffuseTexture": "textures/dev/dev_wall.mtex" -} \ No newline at end of file +{"DiffuseTexture":"textures/dev/wall_basecolor.mtex","NormalTexture":"textures/dev/wall_normal.mtex","AmbientOcclusionTexture":"textures/dev/wall_ambientocclusion.mtex","MetalnessTexture":"textures/dev/wall_metallic.mtex","RoughnessTexture":"textures/dev/wall_roughness.mtex"} \ No newline at end of file diff --git a/Content/core/materials/dev/dev_white.mmat b/Content/core/materials/dev/dev_white.mmat index 5acebfa5..2caca7e6 100644 --- a/Content/core/materials/dev/dev_white.mmat +++ b/Content/core/materials/dev/dev_white.mmat @@ -1 +1 @@ -{"DiffuseTexture":"textures/dev/dev_white.mtex","NormalTexture":null,"AmbientOcclusionTexture":null,"MetalnessTexture":null,"RoughnessTexture":null} \ No newline at end of file +{"DiffuseTexture":"textures/dev/generic_basecolor.mtex","NormalTexture":"textures/dev/generic_normal.mtex","AmbientOcclusionTexture":"textures/dev/generic_ambientocclusion.mtex","MetalnessTexture":"textures/dev/generic_metallic.mtex","RoughnessTexture":"textures/dev/generic_roughness.mtex"} \ No newline at end of file diff --git a/Content/core/textures/dev/dev_floor.png b/Content/core/textures/dev/dev_floor.png deleted file mode 100644 index 69be2111..00000000 Binary files a/Content/core/textures/dev/dev_floor.png and /dev/null differ diff --git a/Content/core/textures/dev/dev_wall.png b/Content/core/textures/dev/dev_wall.png deleted file mode 100644 index 4f5bf92e..00000000 Binary files a/Content/core/textures/dev/dev_wall.png and /dev/null differ diff --git a/Content/core/textures/dev/dev_white.png b/Content/core/textures/dev/dev_white.png deleted file mode 100644 index 72e1a077..00000000 Binary files a/Content/core/textures/dev/dev_white.png and /dev/null differ diff --git a/Content/core/textures/dev/floor_ambientocclusion.png b/Content/core/textures/dev/floor_ambientocclusion.png new file mode 100644 index 00000000..a8d9f02e Binary files /dev/null and b/Content/core/textures/dev/floor_ambientocclusion.png differ diff --git a/Content/core/textures/dev/floor_basecolor.png b/Content/core/textures/dev/floor_basecolor.png new file mode 100644 index 00000000..410a23ac Binary files /dev/null and b/Content/core/textures/dev/floor_basecolor.png differ diff --git a/Content/core/textures/dev/floor_height.png b/Content/core/textures/dev/floor_height.png new file mode 100644 index 00000000..7e41fd9f Binary files /dev/null and b/Content/core/textures/dev/floor_height.png differ diff --git a/Content/core/textures/dev/floor_metallic.png b/Content/core/textures/dev/floor_metallic.png new file mode 100644 index 00000000..58ad6111 Binary files /dev/null and b/Content/core/textures/dev/floor_metallic.png differ diff --git a/Content/core/textures/dev/floor_normal.png b/Content/core/textures/dev/floor_normal.png new file mode 100644 index 00000000..4c94e07f Binary files /dev/null and b/Content/core/textures/dev/floor_normal.png differ diff --git a/Content/core/textures/dev/floor_roughness.png b/Content/core/textures/dev/floor_roughness.png new file mode 100644 index 00000000..f141f8bf Binary files /dev/null and b/Content/core/textures/dev/floor_roughness.png differ diff --git a/Content/core/textures/dev/generic_ambientocclusion.png b/Content/core/textures/dev/generic_ambientocclusion.png new file mode 100644 index 00000000..a8d9f02e Binary files /dev/null and b/Content/core/textures/dev/generic_ambientocclusion.png differ diff --git a/Content/core/textures/dev/generic_basecolor.png b/Content/core/textures/dev/generic_basecolor.png new file mode 100644 index 00000000..16dbc41a Binary files /dev/null and b/Content/core/textures/dev/generic_basecolor.png differ diff --git a/Content/core/textures/dev/generic_height.png b/Content/core/textures/dev/generic_height.png new file mode 100644 index 00000000..7e41fd9f Binary files /dev/null and b/Content/core/textures/dev/generic_height.png differ diff --git a/Content/core/textures/dev/generic_metallic.png b/Content/core/textures/dev/generic_metallic.png new file mode 100644 index 00000000..58ad6111 Binary files /dev/null and b/Content/core/textures/dev/generic_metallic.png differ diff --git a/Content/core/textures/dev/generic_normal.png b/Content/core/textures/dev/generic_normal.png new file mode 100644 index 00000000..4c94e07f Binary files /dev/null and b/Content/core/textures/dev/generic_normal.png differ diff --git a/Content/core/textures/dev/generic_roughness.png b/Content/core/textures/dev/generic_roughness.png new file mode 100644 index 00000000..f141f8bf Binary files /dev/null and b/Content/core/textures/dev/generic_roughness.png differ diff --git a/Content/core/textures/dev/wall_ambientocclusion.png b/Content/core/textures/dev/wall_ambientocclusion.png new file mode 100644 index 00000000..a8d9f02e Binary files /dev/null and b/Content/core/textures/dev/wall_ambientocclusion.png differ diff --git a/Content/core/textures/dev/wall_basecolor.png b/Content/core/textures/dev/wall_basecolor.png new file mode 100644 index 00000000..d7b56137 Binary files /dev/null and b/Content/core/textures/dev/wall_basecolor.png differ diff --git a/Content/core/textures/dev/wall_height.png b/Content/core/textures/dev/wall_height.png new file mode 100644 index 00000000..7e41fd9f Binary files /dev/null and b/Content/core/textures/dev/wall_height.png differ diff --git a/Content/core/textures/dev/wall_metallic.png b/Content/core/textures/dev/wall_metallic.png new file mode 100644 index 00000000..58ad6111 Binary files /dev/null and b/Content/core/textures/dev/wall_metallic.png differ diff --git a/Content/core/textures/dev/wall_normal.png b/Content/core/textures/dev/wall_normal.png new file mode 100644 index 00000000..4c94e07f Binary files /dev/null and b/Content/core/textures/dev/wall_normal.png differ diff --git a/Content/core/textures/dev/wall_roughness.png b/Content/core/textures/dev/wall_roughness.png new file mode 100644 index 00000000..f141f8bf Binary files /dev/null and b/Content/core/textures/dev/wall_roughness.png differ diff --git a/Samples/mocha-minimal/code/Game.cs b/Samples/mocha-minimal/code/Game.cs index 002cd167..cfc1944a 100644 --- a/Samples/mocha-minimal/code/Game.cs +++ b/Samples/mocha-minimal/code/Game.cs @@ -5,20 +5,36 @@ namespace Minimal; public class Game : BaseGame { - [HotloadSkip] - private UIManager Hud { get; set; } + [HotloadSkip] private UIManager Hud { get; set; } - public override void Startup() + public string NetworkedString { get; set; } + + public override void OnStartup() { - // Set up UI - Hud = new UIManager(); - Hud.SetTemplate( "ui/Game.html" ); + if ( Core.IsServer ) + { + // We only want to create these entities on the server. + // They will automatically be replicated to clients. + + // Spawn a model to walk around in + var map = new ModelEntity( "models/dev/dev_map.mmdl" ); + map.SetMeshPhysics( "models/dev/dev_map.mmdl" ); - // Spawn a model to walk around in - var map = new ModelEntity( "models/dev/dev_map.mmdl" ); - map.SetMeshPhysics( "models/dev/dev_map.mmdl" ); + // Spawn a player + var player = new Player(); + player.Position = new Vector3( 0, 0, 50 ); + } + else + { + // UI is client-only + Hud = new UIManager(); + Hud.SetTemplate( "ui/Game.html" ); + } + } - // Spawn a player - var player = new Player(); + [Event.Tick] + public void Tick() + { + DebugOverlay.ScreenText( $"Tick... ({GetType().Assembly.GetHashCode()})" ); } } diff --git a/Samples/mocha-minimal/code/Player.cs b/Samples/mocha-minimal/code/Player.cs index 6f66f53b..8dc780b7 100644 --- a/Samples/mocha-minimal/code/Player.cs +++ b/Samples/mocha-minimal/code/Player.cs @@ -1,24 +1,23 @@ -using System.ComponentModel; - -namespace Minimal; +namespace Minimal; public class Player : Mocha.Player { - private Vector3 PlayerBounds = new( 0.5f, 0.5f, 1.8f ); // Metres - - public QuakeWalkController WalkController { get; private set; } + public WalkController WalkController { get; private set; } - [Category( "Player" )] - public bool IsGrounded => WalkController.IsGrounded; + public float Health { get; set; } - [Category( "Player" )] - public BaseEntity GroundEntity => WalkController.GroundEntity; + protected override void Spawn() + { + // TODO: This would be better as just a ctor + base.Spawn(); - public float Health { get; set; } + PlayerBounds = new( 0.5f, 0.5f, 1.8f ); // Metres + SetCubePhysics( PlayerBounds, false ); + } private void UpdateEyeTransform() { - EyePosition = Position + Vector3.Up * PlayerHalfExtents.Z; + EyePosition = Position + Vector3.Up * PlayerBounds.Z; EyeRotation = Input.Rotation; } @@ -26,18 +25,14 @@ public override void Respawn() { base.Respawn(); - PlayerHalfExtents = PlayerBounds / 2f; - WalkController = new( this ); Velocity = Vector3.Zero; - Position = new Vector3( 0.0f, 4.0f, 0.9f ); + Position = new Vector3( 0.0f, 4.0f, 5.0f ); } - public override void Update() + public void PredictedUpdate() { UpdateEyeTransform(); - - WalkController.Update(); } public override void FrameUpdate() @@ -48,7 +43,6 @@ public override void FrameUpdate() Health = MathX.Sin01( Time.Now ) * 100f; } - float lastHeight = 1.8f; float lastFov = 90f; private void UpdateCamera() @@ -63,19 +57,11 @@ private void UpdateCamera() // Camera.Position = Position + LocalEyePosition; - // Smooth out z-axis so that stairs, crouching are not sudden changes - Camera.Position = Camera.Position.WithZ( lastHeight.LerpTo( Camera.Position.Z, 10f * Time.Delta ) ); - lastHeight = Camera.Position.Z; - // // Field of view // float targetFov = 90f; - // Interpolate velocity when sprinting - if ( WalkController?.Sprinting ?? false && Velocity.WithZ( 0 ).Length > 1.0f ) - targetFov = 100f; - Camera.FieldOfView = lastFov.LerpTo( targetFov, 10 * Time.Delta ); lastFov = Camera.FieldOfView; diff --git a/Samples/mocha-minimal/code/WalkController.cs b/Samples/mocha-minimal/code/WalkController.cs new file mode 100644 index 00000000..da5fa928 --- /dev/null +++ b/Samples/mocha-minimal/code/WalkController.cs @@ -0,0 +1,223 @@ +using System; + +namespace Minimal; + +/* + * This is just a test for now. Will get put in base engine + * once it's done + */ + +public class WalkController +{ + public float Friction => 12.0f; + public float GroundAccelerate => 50.0f; + public float MaxVelocityGround => 50.0f; + public float AirAccelerate => 5.0f; + public float MaxVelocityAir => 100.0f; + public float GroundDistance => 0.1f; + public float StepSize => 0.5f; + public float MaxAngle => 60.0f; + + private bool IsGrounded => GroundEntity != null; + private BaseEntity GroundEntity; + + private Player Player { get; set; } + + private Vector3 Velocity; + + public WalkController( Player player ) + { + Player = player; + Player.IgnoreRigidbodyRotation = true; + + Event.Register( this ); + + Player.Position = new Vector3( 0, 0, 10 ); + } + + [Event.Tick] + public void PredictedUpdate() + { + DebugOverlay.ScreenText( $"--------------------------------------------------------------------------------" ); + DebugOverlay.ScreenText( $"{(Core.IsClient ? "Client" : "Server")}" ); + DebugOverlay.ScreenText( $"Velocity: {Velocity}" ); + DebugOverlay.ScreenText( $"GroundEntity: {GroundEntity?.Name ?? "None"}" ); + DebugOverlay.ScreenText( $"IsGrounded: {IsGrounded}" ); + + CheckGrounded(); + var wishDir = GetWishDir(); + + DebugOverlay.ScreenText( $"wishDir: {wishDir}" ); + DebugOverlay.ScreenText( $"GroundAccelerate: {GroundAccelerate}" ); + + if ( IsGrounded ) + { + Velocity = MoveGround( wishDir, Velocity ); + + Velocity.Z = 0; + + if ( Input.Jump ) + { + Velocity.Z += 4.0f; + GroundEntity = null; + } + } + else + { + Velocity = MoveAir( wishDir, Velocity ); + + Velocity.Z -= 9.8f * Time.Delta; + } + + Player.Velocity = Velocity * 10f; + Move(); + + DebugOverlay.ScreenText( $"--------------------------------------------------------------------------------" ); + } + + private Vector3 GetWishDir() + { + var eulerRotation = Input.Rotation.ToEulerAngles(); + var rotation = Rotation.From( eulerRotation.WithX( 0 ).WithZ( 0 ) ); + + var direction = Input.Direction.WithZ( 0 ); + + return direction * rotation; + } + + private Vector3 Accelerate( Vector3 accelDir, Vector3 oldVelocity, float accelerate, float maxSpeed ) + { + float projVel = Vector3.Dot( oldVelocity, accelDir ); + float accelVel = accelerate * Time.Delta; + + if ( projVel + accelVel > maxSpeed ) + accelVel = maxSpeed - projVel; + + return oldVelocity + accelDir * accelVel; + } + + private Vector3 MoveGround( Vector3 accelDir, Vector3 oldVelocity ) + { + float speed = oldVelocity.Length; + + if ( speed != 0 ) // Avoid divide by zero + { + float drop = speed * Friction * Time.Delta; + oldVelocity *= MathF.Max( speed - drop, 0 ) / speed; + } + + return Accelerate( accelDir, oldVelocity, GroundAccelerate, MaxVelocityGround ); + } + + private Vector3 MoveAir( Vector3 accelDir, Vector3 prevVelocity ) + { + return Accelerate( accelDir, prevVelocity, AirAccelerate, MaxVelocityAir ); + } + + public Mocha.TraceResult TraceBBox( Vector3 start, Vector3 end ) + { + return Cast.Ray( start, end ).WithHalfExtents( Player.PlayerBounds ).Ignore( Player ).Run(); + } + + private void CheckGrounded() + { + var tr = TraceBBox( Player.Position, Player.Position + Vector3.Down * GroundDistance ); + + // Grounded only counts if the normal is facing upwards + var angle = Vector3.GetAngle( tr.Normal, Vector3.Up ); + + if ( tr.Hit && angle < MaxAngle ) + GroundEntity = tr.Entity; + else + GroundEntity = null; + } + + private Vector3 ProjectOnPlane( Vector3 a, Vector3 plane ) + { + plane = plane.Normal; + var dot = Vector3.Dot( a, plane ); + return a - plane * dot; + } + + private (bool wasSuccess, Vector3 endPosition) SlideMove( Vector3 startPos, Vector3 endPos ) + { + var tr = TraceBBox( startPos, endPos ); + + if ( tr.Fraction < 1 ) + { + // The player is colliding with a wall + var normal = tr.Normal; + var newVel = ProjectOnPlane( Velocity, normal ); + endPos = startPos + newVel * Time.Delta; + + // Check for collisions with adjacent walls + var tr2 = TraceBBox( endPos, endPos + Vector3.Up * StepSize ); + if ( tr2.Fraction == 0 ) + { + // The player is stuck in a corner + var sideVel = ProjectOnPlane( Velocity, tr2.Normal ); + newVel += sideVel; + endPos = startPos + newVel * Time.Delta; + } + } + + var finalTr = TraceBBox( startPos, endPos ); + Player.Position = finalTr.EndPosition; + + return (true, finalTr.EndPosition); + } + + private MoveResult StepMove( Vector3 startPos, Vector3 endPos ) + { + var tr = TraceBBox( startPos, endPos ); + + if ( tr.Fraction < 1 ) + { + // Try stepping up + var stepPos = endPos + Vector3.Up * StepSize; + var stepTr = TraceBBox( stepPos, stepPos ); + + if ( stepTr.Fraction > 0 ) + { + // Trace back down to see how far we should step + var stepDownTr = TraceBBox( stepPos, stepPos + Vector3.Down * StepSize ); + return (true, stepDownTr.EndPosition); + } + } + + return (false, endPos); + } + + private void Move() + { + var startPos = Player.Position; + var endPos = startPos + Velocity * Time.Delta; + + var (stepSuccess, stepPosition) = StepMove( startPos, endPos ); + if ( stepSuccess ) + { + Player.Position = stepPosition; + return; + } + + var (slideSuccess, slidePosition) = SlideMove( startPos, stepPosition ); + if ( slideSuccess ) + { + Player.Position = slidePosition; + return; + } + } +} + +internal record struct MoveResult( bool wasSuccess, Vector3 endPosition ) +{ + public static implicit operator (bool wasSuccess, Vector3 endPosition)( MoveResult value ) + { + return (value.wasSuccess, value.endPosition); + } + + public static implicit operator MoveResult( (bool wasSuccess, Vector3 endPosition) value ) + { + return new MoveResult( value.wasSuccess, value.endPosition ); + } +} diff --git a/Source/Mocha.Common/AssemblyInfo.cs b/Source/Mocha.Common/AssemblyInfo.cs new file mode 100644 index 00000000..5bedafd7 --- /dev/null +++ b/Source/Mocha.Common/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible( false )] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid( "c43b1a2c-60e4-4832-a0ea-066eb190ba74" )] + +[assembly: InternalsVisibleTo( "Mocha.Tests" )] +[assembly: InternalsVisibleTo( "Mocha.Engine" )] +[assembly: InternalsVisibleTo( "Mocha.UI" )] +[assembly: InternalsVisibleTo( "Mocha.Hotload" )] +[assembly: InternalsVisibleTo( "Mocha.Networking" )] +[assembly: InternalsVisibleTo( "Mocha.CodeGen" )] +[assembly: InternalsVisibleTo( "Mocha.Editor" )] diff --git a/Source/Mocha.Common/Callbacks/CallbackDispatcher.cs b/Source/Mocha.Common/Callbacks/CallbackDispatcher.cs new file mode 100644 index 00000000..c5c92fdf --- /dev/null +++ b/Source/Mocha.Common/Callbacks/CallbackDispatcher.cs @@ -0,0 +1,29 @@ +namespace Mocha.Common; + +/// +/// We store all of the information required to call a C# method from a native +/// context here. +/// +public class CallbackDispatcher +{ + private static Dictionary _callbacks = new(); + private static uint _nextCallbackId = 0; + + public static uint RegisterCallback( T callback ) where T : Delegate + { + var callbackId = _nextCallbackId++; + _callbacks.Add( callbackId, new CallbackInfo( callback ) ); + + return callbackId; + } + + public static void Invoke( uint callbackId ) + { + _callbacks[callbackId].Invoke(); + } + + public static void Invoke( uint callbackId, IntPtr arg ) + { + _callbacks[callbackId].Invoke( arg ); + } +} diff --git a/Source/Mocha.Common/Callbacks/CallbackInfo.cs b/Source/Mocha.Common/Callbacks/CallbackInfo.cs new file mode 100644 index 00000000..6d64f940 --- /dev/null +++ b/Source/Mocha.Common/Callbacks/CallbackInfo.cs @@ -0,0 +1,21 @@ +namespace Mocha.Common; + +public readonly struct CallbackInfo +{ + public Delegate Callback { get; init; } + + public CallbackInfo( Delegate callback ) + { + Callback = callback; + } + + public void Invoke( IntPtr arg ) + { + Callback.DynamicInvoke( arg ); + } + + public void Invoke() + { + Callback.DynamicInvoke(); + } +} diff --git a/Source/Mocha.Common/Client/Logger.cs b/Source/Mocha.Common/Client/Logger.cs index 9aae70fd..ff722a6e 100644 --- a/Source/Mocha.Common/Client/Logger.cs +++ b/Source/Mocha.Common/Client/Logger.cs @@ -15,33 +15,19 @@ private string GetString( object? obj ) return logStr; } - public void Trace( object? obj ) + private void Log( object? obj, Action logAction ) { - string str = GetString( obj ); - OnLog?.Invoke( str ); - Glue.LogManager.ManagedTrace( str ); - } + var loggerName = Core.IsClient ? "cl" : "sv"; - public void Info( object? obj ) - { string str = GetString( obj ); OnLog?.Invoke( str ); - Glue.LogManager.ManagedInfo( str ); + logAction( loggerName, str ); } - public void Warning( object? obj ) - { - string str = GetString( obj ); - OnLog?.Invoke( str ); - Glue.LogManager.ManagedWarning( str ); - } - - public void Error( object? obj ) - { - string str = GetString( obj ); - OnLog?.Invoke( str ); - Glue.LogManager.ManagedError( str ); - } + public void Trace( object? obj ) => Log( obj, NativeEngine.GetLogManager().ManagedTrace ); + public void Info( object? obj ) => Log( obj, NativeEngine.GetLogManager().ManagedInfo ); + public void Warning( object? obj ) => Log( obj, NativeEngine.GetLogManager().ManagedWarning ); + public void Error( object? obj ) => Log( obj, NativeEngine.GetLogManager().ManagedError ); public struct LogEntry { @@ -60,7 +46,8 @@ public struct LogEntry public List GetHistory() { - var logHistory = Glue.LogManager.GetLogHistory(); + var logManager = NativeEngine.GetLogManager(); + var logHistory = logManager.GetLogHistory(); LogEntry[] logEntries = new LogEntry[logHistory.count]; var ptr = logHistory.items; diff --git a/Source/Mocha.Common/Client/Time.cs b/Source/Mocha.Common/Client/Time.cs index 721cd61d..a52eeee0 100644 --- a/Source/Mocha.Common/Client/Time.cs +++ b/Source/Mocha.Common/Client/Time.cs @@ -8,15 +8,19 @@ public static class Time public static List FPSHistory { get; } = new(); - public static void UpdateFrom( float tickDeltaTime ) - { - Delta = tickDeltaTime; - Now = Glue.Engine.GetTime(); + private const int TimeScale = 5; - FPS = Glue.Engine.GetFramesPerSecond().CeilToInt(); + public static void UpdateFrom( float deltaTime ) + { + Delta = deltaTime; + Now = NativeEngine.GetTime(); + FPS = NativeEngine.GetFramesPerSecond().CeilToInt(); FPSHistory.Add( FPS ); - if ( FPSHistory.Count > 512 ) + + if ( FPSHistory.Count > TimeScale / Delta ) + { FPSHistory.RemoveAt( 0 ); + } } } diff --git a/Source/Mocha.Common/Console/ConVar.cs b/Source/Mocha.Common/Console/ConVar.cs index 932c3e35..960e896b 100644 --- a/Source/Mocha.Common/Console/ConVar.cs +++ b/Source/Mocha.Common/Console/ConVar.cs @@ -6,11 +6,11 @@ public abstract class BaseAttribute : Attribute { public required string Name { get; init; } public required CVarFlags Flags { get; init; } - public required string Description { get; init; } - } + public required string Description { get; init; } +} - public class TestAttribute : Attribute - { +public class TestAttribute : Attribute +{ - } +} } diff --git a/Source/Mocha.Common/Console/ConsoleDispatchInfo.cs b/Source/Mocha.Common/Console/ConsoleDispatchInfo.cs index e0f4f66a..e2475ebf 100644 --- a/Source/Mocha.Common/Console/ConsoleDispatchInfo.cs +++ b/Source/Mocha.Common/Console/ConsoleDispatchInfo.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.InteropServices; namespace Mocha.Common.Console; @@ -39,3 +34,11 @@ public struct BoolCVarDispatchInfo public bool oldValue; public bool newValue; } + +[StructLayout( LayoutKind.Sequential )] +public struct IntCVarDispatchInfo +{ + public IntPtr name; + public int oldValue; + public int newValue; +} diff --git a/Source/Mocha.Common/EntityRegistry.cs b/Source/Mocha.Common/Entities/EntityRegistry.cs similarity index 100% rename from Source/Mocha.Common/EntityRegistry.cs rename to Source/Mocha.Common/Entities/EntityRegistry.cs diff --git a/Source/Mocha.Common/Types/IEntity.cs b/Source/Mocha.Common/Entities/IEntity.cs similarity index 99% rename from Source/Mocha.Common/Types/IEntity.cs rename to Source/Mocha.Common/Entities/IEntity.cs index 700f2be0..760336c3 100644 --- a/Source/Mocha.Common/Types/IEntity.cs +++ b/Source/Mocha.Common/Entities/IEntity.cs @@ -4,6 +4,7 @@ public interface IEntity { string Name { get; set; } uint NativeHandle { get; } + Vector3 Position { get; set; } Rotation Rotation { get; set; } Vector3 Scale { get; set; } diff --git a/Source/Mocha.Common/Event/Event.cs b/Source/Mocha.Common/Event/Event.cs index 5dfd94d1..881b6728 100644 --- a/Source/Mocha.Common/Event/Event.cs +++ b/Source/Mocha.Common/Event/Event.cs @@ -46,16 +46,28 @@ public static void RegisterStatics() public static void Run( string name, params object[] parameters ) { - s_events.ForEach( e => + s_events.ToList().ForEach( e => { if ( e.Name == name ) e.Method?.Invoke( e.Object, parameters ); } ); } + public static void Run( Assembly targetAssembly, string name ) + { + s_events.ToList().ForEach( e => + { + if ( e.Object.GetType().Assembly != targetAssembly ) + return; + + if ( e.Name == name ) + e.Method?.Invoke( e.Object, null ); + } ); + } + public static void Run( string name ) { - s_events.ForEach( e => + s_events.ToList().ForEach( e => { if ( e.Name == name ) e.Method?.Invoke( e.Object, null ); diff --git a/Source/Mocha.Common/Event/Events/Generic.cs b/Source/Mocha.Common/Event/Events/Generic.cs new file mode 100644 index 00000000..7f1e7b92 --- /dev/null +++ b/Source/Mocha.Common/Event/Events/Generic.cs @@ -0,0 +1,10 @@ +namespace Mocha.Common; + +partial class Event +{ + public class TickAttribute : EventAttribute + { + public const string Name = "Event.Tick"; + public TickAttribute() : base( Name ) { } + } +} diff --git a/Source/Mocha.Common/Global.cs b/Source/Mocha.Common/Global/Global.cs similarity index 78% rename from Source/Mocha.Common/Global.cs rename to Source/Mocha.Common/Global/Global.cs index 88db35cd..41a903b5 100644 --- a/Source/Mocha.Common/Global.cs +++ b/Source/Mocha.Common/Global/Global.cs @@ -9,7 +9,7 @@ namespace Mocha.Common; public static class Global { - public static ILogger Log { get; set; } - public static bool IsClient { get; } public static UnmanagedArgs UnmanagedArgs { get; set; } + public static Glue.Root NativeEngine { get; set; } = null!; + public static ILogger Log { get; set; } = null!; } diff --git a/Source/Mocha.Common/Global/GlobalVars.cs b/Source/Mocha.Common/Global/GlobalVars.cs new file mode 100644 index 00000000..a277527d --- /dev/null +++ b/Source/Mocha.Common/Global/GlobalVars.cs @@ -0,0 +1,8 @@ +namespace Mocha.Common; + +public static class Core +{ + public static bool IsServer { get; set; } + public static bool IsClient { get; set; } + public static int TickRate { get; set; } +} diff --git a/Source/Mocha.Common/IGame.cs b/Source/Mocha.Common/IGame.cs index b8ad91a7..359be959 100644 --- a/Source/Mocha.Common/IGame.cs +++ b/Source/Mocha.Common/IGame.cs @@ -5,6 +5,14 @@ public interface IGame void Startup(); void Shutdown(); + /// + /// Called every frame on the client. + /// Note that there is nothing for this on the server, because servers don't render anything. + /// void FrameUpdate(); + + /// + /// Called every tick on the client and the server. + /// void Update(); } diff --git a/Source/Mocha.Common/Input/Input.cs b/Source/Mocha.Common/Input/Input.cs index 8586cb35..e19d3f89 100644 --- a/Source/Mocha.Common/Input/Input.cs +++ b/Source/Mocha.Common/Input/Input.cs @@ -2,24 +2,26 @@ public static partial class Input { - public static bool Left => Glue.Input.IsButtonDown( 1 ); - public static bool Middle => Glue.Input.IsButtonDown( 2 ); - public static bool Right => Glue.Input.IsButtonDown( 3 ); + private static Glue.InputManager NativeInput => NativeEngine.GetInputManager(); - public static bool Button4 => Glue.Input.IsButtonDown( 4 ); - public static bool Button5 => Glue.Input.IsButtonDown( 5 ); + public static bool Left => NativeInput.IsButtonDown( 1 ); + public static bool Middle => NativeInput.IsButtonDown( 2 ); + public static bool Right => NativeInput.IsButtonDown( 3 ); + + public static bool Button4 => NativeInput.IsButtonDown( 4 ); + public static bool Button5 => NativeInput.IsButtonDown( 5 ); // TODO: [ConVar.Archive( "mouse_sensitivity", 2.0f, "Player mouse look sensitivity" )] public static float MouseSensitivity { get; set; } = 2.5f; - public static Vector2 MousePosition => Glue.Input.GetMousePosition(); - public static Vector2 MouseDelta => Glue.Input.GetMouseDelta(); + public static Vector2 MousePosition => NativeInput.GetMousePosition(); + public static Vector2 MouseDelta => NativeInput.GetMouseDelta(); public static Rotation Rotation { get; private set; } = Rotation.Identity; public static Vector3 Direction { get; private set; } - private static bool IsKeyDown( InputButton key ) => Glue.Input.IsKeyDown( (int)key ); + private static bool IsKeyDown( InputButton key ) => NativeInput.IsKeyDown( (int)key ); public static bool Jump => IsKeyDown( InputButton.KeySpace ); public static bool Crouch => IsKeyDown( InputButton.KeyControl ); diff --git a/Source/Mocha.Common/Types/Vector3.cs b/Source/Mocha.Common/Types/Vector3.cs index e50351d0..f64ececc 100644 --- a/Source/Mocha.Common/Types/Vector3.cs +++ b/Source/Mocha.Common/Types/Vector3.cs @@ -48,8 +48,8 @@ public float Z public readonly float Length => _internalVector.Length(); public readonly float LengthSquared => _internalVector.LengthSquared(); - public readonly Vector3 Normal => (Length == 0) ? new( 0 ) : (this / Length); + public void Normalize() { this = Normal; diff --git a/Source/Mocha.Common/Utils/Glue/INativeGlue.cs b/Source/Mocha.Common/Utils/Glue/INativeGlue.cs index 0eaf8c2c..2ac567e9 100644 --- a/Source/Mocha.Common/Utils/Glue/INativeGlue.cs +++ b/Source/Mocha.Common/Utils/Glue/INativeGlue.cs @@ -2,5 +2,5 @@ public interface INativeGlue { - public IntPtr NativePtr { get; } + public IntPtr NativePtr { get; set; } } diff --git a/Source/Mocha.Common/Utils/Glue/ManagedCallbackDispatchInfo.cs b/Source/Mocha.Common/Utils/Glue/ManagedCallbackDispatchInfo.cs new file mode 100644 index 00000000..e8023359 --- /dev/null +++ b/Source/Mocha.Common/Utils/Glue/ManagedCallbackDispatchInfo.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Mocha.Common; + +[StructLayout( LayoutKind.Sequential )] +public struct ManagedCallbackDispatchInfo +{ + public uint handle; + public int argsSize; + public IntPtr args; +}; diff --git a/Source/Mocha.Common/Utils/HexDump.cs b/Source/Mocha.Common/Utils/HexDump.cs new file mode 100644 index 00000000..70bc012b --- /dev/null +++ b/Source/Mocha.Common/Utils/HexDump.cs @@ -0,0 +1,53 @@ +using System.Text; + +namespace Mocha.Common; + +public class HexDump +{ + public static string Dump( byte[] bytes, int bytesPerLine ) + { + StringBuilder sb = new StringBuilder(); + int offset = 0; + + while ( offset < bytes.Length ) + { + int remainingBytes = bytes.Length - offset; + int lineBytes = Math.Min( bytesPerLine, remainingBytes ); + + sb.AppendFormat( "{0:x8} ", offset ); + + for ( int i = 0; i < lineBytes; i++ ) + { + sb.AppendFormat( "{0:x2} ", bytes[offset + i] ); + } + + if ( lineBytes < bytesPerLine ) + { + sb.Append( new string( ' ', (bytesPerLine - lineBytes) * 3 ) ); + } + + sb.Append( "|" ); + + for ( int i = 0; i < lineBytes; i++ ) + { + char c = (char)bytes[offset + i]; + + // If char isn't a letter, symbol, or number, replace it with a dot. + if ( (int)c > 32 && (int)c < 127 ) + { + sb.Append( c ); + } + else + { + sb.Append( "." ); + } + } + + sb.AppendLine( "|" ); + + offset += lineBytes; + } + + return sb.ToString(); + } +} diff --git a/Source/Mocha.Editor/Editor/ConsoleOverlay.cs b/Source/Mocha.Editor/Editor/ConsoleOverlay.cs index ecd6f3f1..7bee86b7 100644 --- a/Source/Mocha.Editor/Editor/ConsoleOverlay.cs +++ b/Source/Mocha.Editor/Editor/ConsoleOverlay.cs @@ -10,17 +10,18 @@ public static void Render() // Setup invisible window // ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); - ImGui.SetNextWindowSize( new Vector2( 0 ) ); - ImGui.PushStyleColor( ImGuiCol.WindowBg, Theme.Gray.ToBackground( 0.75f ) ); - ImGui.PushStyleColor( ImGuiCol.Border, Theme.Transparent ); - ImGui.PushStyleVar( ImGuiStyleVar.WindowRounding, 0 ); - - if ( ImGui.Begin( "consoleoverlay", ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoTitleBar ) ) + if ( ImGuiX.BeginOverlay( "ConsoleOverlay" ) ) { + Vector2 workPos = ImGui.GetMainViewport().WorkPos; + Vector2 workSize = ImGui.GetMainViewport().WorkSize; + Vector2 windowSize = ImGui.GetWindowSize(); + + ImGui.SetWindowPos( new Vector2( workPos.X + workSize.X - windowSize.X - 16, workPos.Y + workSize.Y - windowSize.Y - 16 ) ); + var logEntries = Log.GetHistory().TakeLast( Count ).ToArray(); - for ( int i = 0; i < Count; ++i ) + for ( int i = 0; i < logEntries.Length; ++i ) { var logEntry = logEntries[i]; var alpha = i / (float)Count; @@ -43,13 +44,6 @@ public static void Render() } } - var windowSize = ImGui.GetWindowSize(); - var windowPos = ImGui.GetMainViewport().WorkPos + new System.Numerics.Vector2( ImGui.GetMainViewport().WorkSize.X - windowSize.X, ImGui.GetMainViewport().WorkSize.Y - windowSize.Y ); - ImGui.SetWindowPos( windowPos ); - ImGui.End(); - - ImGui.PopStyleColor( 2 ); - ImGui.PopStyleVar(); } } diff --git a/Source/Mocha.Editor/Editor/Editor.cs b/Source/Mocha.Editor/Editor/Editor.cs index e8deb42f..452693a6 100644 --- a/Source/Mocha.Editor/Editor/Editor.cs +++ b/Source/Mocha.Editor/Editor/Editor.cs @@ -10,7 +10,9 @@ public class Editor { new ConsoleWindow(), new BrowserWindow(), - new InspectorWindow() + new InspectorWindow(), + + new NetworkingWindow() }; public static void Draw() @@ -94,14 +96,67 @@ private static void DrawStatusBar() private static void DrawPerformanceOverlay() { - if ( ImGuiX.BeginOverlay( "Time" ) ) + if ( ImGuiX.BeginOverlay( "PerformanceOverlay" ) ) { - var gpuName = ImGuiX.GetGPUName(); + Vector2 workPos = ImGui.GetMainViewport().WorkPos; + Vector2 workSize = ImGui.GetMainViewport().WorkSize; + Vector2 windowSize = ImGui.GetWindowSize(); + + ImGui.SetWindowPos( new Vector2( workPos.X + workSize.X - windowSize.X - 16, workPos.Y + workSize.Y - windowSize.Y - 128 - 16 ) ); + + var cursorPos = ImGui.GetCursorPos(); + void DrawProperty( string name, string value ) + { + ImGuiX.TextBold( name ); + ImGui.SameLine(); + + var textWidth = ImGui.CalcTextSize( value ).X; + ImGui.SetCursorPosX( cursorPos.X + 128 - textWidth ); + ImGui.Text( value ); + } + + { + var left = $"{Time.FPS} FPS"; + var right = $"{Time.Delta * 1000f:F0}ms"; + + ImGuiX.TextSubheading( left ); + ImGui.SameLine(); + + var textWidth = ImGui.CalcTextSize( right ).X * 1.15f; + ImGui.SetCursorPosX( cursorPos.X + 128 - textWidth ); + ImGui.SetCursorPosY( cursorPos.Y ); + ImGuiX.TextSubheading( right ); + } + + var fpsHistory = Time.FPSHistory.Select( x => (float)x ).ToArray(); + var scaleMax = fpsHistory.Max(); + + ImGui.PushStyleColor( ImGuiCol.FrameBg, Vector4.Zero ); + ImGui.PushStyleVar( ImGuiStyleVar.FramePadding, new Vector2( 0, 0 ) ); + ImGui.PlotHistogram( "##FrameTimes", ref fpsHistory[0], Time.FPSHistory.Count, 0, "", 0f, scaleMax, new Vector2( 128f, 32 ) ); + ImGui.PopStyleVar(); + ImGui.PopStyleColor(); + + ImGuiX.Separator( new Vector4( 1, 1, 1, 0.05f ) ); + + var min = fpsHistory.Min(); + DrawProperty( $"Min", $"{min:F0}fps" ); + var max = fpsHistory.Max(); + DrawProperty( $"Max", $"{max:F0}fps" ); + var avg = fpsHistory.Average(); + DrawProperty( $"Avg", $"{avg:F0}fps" ); + + ImGuiX.Separator( new Vector4( 1, 1, 1, 0.05f ) ); + + DrawProperty( $"Elapsed time", $"{Time.Now:F0}s" ); + DrawProperty( $"Current tick", $"{NativeEngine.GetCurrentTick():F0}" ); + DrawProperty( $"Tick rate", $"{Core.TickRate}" ); + + ImGuiX.Separator( new Vector4( 1, 1, 1, 0.05f ) ); - ImGui.Text( $"GPU: {gpuName}" ); - ImGui.Text( $"FPS: {Time.FPS}" ); - ImGui.Text( $"Current time: {Time.Now}" ); - ImGui.Text( $"Frame time: {(Time.Delta * 1000f).CeilToInt()}ms" ); + DrawProperty( $"Ping", $"{0}ms" ); + DrawProperty( $"Jitter", $"{0}ms" ); + DrawProperty( $"Loss", $"{0}" ); } ImGui.End(); diff --git a/Source/Mocha.Editor/Editor/ImGuiX.cs b/Source/Mocha.Editor/Editor/ImGuiX.cs index 5d22a04a..31e91902 100644 --- a/Source/Mocha.Editor/Editor/ImGuiX.cs +++ b/Source/Mocha.Editor/Editor/ImGuiX.cs @@ -2,6 +2,8 @@ public static class ImGuiX { + private static Glue.EditorManager NativeEditor => NativeEngine.GetEditorManager(); + public static void SeparatorH() { ImGui.Dummy( new Vector2( 0, 4 ) ); @@ -48,32 +50,32 @@ public static bool BeginWindow( string name, ref bool isOpen ) public static void TextMonospace( string text ) { - Glue.Editor.TextMonospace( text ); + NativeEditor.TextMonospace( text ); } public static void TextLight( string text ) { - Glue.Editor.TextLight( text ); + NativeEditor.TextLight( text ); } public static void TextBold( string text ) { - Glue.Editor.TextBold( text ); + NativeEditor.TextBold( text ); } public static void TextHeading( string text ) { - Glue.Editor.TextHeading( text ); + NativeEditor.TextHeading( text ); } public static void TextSubheading( string text ) { - Glue.Editor.TextSubheading( text ); + NativeEditor.TextSubheading( text ); } public static bool BeginMainStatusBar() { - return Glue.Editor.BeginMainStatusBar(); + return NativeEditor.BeginMainStatusBar(); } public static void EndMainStatusBar() @@ -86,27 +88,27 @@ public static bool BeginOverlay( string name ) { ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); + ImGui.SetNextWindowBgAlpha( 0.5f ); + ImGui.PushStyleVar( ImGuiStyleVar.WindowBorderSize, 0 ); + bool b = ImGui.Begin( name, ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoInputs ); if ( b ) - { - Vector2 workPos = ImGui.GetMainViewport().WorkPos; - - ImGui.SetWindowPos( new Vector2( workPos.X + 16, workPos.Y + 16 ) ); ImGui.SetWindowSize( new Vector2( -1, -1 ) ); - } + + ImGui.PopStyleVar(); return b; } public static string GetGPUName() { - return Glue.Editor.GetGPUName(); + return NativeEditor.GetGPUName(); } public static void RenderViewDropdown() { - Glue.Editor.RenderViewDropdown(); + NativeEditor.RenderViewDropdown(); } public static string Align( string str ) => str.PadRight( 16, ' ' ); @@ -135,12 +137,12 @@ public static bool ImageButton( Texture texture, Vector2 size ) public static void Image( Texture texture, Vector2 size ) { - Glue.Editor.Image( texture.NativeTexture, texture.Width, texture.Height, (int)size.X, (int)size.Y ); + NativeEditor.Image( texture.NativeTexture, texture.Width, texture.Height, (int)size.X, (int)size.Y ); } public static void Image( Texture texture, Vector2 size, Vector4 tint ) { - Glue.Editor.Image( texture.NativeTexture, texture.Width, texture.Height, (int)size.X, (int)size.Y ); + NativeEditor.Image( texture.NativeTexture, texture.Width, texture.Height, (int)size.X, (int)size.Y ); } public static void Image( string texturePath, Vector2 size ) @@ -153,10 +155,12 @@ public static void Image( string texture, Vector2 size, Vector4 tint ) Image( Texture.FromCache( texture ), size, tint ); } - public static void Separator() + public static void Separator( Vector4? _color = null ) { + var color = _color ?? new Vector4( 0.28f, 0.28f, 0.28f, 0.29f ); + ImGui.Dummy( new( 0, 4 ) ); - ImGui.PushStyleColor( ImGuiCol.Separator, new System.Numerics.Vector4( 0.28f, 0.28f, 0.28f, 0.29f ) ); + ImGui.PushStyleColor( ImGuiCol.Separator, color ); ImGui.Separator(); ImGui.PopStyleColor(); ImGui.Dummy( new( 0, 4 ) ); diff --git a/Source/Mocha.Editor/Editor/Notifications.cs b/Source/Mocha.Editor/Editor/Notifications.cs index ab62e196..a01484d4 100644 --- a/Source/Mocha.Editor/Editor/Notifications.cs +++ b/Source/Mocha.Editor/Editor/Notifications.cs @@ -14,7 +14,7 @@ private static void DrawNotifications() ImGuiWindowFlags.NoMove; const float padding = 24.0f; - const float margin = 16.0f; + const float margin = 8.0f; var viewport = ImGui.GetMainViewport(); var workPos = viewport.WorkPos; @@ -28,6 +28,7 @@ private static void DrawNotifications() float y = 0; var notifications = Common.Notify.Notifications.ToArray(); + for ( int i = 0; i < notifications.Length; i++ ) { var notification = notifications[i]; @@ -52,7 +53,7 @@ private static void DrawNotifications() ImGui.SetNextWindowSize( new System.Numerics.Vector2( 0, 0 ) ); ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); - if ( ImGui.Begin( $"##{notification.GetHashCode()}_overlay", windowFlags ) ) + if ( ImGui.Begin( $"##notification_{i}_overlay", windowFlags ) ) { ImGui.PushStyleColor( ImGuiCol.Text, new Vector4( 1, 1, 1, alpha ) ); diff --git a/Source/Mocha.Editor/Editor/Windows/NetworkingWindow.cs b/Source/Mocha.Editor/Editor/Windows/NetworkingWindow.cs new file mode 100644 index 00000000..342d1ad8 --- /dev/null +++ b/Source/Mocha.Editor/Editor/Windows/NetworkingWindow.cs @@ -0,0 +1,20 @@ +using Mocha.Editor; + +[Title( "Networking" )] +public class NetworkingWindow : EditorWindow +{ + public NetworkingWindow() + { + isVisible = false; + } + + public override void Draw() + { + if ( ImGuiX.BeginWindow( name: $"Networking", ref isVisible ) ) + { + + } + + ImGui.End(); + } +} diff --git a/Source/Mocha.Editor/Game.cs b/Source/Mocha.Editor/Game.cs index 43e1a394..671d6828 100644 --- a/Source/Mocha.Editor/Game.cs +++ b/Source/Mocha.Editor/Game.cs @@ -17,7 +17,7 @@ public void Shutdown() public void Startup() { - ImGuiNative.igSetCurrentContext( Glue.Editor.GetContextPointer() ); + ImGuiNative.igSetCurrentContext( NativeEngine.GetEditorManager().GetContextPointer() ); } public void Update() diff --git a/Source/Mocha.Engine/AssemblyInfo.cs b/Source/Mocha.Engine/AssemblyInfo.cs new file mode 100644 index 00000000..d8e00fc9 --- /dev/null +++ b/Source/Mocha.Engine/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible( false )] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid( "1ffcb1b0-8fd9-465c-8c90-bc4c3069e6e1" )] +[assembly: InternalsVisibleTo( "Mocha.Tests" )] diff --git a/Source/Mocha.Engine/BaseGame.cs b/Source/Mocha.Engine/BaseGame.cs index 6edbe124..6c719c99 100644 --- a/Source/Mocha.Engine/BaseGame.cs +++ b/Source/Mocha.Engine/BaseGame.cs @@ -46,32 +46,55 @@ private void TryCallMethodOnEntity( string methodName ) } ); } - public virtual void FrameUpdate() + public void FrameUpdate() { UIManager.Instance.Render(); TryCallMethodOnEntity( "FrameUpdate" ); } - public virtual void Update() + public void Update() { - // HACK: Clear DebugOverlay here because doing it - // per-frame doesn't play nice with tick-based - // entries (needs fix) + if ( Core.IsClient ) + { + // HACK: Clear DebugOverlay here because doing it + // per-frame doesn't play nice with tick-based + // entries (needs fix) - DebugOverlay.screenTextList.Clear(); - DebugOverlay.currentLine = 0; + DebugOverlay.screenTextList.Clear(); + DebugOverlay.currentLine = 0; + } - TryCallMethodOnEntity( "Update" ); + DebugOverlay.ScreenText( $"BaseGame.Update assembly {GetType().Assembly.GetHashCode()}" ); } - public virtual void Shutdown() + public void Shutdown() { + OnShutdown(); } - public virtual void Startup() + public void Startup() { + OnStartup(); + } + + #region "Public API" + /// + /// Called on the server when the game starts up + /// + public virtual void OnStartup() + { + + } + + /// + /// Called on the server when the game shuts down + /// + public virtual void OnShutdown() + { + } + #endregion [Event.Game.Hotload] public void OnHotload() diff --git a/Source/Mocha.Engine/DebugOverlay.cs b/Source/Mocha.Engine/DebugOverlay.cs deleted file mode 100644 index eef0e9f7..00000000 --- a/Source/Mocha.Engine/DebugOverlay.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Mocha; - -public struct DebugOverlayText -{ - public Vector2 position; - public string text; - public float time; - - public DebugOverlayText( Vector2 position, string text, float? time = null ) - { - this.position = position; - this.text = text; - this.time = time ?? 0f; - } -} - -/// -/// Public API for -/// -public static partial class DebugOverlay -{ - /// - /// This allows us to buffer debug overlay commands in advance. - /// - public readonly static List screenTextList = new(); - public static int currentLine = 0; - - public static void ScreenText( int line, object obj ) - { - line++; - - var lineHeight = 16.0f; - screenTextList.Add( new( new Vector2( 32, lineHeight * line ), obj.ToString()! ) ); - } - - public static void ScreenText( object obj ) - { - currentLine++; - - var lineHeight = 16.0f; - screenTextList.Add( new( new Vector2( 32, lineHeight * currentLine ), obj.ToString()! ) ); - } -} diff --git a/Source/Mocha.Engine/TraceResult.cs b/Source/Mocha.Engine/Physics/TraceResult.cs similarity index 100% rename from Source/Mocha.Engine/TraceResult.cs rename to Source/Mocha.Engine/Physics/TraceResult.cs diff --git a/Source/Mocha.Engine/Render/Assets/Model.cs b/Source/Mocha.Engine/Render/Assets/Model.cs index 234fc218..37e7b4fd 100644 --- a/Source/Mocha.Engine/Render/Assets/Model.cs +++ b/Source/Mocha.Engine/Render/Assets/Model.cs @@ -39,12 +39,7 @@ public Model( Vertex[] vertices, Material material ) [Icon( FontAwesome.Cube ), Title( "Model" )] public partial class Model : Asset, IModel where T : struct { - public Glue.Model NativeModel { get; set; } - - public Model() - { - NativeModel = new(); - } + public Glue.Model NativeModel { get; } = new(); protected void AddMesh( T[] vertices, Material material ) { @@ -59,5 +54,6 @@ protected void AddMesh( T[] vertices, uint[] indices, Material material ) public interface IModel { - Glue.Model NativeModel { get; set; } + string Path { get; set; } + Glue.Model NativeModel { get; } } diff --git a/Source/Mocha.Engine/Runtimes/Facepunch.Steamworks.Win64.dll b/Source/Mocha.Engine/Runtimes/Facepunch.Steamworks.Win64.dll new file mode 100644 index 00000000..bceb9103 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/Facepunch.Steamworks.Win64.dll differ diff --git a/Source/Mocha.Engine/Runtimes/GameNetworkingSockets.dll b/Source/Mocha.Engine/Runtimes/GameNetworkingSockets.dll new file mode 100644 index 00000000..779ef51b Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/GameNetworkingSockets.dll differ diff --git a/Source/Mocha.Engine/Runtimes/MessagePack.Annotations.dll b/Source/Mocha.Engine/Runtimes/MessagePack.Annotations.dll new file mode 100644 index 00000000..afe839d4 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/MessagePack.Annotations.dll differ diff --git a/Source/Mocha.Engine/Runtimes/MessagePack.dll b/Source/Mocha.Engine/Runtimes/MessagePack.dll new file mode 100644 index 00000000..731ebef4 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/MessagePack.dll differ diff --git a/Source/Mocha.Engine/Runtimes/Microsoft.Build.Framework.dll b/Source/Mocha.Engine/Runtimes/Microsoft.Build.Framework.dll deleted file mode 100644 index b84100aa..00000000 Binary files a/Source/Mocha.Engine/Runtimes/Microsoft.Build.Framework.dll and /dev/null differ diff --git a/Source/Mocha.Engine/Runtimes/Microsoft.Build.Locator.dll b/Source/Mocha.Engine/Runtimes/Microsoft.Build.Locator.dll deleted file mode 100644 index 36fc52c4..00000000 Binary files a/Source/Mocha.Engine/Runtimes/Microsoft.Build.Locator.dll and /dev/null differ diff --git a/Source/Mocha.Engine/Runtimes/Microsoft.Build.dll b/Source/Mocha.Engine/Runtimes/Microsoft.Build.dll deleted file mode 100644 index 486f6100..00000000 Binary files a/Source/Mocha.Engine/Runtimes/Microsoft.Build.dll and /dev/null differ diff --git a/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.CSharp.Workspaces.dll b/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.CSharp.Workspaces.dll new file mode 100644 index 00000000..dc218f98 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.CSharp.Workspaces.dll differ diff --git a/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.Workspaces.dll b/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.Workspaces.dll new file mode 100644 index 00000000..8dec4418 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/Microsoft.CodeAnalysis.Workspaces.dll differ diff --git a/Source/Mocha.Engine/Runtimes/Newtonsoft.Json.dll b/Source/Mocha.Engine/Runtimes/Newtonsoft.Json.dll new file mode 100644 index 00000000..8ba89bf3 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/Newtonsoft.Json.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Common.dll b/Source/Mocha.Engine/Runtimes/NuGet.Common.dll new file mode 100644 index 00000000..e09cbe54 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Common.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Configuration.dll b/Source/Mocha.Engine/Runtimes/NuGet.Configuration.dll new file mode 100644 index 00000000..246c8839 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Configuration.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Frameworks.dll b/Source/Mocha.Engine/Runtimes/NuGet.Frameworks.dll new file mode 100644 index 00000000..d78c4786 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Frameworks.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Packaging.dll b/Source/Mocha.Engine/Runtimes/NuGet.Packaging.dll new file mode 100644 index 00000000..33779601 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Packaging.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Protocol.dll b/Source/Mocha.Engine/Runtimes/NuGet.Protocol.dll new file mode 100644 index 00000000..23749ef4 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Protocol.dll differ diff --git a/Source/Mocha.Engine/Runtimes/NuGet.Versioning.dll b/Source/Mocha.Engine/Runtimes/NuGet.Versioning.dll new file mode 100644 index 00000000..8b301668 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/NuGet.Versioning.dll differ diff --git a/Source/Mocha.Engine/Runtimes/System.Composition.AttributedModel.dll b/Source/Mocha.Engine/Runtimes/System.Composition.AttributedModel.dll new file mode 100644 index 00000000..d37283b1 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/System.Composition.AttributedModel.dll differ diff --git a/Source/Mocha.Engine/Runtimes/System.Composition.Hosting.dll b/Source/Mocha.Engine/Runtimes/System.Composition.Hosting.dll new file mode 100644 index 00000000..c67f1c02 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/System.Composition.Hosting.dll differ diff --git a/Source/Mocha.Engine/Runtimes/System.Composition.Runtime.dll b/Source/Mocha.Engine/Runtimes/System.Composition.Runtime.dll new file mode 100644 index 00000000..2a4b38c9 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/System.Composition.Runtime.dll differ diff --git a/Source/Mocha.Engine/Runtimes/System.Composition.TypedParts.dll b/Source/Mocha.Engine/Runtimes/System.Composition.TypedParts.dll new file mode 100644 index 00000000..7c0c780d Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/System.Composition.TypedParts.dll differ diff --git a/Source/Mocha.Engine/Runtimes/libcrypto-3-x64.dll b/Source/Mocha.Engine/Runtimes/libcrypto-3-x64.dll new file mode 100644 index 00000000..f855ff05 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/libcrypto-3-x64.dll differ diff --git a/Source/Mocha.Engine/Runtimes/libprotobuf.dll b/Source/Mocha.Engine/Runtimes/libprotobuf.dll new file mode 100644 index 00000000..911305f5 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/libprotobuf.dll differ diff --git a/Source/Mocha.Engine/Runtimes/steam_api.dll b/Source/Mocha.Engine/Runtimes/steam_api.dll new file mode 100644 index 00000000..6e0f4401 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/steam_api.dll differ diff --git a/Source/Mocha.Engine/Runtimes/steam_api64.dll b/Source/Mocha.Engine/Runtimes/steam_api64.dll new file mode 100644 index 00000000..ad13f2b6 Binary files /dev/null and b/Source/Mocha.Engine/Runtimes/steam_api64.dll differ diff --git a/Source/Mocha.Engine/UI/UIManager.cs b/Source/Mocha.Engine/UI/UIManager.cs index e9667e6f..259a3ecd 100644 --- a/Source/Mocha.Engine/UI/UIManager.cs +++ b/Source/Mocha.Engine/UI/UIManager.cs @@ -41,7 +41,7 @@ public void LoadTemplate( string? file = null ) if ( shouldLoad ) { - Screen.UpdateFrom( Glue.Editor.GetRenderSize() ); + Screen.UpdateFrom( NativeEngine.GetRenderSize() ); RootPanel = Template.FromFile( _renderer, _templatePath ); _isDirty = true; } diff --git a/Source/Mocha.Engine/World/Base/BaseEntity.cs b/Source/Mocha.Engine/World/Base/BaseEntity.cs index 013dbc90..b3dd640a 100644 --- a/Source/Mocha.Engine/World/Base/BaseEntity.cs +++ b/Source/Mocha.Engine/World/Base/BaseEntity.cs @@ -10,6 +10,9 @@ public class BaseEntity : IEntity [HideInInspector] public uint NativeHandle { get; protected set; } + [HideInInspector] + private Glue.BaseEntity NativeEntity => NativeEngine.GetEntityManager().GetBaseEntity( NativeHandle ); + public bool IsValid() { return true; @@ -18,39 +21,39 @@ public bool IsValid() [Category( "Transform" )] public Vector3 Scale { - get => Glue.Entities.GetScale( NativeHandle ); - set => Glue.Entities.SetScale( NativeHandle, value ); + get => NativeEntity.GetScale(); + set => NativeEntity.SetScale( value ); } [Category( "Transform" )] public Vector3 Position { - get => Glue.Entities.GetPosition( NativeHandle ); - set => Glue.Entities.SetPosition( NativeHandle, value ); + get => NativeEntity.GetPosition(); + set => NativeEntity.SetPosition( value ); } [Category( "Transform" )] public Rotation Rotation { - get => Glue.Entities.GetRotation( NativeHandle ); - set => Glue.Entities.SetRotation( NativeHandle, value ); + get => NativeEntity.GetRotation(); + set => NativeEntity.SetRotation( value ); } [HideInInspector] public string Name { - get => Glue.Entities.GetName( NativeHandle ); - set => Glue.Entities.SetName( NativeHandle, value ); + get => NativeEntity.GetName(); + set => NativeEntity.SetName( value ); } public bool IsViewModel { - set => Glue.Entities.SetViewmodel( NativeHandle, value ); + set => NativeEntity.SetViewmodel( value ); } public bool IsUI { - set => Glue.Entities.SetUI( NativeHandle, value ); + set => NativeEntity.SetUI( value ); } public BaseEntity() @@ -66,6 +69,9 @@ public BaseEntity() var displayInfo = DisplayInfo.For( this ); Name = $"[{displayInfo.Category}] {displayInfo.Name} {NativeHandle}"; + Event.Register( this ); + Log.Info( $"Spawning entity {Name} on {(Core.IsClient ? "client" : "server")}" ); + Spawn(); } @@ -75,7 +81,7 @@ protected virtual void Spawn() protected virtual void CreateNativeEntity() { - NativeHandle = Glue.Entities.CreateBaseEntity(); + NativeHandle = NativeEngine.CreateBaseEntity(); } public virtual void Update() { } diff --git a/Source/Mocha.Engine/World/Base/ModelEntity.cs b/Source/Mocha.Engine/World/Base/ModelEntity.cs index 7eaa2ecf..70e78eae 100644 --- a/Source/Mocha.Engine/World/Base/ModelEntity.cs +++ b/Source/Mocha.Engine/World/Base/ModelEntity.cs @@ -1,58 +1,86 @@ -using System.Runtime.InteropServices; - -namespace Mocha; +namespace Mocha; [Category( "World" ), Title( "Model Entity" ), Icon( FontAwesome.Cube )] public partial class ModelEntity : BaseEntity { + // This is a stop-gap solution until we have a proper physics body implementation + + public struct Physics + { + public string PhysicsModelPath { get; set; } + } + + [HideInInspector] + private Glue.ModelEntity NativeModelEntity => NativeEngine.GetEntityManager().GetModelEntity( NativeHandle ); + [Category( "Physics" )] public Vector3 Velocity { - get => Glue.Entities.GetVelocity( NativeHandle ); - set => Glue.Entities.SetVelocity( NativeHandle, value ); + get => NativeModelEntity.GetVelocity(); + set => NativeModelEntity.SetVelocity( value ); } [Category( "Physics" )] public float Mass { - get => Glue.Entities.GetMass( NativeHandle ); - set => Glue.Entities.SetMass( NativeHandle, value ); + get => NativeModelEntity.GetMass(); + set => NativeModelEntity.SetMass( value ); } [Category( "Physics" )] public float Friction { - get => Glue.Entities.GetFriction( NativeHandle ); - set => Glue.Entities.SetFriction( NativeHandle, value ); + get => NativeModelEntity.GetFriction(); + set => NativeModelEntity.SetFriction( value ); } [Category( "Physics" )] public float Restitution { - get => Glue.Entities.GetRestitution( NativeHandle ); - set => Glue.Entities.SetRestitution( NativeHandle, value ); + get => NativeModelEntity.GetRestitution(); + set => NativeModelEntity.SetRestitution( value ); } [Category( "Physics" )] public bool IgnoreRigidbodyRotation { - get => Glue.Entities.GetIgnoreRigidbodyRotation( NativeHandle ); - set => Glue.Entities.SetIgnoreRigidbodyRotation( NativeHandle, value ); + get => NativeModelEntity.GetIgnoreRigidbodyRotation(); + set => NativeModelEntity.SetIgnoreRigidbodyRotation( value ); } [Category( "Physics" )] public bool IgnoreRigidbodyPosition { - get => Glue.Entities.GetIgnoreRigidbodyPosition( NativeHandle ); - set => Glue.Entities.SetIgnoreRigidbodyPosition( NativeHandle, value ); + get => NativeModelEntity.GetIgnoreRigidbodyPosition(); + set => NativeModelEntity.SetIgnoreRigidbodyPosition( value ); } + private string _modelPath; + [Category( "Rendering" )] public IModel Model { - set => Glue.Entities.SetModel( NativeHandle, value.NativeModel ); + set + { + NativeModelEntity.SetModel( value.NativeModel ); + _modelPath = value.Path; + } } + [Category( "Rendering" )] + public string ModelPath + { + get => _modelPath; + set + { + _modelPath = value; + Model = new Model( value ); + } + } + + [HideInInspector] + public Physics PhysicsSetup { get; set; } + public ModelEntity() { } @@ -64,22 +92,37 @@ public ModelEntity( string path ) protected override void CreateNativeEntity() { - NativeHandle = Glue.Entities.CreateModelEntity(); + NativeHandle = NativeEngine.CreateModelEntity(); } public void SetCubePhysics( Vector3 bounds, bool isStatic ) { - Glue.Entities.SetCubePhysics( NativeHandle, bounds, isStatic ); + // TODO: Predicted physics + if ( !Core.IsServer ) + return; + + NativeModelEntity.SetCubePhysics( bounds, isStatic ); } public void SetSpherePhysics( float radius, bool isStatic ) { - Glue.Entities.SetSpherePhysics( NativeHandle, radius, isStatic ); + // TODO: Predicted physics + if ( !Core.IsServer ) + return; + + NativeModelEntity.SetSpherePhysics( radius, isStatic ); } // TODO: Replace... public void SetMeshPhysics( string path ) { + PhysicsSetup = new Physics() + { + PhysicsModelPath = path + }; + + Log.Info( $"SetMeshPhysics: {path}" ); + using var _ = new Stopwatch( "Mocha phys model generation" ); var fileBytes = FileSystem.Mounted.ReadAllBytes( path ); var modelFile = Serializer.Deserialize>( fileBytes ); @@ -157,16 +200,6 @@ Vector2 ReadVector2() // Reverse winding order // // vertexList.Reverse(); - - unsafe - { - int vertexStride = Marshal.SizeOf( typeof( Vector3 ) ); - int vertexSize = vertexStride * vertexList.Count; - - fixed ( void* vertexData = vertexList.ToArray() ) - { - Glue.Entities.SetMeshPhysics( NativeHandle, vertexSize, (IntPtr)vertexData ); - } - } + NativeModelEntity.SetMeshPhysics( vertexList.ToInterop() ); } } diff --git a/Source/Mocha.Engine/World/Camera.cs b/Source/Mocha.Engine/World/Camera.cs index 65f41f3b..9be3f3fb 100644 --- a/Source/Mocha.Engine/World/Camera.cs +++ b/Source/Mocha.Engine/World/Camera.cs @@ -4,31 +4,31 @@ public static class Camera { public static Vector3 Position { - get => Glue.Entities.GetCameraPosition(); - set => Glue.Entities.SetCameraPosition( value ); + get => NativeEngine.GetCameraPosition(); + set => NativeEngine.SetCameraPosition( value ); } public static Rotation Rotation { - get => Glue.Entities.GetCameraRotation(); - set => Glue.Entities.SetCameraRotation( value ); + get => NativeEngine.GetCameraRotation(); + set => NativeEngine.SetCameraRotation( value ); } public static float FieldOfView { - get => Glue.Entities.GetCameraFieldOfView(); - set => Glue.Entities.SetCameraFieldOfView( value ); + get => NativeEngine.GetCameraFieldOfView(); + set => NativeEngine.SetCameraFieldOfView( value ); } public static float ZNear { - get => Glue.Entities.GetCameraZNear(); - set => Glue.Entities.SetCameraZNear( value ); + get => NativeEngine.GetCameraZNear(); + set => NativeEngine.SetCameraZNear( value ); } public static float ZFar { - get => Glue.Entities.GetCameraZFar(); - set => Glue.Entities.SetCameraZFar( value ); + get => NativeEngine.GetCameraZFar(); + set => NativeEngine.SetCameraZFar( value ); } } diff --git a/Source/Mocha.Engine/World/Player.cs b/Source/Mocha.Engine/World/Player.cs index 15b60976..c51f6ac8 100644 --- a/Source/Mocha.Engine/World/Player.cs +++ b/Source/Mocha.Engine/World/Player.cs @@ -10,7 +10,7 @@ public class Player : ModelEntity public Ray EyeRay => new Ray( EyePosition, EyeRotation.Forward ); [Category( "Player" )] - public Vector3 PlayerHalfExtents { get; set; } + public Vector3 PlayerBounds { get; set; } [Category( "Player" )] public Vector3 EyePosition { get; set; } diff --git a/Source/Mocha.Engine/World/PlayerController/QuakeWalkController.cs b/Source/Mocha.Engine/World/PlayerController/QuakeWalkController.cs index 6e1526e1..4d8fb2bb 100644 --- a/Source/Mocha.Engine/World/PlayerController/QuakeWalkController.cs +++ b/Source/Mocha.Engine/World/PlayerController/QuakeWalkController.cs @@ -263,7 +263,7 @@ private void WalkMove() public TraceResult TraceBBox( Vector3 start, Vector3 end ) { - return Cast.Ray( start, end ).WithHalfExtents( Player.PlayerHalfExtents ).Ignore( Player ).Run(); + return Cast.Ray( start, end ).WithHalfExtents( Player.PlayerBounds ).Ignore( Player ).Run(); } private void TraceToGround() diff --git a/Source/Mocha.Engine/World/Trace.cs b/Source/Mocha.Engine/World/Trace.cs index bedcd534..7a5f2c82 100644 --- a/Source/Mocha.Engine/World/Trace.cs +++ b/Source/Mocha.Engine/World/Trace.cs @@ -65,7 +65,9 @@ public TraceResult Run() fixed ( void* data = _ignoredEntities.Select( x => x.NativeHandle ).ToArray() ) { traceInfo.ignoredEntityHandles = (IntPtr)data; - var result = Glue.Physics.Trace( traceInfo ); + + var physicsManager = NativeEngine.GetPhysicsManager(); + var result = physicsManager.Trace( traceInfo ); return TraceResult.From( result ); } } diff --git a/Source/Mocha.Engine/World/ViewModel.cs b/Source/Mocha.Engine/World/ViewModel.cs index 1b5744e2..aabff619 100644 --- a/Source/Mocha.Engine/World/ViewModel.cs +++ b/Source/Mocha.Engine/World/ViewModel.cs @@ -46,7 +46,7 @@ protected override void Spawn() public Dictionary Offsets = new() { - { "Base", new( new( 0.4f, 0.12f, -0.18f ) )}, + { "Base", new( new( 0.4f, 0.12f, -0.18f ) ) }, { "Sprint", new( new( 0.02f, 0.0f, 0.07f ), new( 30.89f, -11.25f, -5.6f ) ) }, { "Avoid", new( new( 0.02f, 0.0f, 0.07f ), new( 30.89f, -11.25f, -5.6f ) ) } }; diff --git a/Source/Mocha.FrameworkBench/Mocha.FrameworkBench.cpp b/Source/Mocha.FrameworkBench/Mocha.FrameworkBench.cpp new file mode 100644 index 00000000..2913cfec --- /dev/null +++ b/Source/Mocha.FrameworkBench/Mocha.FrameworkBench.cpp @@ -0,0 +1,175 @@ +// +#include +// +#include +#include + +const double CheckHandleMapSpeed_Single(); +const double CheckArraySpeed_SystemAlloc_Single(); +const double CheckArraySpeed_LinearAlloc_Single(); +const double CheckVectorSpeed_Single(); +const void CheckSpeed( const std::function func ); + +const int g_benchmarkCount = 1000; + +int main() +{ + std::cout << "Running " << g_benchmarkCount << " benchmarks per type..." << std::endl; + std::cout << "--------------------" << std::endl; + std::cout << "[Mocha::HandleMap]" << std::endl; + CheckSpeed( CheckHandleMapSpeed_Single ); + + std::cout << "--------------------" << std::endl; + std::cout << "[Mocha::Array - SystemAllocator]" << std::endl; + CheckSpeed( CheckArraySpeed_SystemAlloc_Single ); + + std::cout << "--------------------" << std::endl; + std::cout << "[Mocha::Array - LinearAllocator]" << std::endl; + CheckSpeed( CheckArraySpeed_LinearAlloc_Single ); + + std::cout << "--------------------" << std::endl; + std::cout << "[std::vector]" << std::endl; + CheckSpeed( CheckVectorSpeed_Single ); + + std::cout << "--------------------" << std::endl; + return 0; +} + +#define SPEED_TEST_BEGIN() + +const void CheckSpeed( const std::function func ) +{ + double totalDurationSeconds = 0.0; + + for ( int i = 0; i < g_benchmarkCount; ++i ) + { + // std::cout << "\tRunning benchmark " << ( i + 1 ) << " of " << g_benchmarkCount << "... " << std::endl; + + double durationSeconds = func(); + // std::cout << "\tTook " << durationSeconds << " seconds" << std::endl; + + totalDurationSeconds += durationSeconds; + } + + double averageDurationSeconds = totalDurationSeconds / static_cast( g_benchmarkCount ); + + // std::cout << "\tAvg. time taken: " << averageDurationSeconds << " seconds" << std::endl; + + const double lookupsPerSecond = 1000000.0 / averageDurationSeconds; + const double millionLookupsPerSecond = lookupsPerSecond / 1000000.0; + std::cout << "\tAvg. lookup speed: " << millionLookupsPerSecond << "M/s" << std::endl; +} + +inline const std::chrono::steady_clock::time_point StartClock() +{ + return std::chrono::high_resolution_clock::now(); +} + +inline const double CalculateDurationSeconds( const std::chrono::steady_clock::time_point start, const int count ) +{ + const auto end = std::chrono::high_resolution_clock::now(); + + // Convert the duration into seconds with high precision + const auto duration = std::chrono::duration_cast( end - start ); + const double durationSeconds = duration.count() / static_cast( count ); + return durationSeconds; +} + +const double CheckArraySpeed_SystemAlloc_Single() +{ + const int count = 1'000'000; + + using Mocha::Array; + using Mocha::SystemAllocator; + + SystemAllocator alloc; + Array array; + array.Init( &alloc, count, count ); + + // Add values to the container + for ( int i = 0; i < count; i++ ) + { + array.Push( i ); + } + + const auto start = StartClock(); + + for ( int i = 0; i < count; i++ ) + { + int _ = array[i]; + } + + return CalculateDurationSeconds( start, count ); +} + +const double CheckArraySpeed_LinearAlloc_Single() +{ + const int count = 1'000'000; + + using Mocha::Array; + using Mocha::LinearAllocator; + + LinearAllocator alloc( sizeof( int ) * count * 4 ); + Array array; + array.Init( &alloc, count, count ); + + // Add values to the container + for ( int i = 0; i < count; i++ ) + { + array.Push( i ); + } + + const auto start = StartClock(); + + for ( int i = 0; i < count; i++ ) + { + int _ = array[i]; + } + + return CalculateDurationSeconds( start, count ); +} + +const double CheckVectorSpeed_Single() +{ + const int count = 1'000'000; + + std::vector array; + + // Add values to the container + for ( int i = 0; i < count; i++ ) + { + array.push_back( i ); + } + + const auto start = StartClock(); + + for ( int i = 0; i < count; i++ ) + { + int _ = array[i]; + } + + return CalculateDurationSeconds( start, count ); +} + +const double CheckHandleMapSpeed_Single() +{ + const int count = 1'000; + + using Mocha::HandleMap; + HandleMap handleMap; + + // Add values to the container + for ( int i = 0; i < count; i++ ) + { + handleMap.Add( i ); + } + + const auto start = StartClock(); + + for ( int i = 0; i < count; i++ ) + { + auto _ = handleMap.Get( i ); + } + + return CalculateDurationSeconds( start, count ); +} \ No newline at end of file diff --git a/Source/Mocha.Host/Entities/baseentity.h b/Source/Mocha.Host/Entities/baseentity.h new file mode 100644 index 00000000..52826da1 --- /dev/null +++ b/Source/Mocha.Host/Entities/baseentity.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include +#include + +enum EntityFlags : int +{ + ENTITY_NONE = 1 << 0, + ENTITY_MANAGED = 1 << 1, + ENTITY_RENDERABLE = 1 << 2, + ENTITY_VIEWMODEL = 1 << 3, + ENTITY_UI = 1 << 4, +}; + +DEFINE_FLAG_OPERATORS( EntityFlags ); + +class Camera; + +class BaseEntity +{ +public: + BaseEntity() + : m_spawnTime( Globals::m_curTick ){}; + + virtual ~BaseEntity() {} + + int m_spawnTime; + + EntityFlags m_flags = ENTITY_NONE; + + std::string m_type = "No type"; + std::string m_name = "Unnamed"; + + Transform m_transformLastFrame = {}; + Transform m_transformCurrentFrame = {}; + + Transform m_transform = {}; + + inline void AddFlag( EntityFlags flags ) { m_flags = m_flags | flags; } + inline void RemoveFlag( EntityFlags flags ) { m_flags = m_flags & ~flags; } + inline bool HasFlag( EntityFlags flag ) { return ( m_flags & flag ) != 0; } + + // + // Managed bindings + // + GENERATE_BINDINGS inline void SetName( const char* name ) { m_name = name; } + GENERATE_BINDINGS inline const char* GetName() { return m_name.c_str(); } + + GENERATE_BINDINGS inline void SetType( const char* type ) { m_type = type; } + GENERATE_BINDINGS inline const char* GetType() { return m_type.c_str(); } + + GENERATE_BINDINGS inline void SetPosition( const Vector3& pos ) { m_transform.position = pos; } + GENERATE_BINDINGS inline Vector3 GetPosition() { return m_transform.position; } + + GENERATE_BINDINGS inline void SetRotation( const Quaternion& rot ) { m_transform.rotation = rot; } + GENERATE_BINDINGS inline Quaternion GetRotation() { return m_transform.rotation; } + + GENERATE_BINDINGS inline void SetScale( const Vector3& scale ) { m_transform.scale = scale; } + GENERATE_BINDINGS inline Vector3 GetScale() { return m_transform.scale; } + + GENERATE_BINDINGS inline void SetViewmodel( bool isViewmodel ) + { + if ( isViewmodel ) + AddFlag( ENTITY_VIEWMODEL ); + else + RemoveFlag( ENTITY_VIEWMODEL ); + } + + GENERATE_BINDINGS inline void SetUI( bool isUI ) + { + if ( isUI ) + AddFlag( ENTITY_UI ); + else + RemoveFlag( ENTITY_UI ); + } +}; diff --git a/Source/Mocha.Host/entitymanager.h b/Source/Mocha.Host/Entities/entitymanager.h similarity index 77% rename from Source/Mocha.Host/entitymanager.h rename to Source/Mocha.Host/Entities/entitymanager.h index 65dab976..8b90b873 100644 --- a/Source/Mocha.Host/entitymanager.h +++ b/Source/Mocha.Host/Entities/entitymanager.h @@ -1,11 +1,13 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include #include -#include -#include #include -#include #include class EntityManager : HandleMap, ISubSystem @@ -27,6 +29,12 @@ class EntityManager : HandleMap, ISubSystem void Startup() override{}; void Shutdown() override{}; + + GENERATE_BINDINGS BaseEntity* GetBaseEntity( uint32_t entityHandle ) { return GetEntity( entityHandle ).get(); } + GENERATE_BINDINGS ModelEntity* GetModelEntity( uint32_t entityHandle ) + { + return GetEntity( entityHandle ).get(); + } }; template diff --git a/Source/Mocha.Host/modelentity.cpp b/Source/Mocha.Host/Entities/modelentity.cpp similarity index 74% rename from Source/Mocha.Host/modelentity.cpp rename to Source/Mocha.Host/Entities/modelentity.cpp index ae1a6abb..a8839e2a 100644 --- a/Source/Mocha.Host/modelentity.cpp +++ b/Source/Mocha.Host/Entities/modelentity.cpp @@ -1,7 +1,7 @@ #include "modelentity.h" -#include -#include +#include +#include void ModelEntity::SetSpherePhysics( float radius, bool isStatic ) { @@ -18,7 +18,7 @@ void ModelEntity::SetSpherePhysics( float radius, bool isStatic ) body.shape.shapeData.radius = radius; body.shape.shapeType = PhysicsShapeType::PHYSICS_SHAPE_SPHERE; - m_physicsHandle = g_physicsManager->AddBody( this, body ); + m_physicsHandle = Globals::m_physicsManager->AddBody( this, body ); } void ModelEntity::SetCubePhysics( Vector3 bounds, bool isStatic ) @@ -36,11 +36,13 @@ void ModelEntity::SetCubePhysics( Vector3 bounds, bool isStatic ) body.shape.shapeData.extents = bounds; body.shape.shapeType = PhysicsShapeType::PHYSICS_SHAPE_BOX; - m_physicsHandle = g_physicsManager->AddBody( this, body ); + m_physicsHandle = Globals::m_physicsManager->AddBody( this, body ); } -void ModelEntity::SetMeshPhysics( std::vector vertices ) +void ModelEntity::SetMeshPhysics( UtilArray interopVertices ) { + std::vector vertices = interopVertices.GetData(); + PhysicsBody body = {}; body.friction = 1.0f; @@ -54,5 +56,5 @@ void ModelEntity::SetMeshPhysics( std::vector vertices ) body.shape.shapeData.vertices = vertices; body.shape.shapeType = PhysicsShapeType::PHYSICS_SHAPE_MESH; - m_physicsHandle = g_physicsManager->AddBody( this, body ); + m_physicsHandle = Globals::m_physicsManager->AddBody( this, body ); } \ No newline at end of file diff --git a/Source/Mocha.Host/Entities/modelentity.h b/Source/Mocha.Host/Entities/modelentity.h new file mode 100644 index 00000000..319e1898 --- /dev/null +++ b/Source/Mocha.Host/Entities/modelentity.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include + +struct PhysicsBody; + +class ModelEntity : public BaseEntity +{ +private: + Model m_model; + + // + // Physics values + // + uint32_t m_physicsHandle = UINT32_MAX; + + Vector3 m_velocity = {}; + float m_friction = 0.5f; + float m_mass = 10.0f; + float m_restitution = 0.5f; + + bool m_ignoreRigidbodyRotation; + bool m_ignoreRigidbodyPosition; + +public: + // If this model has no physics, this function will return UINT32_MAX. + uint32_t GetPhysicsHandle() { return m_physicsHandle; }; + + GENERATE_BINDINGS void SetModel( Model* model ) { m_model = *model; } + GENERATE_BINDINGS Model* GetModel() { return &m_model; } + + // + // Managed bindings + // + GENERATE_BINDINGS void SetSpherePhysics( float radius, bool isStatic ); + GENERATE_BINDINGS void SetCubePhysics( Vector3 bounds, bool isStatic ); + GENERATE_BINDINGS void SetMeshPhysics( UtilArray vertices ); + + GENERATE_BINDINGS Vector3 GetVelocity() { return m_velocity; } + GENERATE_BINDINGS void SetVelocity( Vector3 velocity ) { m_velocity = velocity; } + + GENERATE_BINDINGS float GetFriction() { return m_friction; } + GENERATE_BINDINGS void SetFriction( float friction ) { m_friction = friction; } + + GENERATE_BINDINGS float GetMass() { return m_mass; } + GENERATE_BINDINGS void SetMass( float mass ) { m_mass = mass; } + + GENERATE_BINDINGS float GetRestitution() { return m_restitution; } + GENERATE_BINDINGS void SetRestitution( float restitution ) { m_restitution = restitution; } + + GENERATE_BINDINGS bool GetIgnoreRigidbodyRotation() { return m_ignoreRigidbodyRotation; } + GENERATE_BINDINGS void SetIgnoreRigidbodyRotation( bool ignore ) { m_ignoreRigidbodyRotation = ignore; } + + GENERATE_BINDINGS bool GetIgnoreRigidbodyPosition() { return m_ignoreRigidbodyPosition; } + GENERATE_BINDINGS void SetIgnoreRigidbodyPosition( bool ignore ) { m_ignoreRigidbodyPosition = ignore; } +}; diff --git a/Source/Mocha.Host/Framework/allocators.h b/Source/Mocha.Host/Framework/allocators.h new file mode 100644 index 00000000..88f89d00 --- /dev/null +++ b/Source/Mocha.Host/Framework/allocators.h @@ -0,0 +1,108 @@ +#pragma once +#include +#include + +namespace Mocha +{ + /// + /// Base allocator interface + /// + class IAllocator + { + public: + virtual void* Alloc( const size_t size, const size_t alignment, const size_t offset ) = 0; + virtual void Free( void* ptr ) = 0; + virtual void Reset() = 0; + }; + + /// + /// Basic linear allocator. Allocates memory in a linear fashion, and can be reset to free all memory. + /// + class LinearAllocator : public IAllocator + { + private: + char* m_start{ nullptr }; + char* m_end{ nullptr }; + char* m_current{ nullptr }; + + public: + LinearAllocator( void* start, void* end ) + { + m_start = ( char* )start; + m_end = ( char* )end; + Reset(); + } + + explicit LinearAllocator( size_t size ) + { + m_start = new char[size]; + m_end = m_start + size; + Reset(); + } + + /// + /// Increments a value indicating the current buffer offset + /// + inline void* Alloc( const size_t size, const size_t alignment, const size_t offset ) + { + void* ptr = m_current + offset; + size_t space = m_end - m_current; + + m_current = ( char* )std::align( alignment, size, ptr, space ) - offset; + + void* result = m_current; + m_current += size; + + assert( m_current < m_end ); + + return ptr; + } + + /// + /// Does nothing + /// + void Free( void* ptr ) + { + // + } + + /// + /// Free all allocated memory and return the allocator to its original initialized state + /// + void Reset() + { + m_current = m_start; + } + }; + + /// + /// Basic passthrough allocator, wraps malloc & free + /// + class SystemAllocator : public IAllocator + { + /// + /// Allocate a block of memory. + /// Alignment and offset do not do anything here. + /// + inline void* Alloc( const size_t size, const size_t alignment, const size_t offset ) + { + return malloc( size ); + } + + /// + /// Free a block of memory. + /// + inline void Free( void* ptr ) + { + return free( ptr ); + } + + /// + /// Does nothing + /// + inline void Reset() + { + // + } + }; +} // namespace Mocha \ No newline at end of file diff --git a/Source/Mocha.Host/Framework/array.h b/Source/Mocha.Host/Framework/array.h new file mode 100644 index 00000000..42f1b7a6 --- /dev/null +++ b/Source/Mocha.Host/Framework/array.h @@ -0,0 +1,207 @@ +#pragma once + +#include + +namespace Mocha +{ + template + class Array + { + private: + T* m_data{ nullptr }; + size_t m_size{ 0 }; + size_t m_capacity{ 0 }; + + IAllocator* m_allocator{ nullptr }; + + public: + void Init( IAllocator* allocator, size_t capacity, size_t size ); + void Destroy(); + + // ---------------------------------------- + + void Push( const T& object ); + void Pop(); + T& PushUse(); + + // ---------------------------------------- + + T& operator[]( size_t index ); + const T& operator[]( size_t index ) const; + + T* Data(); + const T* Data() const; + + // ---------------------------------------- + + size_t Size() const; + size_t Capacity() const; + + void Clear(); + void Grow( size_t capacity ); + + // ---------------------------------------- + + T& Back(); + const T& Back() const; + + T& Front(); + const T& Front() const; + }; + + // ---------------------------------------------------------------------------------------------------------------------------- + + template + inline void Array::Init( IAllocator* allocator, size_t capacity, size_t size ) + { + m_data = nullptr; + m_size = size; + + m_capacity = 0; + m_allocator = allocator; + + if ( capacity > 0 ) + { + Grow( capacity ); + } + } + + template + inline void Array::Destroy() + { + if ( m_capacity > 0 ) + { + m_allocator->Free( m_data ); + } + + m_data = nullptr; + + m_size = 0; + m_capacity = 0; + } + + template + inline void Array::Push( const T& object ) + { + if ( m_size >= m_capacity ) + { + Grow( m_capacity + 1 ); + } + + m_data[m_size++] = object; + } + + template + inline void Array::Pop() + { + assert( m_size > 0 ); + --m_size; + } + + template + inline T& Array::PushUse() + { + if ( m_size >= m_capacity ) + { + Grow( m_capacity + 1 ); + } + + ++m_size; + + return Back(); + } + + template + inline T& Array::operator[]( size_t index ) + { + assert( index >= 0 && index < m_size ); + return m_data[index]; + } + + template + inline const T& Array::operator[]( size_t index ) const + { + assert( index >= 0 && index < m_size ); + return m_data[index]; + } + + template + inline T* Array::Data() + { + return m_data; + } + + template + inline const T* Array::Data() const + { + return m_data; + } + + template + inline size_t Array::Size() const + { + return m_size; + } + + template + inline size_t Array::Capacity() const + { + return m_capacity; + } + + template + inline void Array::Clear() + { + m_size = 0; + } + + template + inline void Array::Grow( size_t capacity ) + { + assert( capacity > 0 ); + + if ( capacity < m_capacity * 2 ) + capacity = capacity * 2; + else if ( capacity < 4 ) + capacity = 4; + + T* newData = ( T* )m_allocator->Alloc( capacity * sizeof( T ), alignof( T ), 0 ); + + if ( m_capacity ) + { + memcpy_s( newData, capacity * sizeof( T ), m_data, m_size * sizeof( T ) ); + m_allocator->Free( m_data ); + } + + m_data = newData; + m_capacity = capacity; + } + + template + inline T& Array::Back() + { + assert( m_size > 0 ); + return m_data[m_size - 1]; + } + + template + inline const T& Array::Back() const + { + assert( m_size > 0 ); + return m_data[m_size - 1]; + } + + template + inline T& Array::Front() + { + assert( m_size >= 0 ); + return m_data[0]; + } + + template + inline const T& Array::Front() const + { + assert( m_size >= 0 ); + return m_data[0]; + } +} // namespace Mocha diff --git a/Source/Mocha.Host/Framework/handlemap.h b/Source/Mocha.Host/Framework/handlemap.h new file mode 100644 index 00000000..3f24cb3a --- /dev/null +++ b/Source/Mocha.Host/Framework/handlemap.h @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Mocha +{ + template + class HandleMap + { + private: + std::shared_mutex m_mutex; + + std::vector> m_objects; + std::unique_ptr m_allocator; + + public: + HandleMap() + { + m_allocator = std::make_unique(); + // m_objects.Init( m_allocator.get(), 0, 0 ); + } + + void Remove( Handle handle ); + Handle Add( T object ); + + const std::unique_ptr& Get( Handle handle ); + + template + const std::unique_ptr& GetSpecific( Handle handle ); + + template + Handle AddSpecific( T1 object ); + + void ForEach( std::function& object )> func ); + void For( std::function& object )> func ); + + const std::unique_ptr& operator[]( Handle handle ); + + const std::unique_ptr& Front() const; + const std::unique_ptr& Back() const; + }; + + template + inline Handle HandleMap::Add( T object ) + { + std::unique_lock lock( m_mutex ); + + Handle handle = m_objects.size(); + + auto objectPtr = std::make_unique( object ); + m_objects.push_back( std::move( objectPtr ) ); + return handle; + } + + template + inline const std::unique_ptr& HandleMap::Get( Handle handle ) + { + std::shared_lock lock( m_mutex ); + std::unique_ptr& object = m_objects[handle]; + + return object; + } + + template + template + inline const std::unique_ptr& HandleMap::GetSpecific( Handle handle ) + { + static_assert( std::is_base_of::value, "T1 must be derived from T" ); + + std::unique_ptr object = Get( handle ); + + return std::dynamic_pointer_cast( object ); + } + + template + template + inline Handle HandleMap::AddSpecific( T1 object ) + { + static_assert( std::is_base_of::value, "T1 must be derived from T" ); + std::unique_lock lock( m_mutex ); + + Handle handle = m_objects.size(); + + auto objectPtr = std::make_unique( object ); + m_objects.push_back( std::move( objectPtr ) ); + return handle; + } + + template + inline void HandleMap::ForEach( std::function& object )> func ) + { + std::shared_lock lock( m_mutex ); + + for ( const auto& [handle, object] : m_objects ) + { + func( object ); + } + } + + template + inline void HandleMap::For( std::function& object )> func ) + { + std::shared_lock lock( m_mutex ); + + for ( const auto& [handle, object] : m_objects ) + { + func( handle, object ); + } + } + + template + inline void HandleMap::Remove( Handle handle ) + { + std::unique_lock lock( m_mutex ); + + m_objects.erase( handle ); + } + + template + inline const std::unique_ptr& HandleMap::operator[]( Handle handle ) + { + return Get( handle ); + } + + template + inline const std::unique_ptr& HandleMap::Front() const + { + return m_objects.front(); + } + + template + inline const std::unique_ptr& HandleMap::Back() const + { + return m_objects.back(); + } +} // namespace Mocha \ No newline at end of file diff --git a/Source/Mocha.Host/Managed/Bindings/client.h b/Source/Mocha.Host/Managed/Bindings/client.h new file mode 100644 index 00000000..e543c71f --- /dev/null +++ b/Source/Mocha.Host/Managed/Bindings/client.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +namespace Client +{ + GENERATE_BINDINGS inline void StartListenServer() + { + // TODO + } +}; // namespace Client \ No newline at end of file diff --git a/Source/Mocha.Host/consolesystem.h b/Source/Mocha.Host/Managed/Bindings/consolesystem.h similarity index 83% rename from Source/Mocha.Host/consolesystem.h rename to Source/Mocha.Host/Managed/Bindings/consolesystem.h index e6e649cb..a9aa8c48 100644 --- a/Source/Mocha.Host/consolesystem.h +++ b/Source/Mocha.Host/Managed/Bindings/consolesystem.h @@ -1,8 +1,8 @@ #pragma once -#include -#include -#include +#include +#include +#include #include #include @@ -23,7 +23,8 @@ namespace ConsoleSystem CVarSystem::Instance().RegisterCommand( name, ( CVarFlags )( CVarFlags::Managed | flags ), description, nullptr ); } - GENERATE_BINDINGS inline void RegisterString( const char* name, const char* value, CVarFlags flags, const char* description ) + GENERATE_BINDINGS inline void RegisterString( + const char* name, const char* value, CVarFlags flags, const char* description ) { CVarSystem::Instance().RegisterString( name, value, ( CVarFlags )( CVarFlags::Managed | flags ), description, nullptr ); } @@ -47,14 +48,14 @@ namespace ConsoleSystem { return CVarSystem::Instance().GetFlags( name ); } - + // TODO: Not until all memory leak concerns are addressed -/* - GENERATE_BINDINGS inline const char* GetString( const char* name ) - { - return CVarSystem::Instance().GetString( name ).c_str(); - } -*/ + /* + GENERATE_BINDINGS inline const char* GetString( const char* name ) + { + return CVarSystem::Instance().GetString( name ).c_str(); + } + */ GENERATE_BINDINGS inline float GetFloat( const char* name ) { @@ -82,12 +83,12 @@ namespace ConsoleSystem } // TODO: Not until all memory leak concerns are addressed -/* - GENERATE_BINDINGS inline const char* ToString( const char* name ) - { - - } -*/ + /* + GENERATE_BINDINGS inline const char* ToString( const char* name ) + { + + } + */ GENERATE_BINDINGS inline void FromString( const char* name, const char* valueStr ) { diff --git a/Source/Mocha.Host/Managed/hostmanager.cpp b/Source/Mocha.Host/Managed/hostmanager.cpp new file mode 100644 index 00000000..c48d9489 --- /dev/null +++ b/Source/Mocha.Host/Managed/hostmanager.cpp @@ -0,0 +1,184 @@ +#include "hostmanager.h" + +#include + +void* HostGlobals::LoadHostLibrary( const char_t* path ) +{ + const HMODULE h = ::LoadLibraryW( path ); + assert( h != nullptr ); + return ( void* )h; +} + +void* HostGlobals::GetExport( void* h, const char* name ) +{ + void* f = ::GetProcAddress( ( HMODULE )h, name ); + assert( f != nullptr ); + return f; +} + +bool HostGlobals::LoadHostFxr() +{ + // Pre-allocate a large buffer for the path to hostfxr + char_t buffer[MAX_PATH]; + size_t buffer_size = sizeof( buffer ) / sizeof( char_t ); + + const int getHostfxrPathResult = get_hostfxr_path( buffer, &buffer_size, nullptr ); + if (getHostfxrPathResult != 0) + return false; + + // Load hostfxr and get desired exports + void* lib = LoadHostLibrary( buffer ); + init = static_cast( GetExport( lib, + + "hostfxr_initialize_for_runtime_config" ) ); + getDelegate = static_cast( GetExport( lib, "hostfxr_get_runtime_delegate" ) ); + setProperty = static_cast( GetExport( lib, + "hostfxr_set_runtime_property_value" ) ); + close = static_cast( GetExport( lib, "hostfxr_close" ) ); + + return ( init && getDelegate && close ); +} + +load_assembly_and_get_function_pointer_fn HostGlobals::GetDotnetLoadAssembly( const char_t* configPath ) +{ + LoadHostFxr(); + + // Load .NET Core + void* load_assembly_and_get_function_pointer = nullptr; + hostfxr_handle cxt = nullptr; + int rc = init( configPath, nullptr, &cxt ); + if (rc != 0 || cxt == nullptr) + { + spdlog::error( "Failed to initialize: 0x{:x}", rc ); + close( cxt ); + return nullptr; + } + + // Get current working directory + char_t cwd[MAX_PATH]; + const DWORD cwdLength = GetCurrentDirectoryW( MAX_PATH, cwd ); + if (cwdLength == 0) + { + spdlog::error( "Failed to get current directory" ); + close( cxt ); + return nullptr; + } + + // Add "build" to cwd + std::wstring buildPath = cwd; + buildPath += L"\\build"; + + // Set CoreCLR properties + setProperty( cxt, L"APP_CONTEXT_BASE_DIRECTORY", buildPath.c_str() ); + setProperty( cxt, L"APP_PATHS", buildPath.c_str() ); + setProperty( cxt, L"APP_NI_PATHS", buildPath.c_str() ); + setProperty( cxt, L"NATIVE_DLL_SEARCH_DIRECTORIES", buildPath.c_str() ); + setProperty( cxt, L"PLATFORM_RESOURCE_ROOTS", buildPath.c_str() ); + + // Get the load assembly function pointer + rc = getDelegate( cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer ); + if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) + spdlog::error( "Get delegate failed: 0x{:x}", rc ); + + close( cxt ); + return static_cast( load_assembly_and_get_function_pointer ); +} + +HostManager::HostManager() +{ + // TODO: Hardcoding these might be a bad idea? + std::wstring basePath = L".\\build\\Mocha.Hotload"; + std::wstring signature = L"Mocha.Hotload.Main, Mocha.Hotload"; + + m_dllPath = basePath + L".dll"; + m_configPath = basePath + L".runtimeconfig.json"; + m_signature = signature; + + if (!IsAssemblyLoaded.load()) + { + IsAssemblyLoaded.store( true ); + LoadFnPtr.store( HostGlobals::GetDotnetLoadAssembly( m_configPath.c_str() ) ); + } +} + +void HostManager::Update() const +{ + Invoke( "Update" ); +} + +void HostManager::Render() const +{ + Invoke( "Render" ); +} + +void HostManager::DrawEditor() const +{ + Invoke( "DrawEditor" ); +} + +void HostManager::Startup() +{ + Invoke( "Run", ( void* )&args ); +} + +void HostManager::Shutdown() {} + +void HostManager::FireEvent( std::string eventName ) const +{ + Invoke( "FireEvent", ( void* )eventName.c_str() ); +} + +void HostManager::DispatchCommand( CVarManagedCmdDispatchInfo info ) +{ + Invoke( "DispatchCommand", &info ); +} + +void HostManager::DispatchStringCVarCallback( CVarManagedVarDispatchInfo info ) +{ + Invoke( "DispatchStringCVarCallback", &info ); +} + +void HostManager::DispatchFloatCVarCallback( CVarManagedVarDispatchInfo info ) +{ + Invoke( "DispatchFloatCVarCallback", &info ); +} + +void HostManager::DispatchBoolCVarCallback( CVarManagedVarDispatchInfo info ) +{ + Invoke( "DispatchBoolCVarCallback", &info ); +} + +void HostManager::DispatchIntCVarCallback( CVarManagedVarDispatchInfo info ) +{ + Invoke( "DispatchIntCVarCallback", &info ); +} + +void HostManager::InvokeCallback( Handle callbackHandle, int argsCount, void* args ) const +{ + ManagedCallbackDispatchInfo dispatchInfo{}; + dispatchInfo.args = args; + dispatchInfo.argsSize = argsCount; + dispatchInfo.handle = callbackHandle; + + Invoke( "InvokeCallback", &dispatchInfo ); +} + +inline void HostManager::Invoke( std::string _method, void* params, const char_t* delegateTypeName ) const +{ + // Convert to std::wstring + const std::wstring method( _method.begin(), _method.end() ); + + // Function pointer to managed delegate + void* fnPtr = nullptr; + + int rc = + LoadFnPtr.load()( m_dllPath.c_str(), m_signature.c_str(), method.c_str(), delegateTypeName, nullptr, ( void** )&fnPtr ); + + if (fnPtr == nullptr) + { + spdlog::error( "Failed to load managed method {}", _method ); + } + + // Invoke method + static_cast( fnPtr )( params ); +} diff --git a/Source/Mocha.Host/hostmanager.h b/Source/Mocha.Host/Managed/hostmanager.h similarity index 52% rename from Source/Mocha.Host/hostmanager.h rename to Source/Mocha.Host/Managed/hostmanager.h index 8f0fdfb7..0dd2c4f0 100644 --- a/Source/Mocha.Host/hostmanager.h +++ b/Source/Mocha.Host/Managed/hostmanager.h @@ -1,7 +1,10 @@ #pragma once +#include +#include #include #include +#include #include #include #include @@ -9,12 +12,11 @@ #include #include #include -#include #include using string_t = std::basic_string; -typedef int( CORECLR_DELEGATE_CALLTYPE* run_fn )( UnmanagedArgs* args ); +typedef int( CORECLR_DELEGATE_CALLTYPE* RunFn )( UnmanagedArgs* args ); struct CVarManagedCmdDispatchInfo; @@ -24,42 +26,48 @@ struct CVarManagedVarDispatchInfo; namespace HostGlobals { // Globals to hold hostfxr exports - inline hostfxr_initialize_for_runtime_config_fn init_fptr; - inline hostfxr_get_runtime_delegate_fn get_delegate_fptr; - inline hostfxr_set_runtime_property_value_fn set_property_fptr; - inline hostfxr_close_fn close_fptr; + inline hostfxr_initialize_for_runtime_config_fn init; + inline hostfxr_get_runtime_delegate_fn getDelegate; + inline hostfxr_set_runtime_property_value_fn setProperty; + inline hostfxr_close_fn close; - void* load_library( const char_t* path ); - void* get_export( void* h, const char* name ); + void* LoadHostLibrary( const char_t* path ); + void* GetExport( void* h, const char* name ); bool LoadHostFxr(); load_assembly_and_get_function_pointer_fn GetDotnetLoadAssembly( const char_t* configPath ); }; // namespace HostGlobals +inline static std::atomic IsAssemblyLoaded = false; +inline static std::atomic LoadFnPtr; + class HostManager : ISubSystem { private: - load_assembly_and_get_function_pointer_fn m_lagfp; + load_assembly_and_get_function_pointer_fn m_loadAssemblyFunction; std::wstring m_dllPath; std::wstring m_configPath; std::wstring m_signature; - void Invoke( std::string _method, void* params = nullptr, const char_t* delegateTypeName = UNMANAGEDCALLERSONLY_METHOD ); - + void Invoke( std::string _method, void* params = nullptr, const char_t* delegateTypeName = UNMANAGEDCALLERSONLY_METHOD ) const; public: HostManager(); void Startup(); void Shutdown(); - void Update(); - void Render(); - void DrawEditor(); - void FireEvent( std::string eventName ); + void Update() const; + void Render() const; + void DrawEditor() const; + + void FireEvent( std::string eventName ) const; + void InvokeCallback( Handle callbackHandle, int argsCount, void* args ) const; + // TODO: Remove all below void DispatchCommand( CVarManagedCmdDispatchInfo info ); void DispatchStringCVarCallback( CVarManagedVarDispatchInfo info ); void DispatchFloatCVarCallback( CVarManagedVarDispatchInfo info ); void DispatchBoolCVarCallback( CVarManagedVarDispatchInfo info ); -}; + void DispatchIntCVarCallback( CVarManagedVarDispatchInfo info ); +}; \ No newline at end of file diff --git a/Source/Mocha.Host/Managed/managedcallback.cpp b/Source/Mocha.Host/Managed/managedcallback.cpp new file mode 100644 index 00000000..4fc90f16 --- /dev/null +++ b/Source/Mocha.Host/Managed/managedcallback.cpp @@ -0,0 +1,24 @@ +#include "managedcallback.h" + +#include + +ManagedCallback::ManagedCallback( Handle handle ) +{ + m_handle = handle; +} + +void ManagedCallback::Invoke() +{ + InternalInvoke( 0, nullptr ); +} + +void ManagedCallback::Invoke( void* args ) +{ + InternalInvoke( 1, args ); +} + +void ManagedCallback::InternalInvoke( int argsCount, void* args ) +{ + if ( m_handle != HANDLE_INVALID ) + Globals::m_hostManager->InvokeCallback( m_handle, argsCount, args ); +} diff --git a/Source/Mocha.Host/Managed/managedcallback.h b/Source/Mocha.Host/Managed/managedcallback.h new file mode 100644 index 00000000..c459fe9e --- /dev/null +++ b/Source/Mocha.Host/Managed/managedcallback.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +class ManagedCallback +{ +private: + Handle m_handle = HANDLE_INVALID; + void InternalInvoke( int argsCount, void* args ); + +public: + ManagedCallback() {} + + ManagedCallback( Handle handle ); + + void Invoke(); + void Invoke( void* args ); +}; \ No newline at end of file diff --git a/Source/Mocha.Host/Managed/managedcallbackdispatchinfo.h b/Source/Mocha.Host/Managed/managedcallbackdispatchinfo.h new file mode 100644 index 00000000..719d2843 --- /dev/null +++ b/Source/Mocha.Host/Managed/managedcallbackdispatchinfo.h @@ -0,0 +1,10 @@ +#pragma once +#include + +struct ManagedCallbackDispatchInfo +{ + Handle handle = HANDLE_INVALID; + + int argsSize; + void* args; +}; diff --git a/Source/Mocha.Host/cvarmanager.cpp b/Source/Mocha.Host/Misc/cvarmanager.cpp similarity index 80% rename from Source/Mocha.Host/cvarmanager.cpp rename to Source/Mocha.Host/Misc/cvarmanager.cpp index 50b5b100..754049c1 100644 --- a/Source/Mocha.Host/cvarmanager.cpp +++ b/Source/Mocha.Host/Misc/cvarmanager.cpp @@ -1,12 +1,11 @@ #include "cvarmanager.h" +#include #include -#include size_t CVarSystem::GetHash( std::string string ) { - std::transform( string.begin(), string.end(), string.begin(), - []( unsigned char c ) { return std::tolower( c ); } ); + std::transform( string.begin(), string.end(), string.begin(), []( unsigned char c ) { return std::tolower( c ); } ); return std::hash{}( string ); } @@ -47,6 +46,19 @@ void CVarSystem::Startup() if ( entry.m_flags & CVarFlags::Archive ) entry.FromString( value ); } + + // Register commands + RegisterCommand( "cvars.run", CVarFlags::Command, "Run commands from a file", [&]( std::vector args ) { + if ( args.size() != 1 ) + { + spdlog::error( "Invalid number of arguments" ); + return; + } + + std::string fileName = args[0]; + + RunFile( fileName ); + } ); } void CVarSystem::Shutdown() @@ -348,6 +360,26 @@ void CVarSystem::Run( const char* input ) } } +void CVarSystem::RunFile( std::string fileName ) +{ + std::ifstream file( fileName ); + + if ( !file.is_open() ) + { + spdlog::error( "Couldn't open '{}'", fileName ); + return; + } + + std::string line; + + while ( std::getline( file, line ) ) + { + Run( line.c_str() ); + } + + file.close(); +} + bool CVarSystem::Exists( std::string name ) { return m_cvarEntries.find( GetHash( name ) ) != m_cvarEntries.end(); @@ -381,7 +413,8 @@ void CVarSystem::RegisterCommand( std::string name, CVarFlags flags, std::string } template -inline void CVarSystem::RegisterVariable( std::string name, T value, CVarFlags flags, std::string description, CVarCallback callback ) +inline void CVarSystem::RegisterVariable( + std::string name, T value, CVarFlags flags, std::string description, CVarCallback callback ) { // This *must not* have the command flag flags = ( CVarFlags )( flags & ~( CVarFlags::Command ) ); @@ -397,21 +430,29 @@ inline void CVarSystem::RegisterVariable( std::string name, T value, CVarFlags f m_cvarEntries[hash] = entry; } -void CVarSystem::RegisterString( std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ) +void CVarSystem::RegisterString( + std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ) { RegisterVariable( name, value, flags, description, callback ); } -void CVarSystem::RegisterFloat( std::string name, float value, CVarFlags flags, std::string description, CVarCallback callback ) +void CVarSystem::RegisterFloat( + std::string name, float value, CVarFlags flags, std::string description, CVarCallback callback ) { RegisterVariable( name, value, flags, description, callback ); } -void CVarSystem::RegisterBool( std::string name, bool value, CVarFlags flags, std::string description, CVarCallback callback ) +void CVarSystem::RegisterBool( + std::string name, bool value, CVarFlags flags, std::string description, CVarCallback callback ) { RegisterVariable( name, value, flags, description, callback ); } +void CVarSystem::RegisterInt( + std::string name, int value, CVarFlags flags, std::string description, CVarCallback callback ) +{ + RegisterVariable( name, value, flags, description, callback ); +} void CVarSystem::Remove( std::string name ) { @@ -419,7 +460,6 @@ void CVarSystem::Remove( std::string name ) m_cvarEntries.erase( hash ); } - void CVarEntry::InvokeCommand( std::vector arguments ) { assert( IsCommand() ); @@ -431,16 +471,12 @@ void CVarEntry::InvokeCommand( std::vector arguments ) for ( auto& argument : arguments ) managedArguments.push_back( argument.c_str() ); - CVarManagedCmdDispatchInfo info - { - m_name.c_str(), - managedArguments.data(), - managedArguments.size() - }; + CVarManagedCmdDispatchInfo info{ m_name.c_str(), managedArguments.data(), managedArguments.size() }; - g_hostManager->DispatchCommand( info ); + Globals::m_hostManager->DispatchCommand( info ); } - else { + else + { auto callback = std::any_cast( m_callback ); if ( callback ) @@ -465,11 +501,10 @@ void CVarSystem::InvokeCommand( std::string name, std::vector argum spdlog::error( "Tried to invoke command '{}', but it's a variable!", name ); return; } - + entry.InvokeCommand( arguments ); } - CVarFlags CVarSystem::GetFlags( std::string name ) { if ( !Exists( name ) ) @@ -480,7 +515,6 @@ CVarFlags CVarSystem::GetFlags( std::string name ) return ( CVarFlags )GetEntry( name ).m_flags; } - // Putting this stuff in the header caused bad juju template @@ -513,19 +547,25 @@ inline void CVarEntry::SetValue( T value ) { CVarManagedVarDispatchInfo stringInfo{ m_name.c_str(), oldValue.c_str(), value.c_str() }; - g_hostManager->DispatchStringCVarCallback( stringInfo ); + Globals::m_hostManager->DispatchStringCVarCallback( stringInfo ); } else if constexpr ( std::is_same::value ) { CVarManagedVarDispatchInfo primitiveInfo{ m_name.c_str(), oldValue, value }; - g_hostManager->DispatchFloatCVarCallback( primitiveInfo ); + Globals::m_hostManager->DispatchFloatCVarCallback( primitiveInfo ); } else if constexpr ( std::is_same::value ) { CVarManagedVarDispatchInfo primitiveInfo{ m_name.c_str(), oldValue, value }; - g_hostManager->DispatchBoolCVarCallback( primitiveInfo ); + Globals::m_hostManager->DispatchBoolCVarCallback( primitiveInfo ); + } + else if constexpr ( std::is_same::value ) + { + CVarManagedVarDispatchInfo primitiveInfo{ m_name.c_str(), oldValue, value }; + + Globals::m_hostManager->DispatchIntCVarCallback( primitiveInfo ); } } else @@ -541,7 +581,6 @@ inline void CVarEntry::SetValue( T value ) spdlog::info( "{} was set to '{}'.", m_name, value ); } - std::string CVarEntry::GetString() { return GetValue(); @@ -557,7 +596,6 @@ std::string CVarSystem::GetString( std::string name ) return GetEntry( name ).GetString(); } - float CVarEntry::GetFloat() { return GetValue(); @@ -573,7 +611,6 @@ float CVarSystem::GetFloat( std::string name ) return GetEntry( name ).GetFloat(); } - bool CVarEntry::GetBool() { return GetValue(); @@ -589,6 +626,20 @@ bool CVarSystem::GetBool( std::string name ) return GetEntry( name ).GetBool(); } +int CVarEntry::GetInt() +{ + return GetValue(); +} + +int CVarSystem::GetInt( std::string name ) +{ + if ( !Exists( name ) ) + { + return false; + } + + return GetEntry( name ).GetInt(); +} void CVarEntry::SetString( std::string value ) { @@ -601,11 +652,10 @@ void CVarSystem::SetString( std::string name, std::string value ) { return; } - + GetEntry( name ).SetString( value ); } - void CVarEntry::SetFloat( float value ) { SetValue( value ); @@ -617,11 +667,10 @@ void CVarSystem::SetFloat( std::string name, float value ) { return; } - + GetEntry( name ).SetFloat( value ); } - void CVarEntry::SetBool( bool value ) { SetValue( value ); @@ -633,10 +682,25 @@ void CVarSystem::SetBool( std::string name, bool value ) { return; } - + GetEntry( name ).SetBool( value ); } +void CVarEntry::SetInt( int value ) +{ + SetValue( value ); +} + +void CVarSystem::SetInt( std::string name, int value ) +{ + if ( !Exists( name ) ) + { + return; + } + + GetEntry( name ).SetInt( value ); +} + std::string CVarEntry::ToString() { const std::type_info& type = m_value.type(); @@ -648,6 +712,8 @@ std::string CVarEntry::ToString() valueStr = std::to_string( std::any_cast( m_value ) ); else if ( type == typeid( bool ) ) valueStr = std::any_cast( m_value ) ? "true" : "false"; + else if ( type == typeid( int ) ) + valueStr = std::to_string( std::any_cast( m_value ) ); return valueStr; } @@ -662,7 +728,6 @@ std::string CVarSystem::ToString( std::string name ) return GetEntry( name ).ToString(); } - void CVarEntry::FromString( std::string valueStr ) { std::stringstream valueStream( valueStr ); @@ -693,6 +758,13 @@ void CVarEntry::FromString( std::string valueStr ) { SetValue( valueStr ); } + else if ( type == typeid( int ) ) + { + float value; + valueStream >> value; + + SetValue( value ); + } } void CVarSystem::FromString( std::string name, std::string valueStr ) @@ -701,11 +773,10 @@ void CVarSystem::FromString( std::string name, std::string valueStr ) { return; } - + GetEntry( name ).FromString( valueStr ); } - void CVarSystem::ForEach( std::function func ) { for ( auto& entry : m_cvarEntries ) @@ -742,11 +813,16 @@ void CVarManager::Shutdown() CVarSystem::Instance().Shutdown(); } +void CVarManager::Run( const char* input ) +{ + CVarSystem::Instance().Run( input ); +} + // ---------------------------------------- // Built-in CVars // ---------------------------------------- -static std::string GetFlagsString(CVarFlags flags) +static std::string GetFlagsString( CVarFlags flags ) { std::vector flagNames; @@ -786,46 +862,36 @@ static std::string GetFlagsString(CVarFlags flags) return ss.str(); } -static CCmd ccmd_list( "list", CVarFlags::None, "List all commands and variables", - []( std::vector arguments ) - { - auto instance = CVarSystem::Instance(); +static CCmd ccmd_list( "list", CVarFlags::None, "List all commands and variables", []( std::vector arguments ) { + auto instance = CVarSystem::Instance(); // This fails on libclang so we'll ignore it for now... #ifndef __clang__ - // List all available cvars - instance.ForEach( [&]( CVarEntry& entry ) { - std::string flagNames = GetFlagsString( (CVarFlags) entry.m_flags ); - - if ( entry.IsCommand() ) - spdlog::info( "- '{}' - {}", entry.m_name, flagNames ); - else - spdlog::info( "- '{}': '{}' - {}", entry.m_name, entry.ToString(), flagNames ); - spdlog::info( "\t{}", entry.m_description ); - } ); + // List all available cvars + instance.ForEach( [&]( CVarEntry& entry ) { + std::string flagNames = GetFlagsString( ( CVarFlags )entry.m_flags ); + + if ( entry.IsCommand() ) + spdlog::info( "- '{}' - {}", entry.m_name, flagNames ); + else + spdlog::info( "- '{}': '{}' - {}", entry.m_name, entry.ToString(), flagNames ); + spdlog::info( "\t{}", entry.m_description ); + } ); #endif - } -); +} ); // ---------------------------------------- // Test CVars // ---------------------------------------- static FloatCVar cvartest_float( "cvartest.float", 0.0f, CVarFlags::None, "Yeah", - []( float oldValue, float newValue ) - { - spdlog::trace( "cvartest.float changed! old {}, new {}", oldValue, newValue ); - } -); + []( float oldValue, float newValue ) { spdlog::trace( "cvartest.float changed! old {}, new {}", oldValue, newValue ); } ); + +static CCmd cvartest_command( "cvartest.command", CVarFlags::None, "A test command", []( std::vector arguments ) { + spdlog::trace( "cvartest.command has been invoked! Hooray" ); -static CCmd cvartest_command( "cvartest.command", CVarFlags::None, "A test command", - []( std::vector arguments ) + for ( int i = 0; i < arguments.size(); i++ ) { - spdlog::trace( "cvartest.command has been invoked! Hooray" ); - - for ( int i = 0; i < arguments.size(); i++ ) - { - spdlog::trace( "\t{} - '{}'", i, arguments.at( i ) ); - } + spdlog::trace( "\t{} - '{}'", i, arguments.at( i ) ); } -); \ No newline at end of file +} ); \ No newline at end of file diff --git a/Source/Mocha.Host/cvarmanager.h b/Source/Mocha.Host/Misc/cvarmanager.h similarity index 83% rename from Source/Mocha.Host/cvarmanager.h rename to Source/Mocha.Host/Misc/cvarmanager.h index af172ada..dcd803cf 100644 --- a/Source/Mocha.Host/cvarmanager.h +++ b/Source/Mocha.Host/Misc/cvarmanager.h @@ -1,13 +1,14 @@ #pragma once +#include +#include +#include #include #include #include -#include #include #include #include #include -#include #include // ---------------------------------------- @@ -19,7 +20,6 @@ using CVarCallback = std::function; using CCmdCallback = std::function )>; - struct CVarManagedCmdDispatchInfo { const char* name; @@ -35,7 +35,6 @@ struct CVarManagedVarDispatchInfo T newValue; }; - enum CVarFlags : int32_t { None = 0, @@ -62,7 +61,6 @@ enum CVarFlags : int32_t Replicated = 1 << 6, }; - struct CVarEntry { private: @@ -93,21 +91,24 @@ struct CVarEntry std::string GetString(); float GetFloat(); bool GetBool(); + int GetInt(); void SetString( std::string value ); void SetFloat( float value ); void SetBool( bool value ); + void SetInt( int value ); std::string ToString(); void FromString( std::string valueStr ); }; - class CVarManager : ISubSystem { public: void Startup() override; void Shutdown() override; + + GENERATE_BINDINGS void Run( const char* input ); }; class CVarSystem @@ -120,7 +121,6 @@ class CVarSystem void RegisterVariable( std::string name, T value, CVarFlags flags, std::string description, CVarCallback callback ); public: - // // CVarSystem is a singleton because it needs creating *as soon as* it's referenced // and not after. @@ -161,6 +161,12 @@ class CVarSystem /// void Run( const char* input ); + /// + /// Run statements from a .cfg file + /// + /// + void RunFile( std::string fileName ); + /// /// Check if a specific convar exists /// @@ -172,9 +178,11 @@ class CVarSystem void RegisterCommand( std::string name, CVarFlags flags, std::string description, CCmdCallback callback ); - void RegisterString( std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ); + void RegisterString( + std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ); void RegisterFloat( std::string name, float value, CVarFlags flags, std::string description, CVarCallback callback ); void RegisterBool( std::string name, bool value, CVarFlags flags, std::string description, CVarCallback callback ); + void RegisterInt( std::string name, int value, CVarFlags flags, std::string description, CVarCallback callback ); void Remove( std::string name ); @@ -185,10 +193,12 @@ class CVarSystem std::string GetString( std::string name ); float GetFloat( std::string name ); bool GetBool( std::string name ); + int GetInt( std::string name ); void SetString( std::string name, std::string value ); void SetFloat( std::string name, float value ); void SetBool( std::string name, bool value ); + void SetInt( std::string name, int value ); std::string ToString( std::string name ); void FromString( std::string name, std::string valueStr ); @@ -216,7 +226,8 @@ class CVarParameter class StringCVar : CVarParameter { public: - StringCVar( std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ) + StringCVar( + std::string name, std::string value, CVarFlags flags, std::string description, CVarCallback callback ) { m_name = name; @@ -226,12 +237,11 @@ class StringCVar : CVarParameter StringCVar( std::string name, std::string value, CVarFlags flags, std::string description ) : StringCVar( name, value, flags, description, nullptr ) { - } std::string GetValue() { return CVarSystem::Instance().GetString( m_name ); } void SetValue( std::string value ) { CVarSystem::Instance().SetString( m_name, value ); } - + operator std::string() { return GetValue(); } }; @@ -248,12 +258,11 @@ class FloatCVar : CVarParameter FloatCVar( std::string name, float value, CVarFlags flags, std::string description ) : FloatCVar( name, value, flags, description, nullptr ) { - } float GetValue() { return CVarSystem::Instance().GetFloat( m_name ); } void SetValue( float value ) { CVarSystem::Instance().SetFloat( m_name, value ); } - + operator float() { return GetValue(); } }; @@ -270,7 +279,6 @@ class BoolCVar : CVarParameter BoolCVar( std::string name, bool value, CVarFlags flags, std::string description ) : BoolCVar( name, value, flags, description, nullptr ) { - } bool GetValue() { return CVarSystem::Instance().GetBool( m_name ); } @@ -279,6 +287,27 @@ class BoolCVar : CVarParameter operator bool() { return GetValue(); }; }; +class IntCVar : CVarParameter +{ +public: + IntCVar( std::string name, int value, CVarFlags flags, std::string description, CVarCallback callback ) + { + m_name = name; + + CVarSystem::Instance().RegisterInt( name, value, flags, description, callback ); + } + + IntCVar( std::string name, int value, CVarFlags flags, std::string description ) + : IntCVar( name, value, flags, description, nullptr ) + { + } + + int GetValue() { return CVarSystem::Instance().GetInt( m_name ); } + void SetValue( int value ) { CVarSystem::Instance().SetInt( m_name, value ); } + + operator int() { return GetValue(); }; +}; + class CCmd : CVarParameter { public: @@ -294,10 +323,7 @@ class CCmd : CVarParameter // This is not going to be as clean as C#. // - void Invoke( std::vector arguments ) - { - CVarSystem::Instance().InvokeCommand( m_name, arguments ); - } + void Invoke( std::vector arguments ) { CVarSystem::Instance().InvokeCommand( m_name, arguments ); } void operator()( std::vector arguments ) { Invoke( arguments ); } }; \ No newline at end of file diff --git a/Source/Mocha.Host/defs.h b/Source/Mocha.Host/Misc/defs.h similarity index 81% rename from Source/Mocha.Host/defs.h rename to Source/Mocha.Host/Misc/defs.h index da55ab1c..2c0b2dc4 100644 --- a/Source/Mocha.Host/defs.h +++ b/Source/Mocha.Host/Misc/defs.h @@ -1,6 +1,6 @@ #pragma once -#include -#include +#include +#include // clang-format off @@ -30,7 +30,6 @@ inline void ErrorMessage( std::string str, const std::source_location& location { MessageBoxA( nullptr, str.c_str(), "Engine Error", MB_OK | MB_ICONERROR ); printf( "Engine Error %s occurred at line %d in file %s", str.c_str(), location.line(), location.file_name() ); - __debugbreak(); } #endif @@ -47,24 +46,12 @@ inline ENUMTYPE &operator &= (ENUMTYPE &a, ENUMTYPE b) { return (ENUMTYPE &)(((i inline ENUMTYPE operator ~ (ENUMTYPE a) { return ENUMTYPE(~((int)a)); } \ inline ENUMTYPE operator ^ (ENUMTYPE a, ENUMTYPE b) { return ENUMTYPE(((int)a) ^ ((int)b)); } \ inline ENUMTYPE &operator ^= (ENUMTYPE &a, ENUMTYPE b) { return (ENUMTYPE &)(((int &)a) ^= ((int)b)); } \ -} - -// -// Engine features -// -namespace EngineProperties -{ - extern StringCVar LoadedProject; - extern BoolCVar Raytracing; - extern BoolCVar Renderdoc; -}; +} // // Engine properties // #define ENGINE_NAME "Mocha" -#define GAME_VERSION ADD_QUOTES( GIT_CUR_COMMIT ) " on " ADD_QUOTES( GIT_BRANCH ) -#define WINDOW_TITLE std::string( g_projectManager->GetProject().name + " [" + g_projectManager->GetProject().version + "] - " GAME_VERSION ).c_str() // // Types @@ -72,4 +59,37 @@ namespace EngineProperties typedef uint32_t Handle; #define HANDLE_INVALID UINT32_MAX +// TODO: Remove +enum RenderDebugViews +{ + NONE = 0, + DIFFUSE = 1, + NORMAL = 2, + AMBIENTOCCLUSION = 3, + METALNESS = 4, + ROUGHNESS = 5, + + OTHER = 63 +}; + +enum Realm +{ + REALM_SERVER, + REALM_CLIENT +}; + +inline const char* RealmToString( const Realm& realm ) +{ + switch ( realm ) + { + case REALM_SERVER: + return "Server"; + case REALM_CLIENT: + return "Client"; + } + + __debugbreak(); + return "Unknown"; +} + // clang-format on \ No newline at end of file diff --git a/Source/Mocha.Host/Misc/editormanager.cpp b/Source/Mocha.Host/Misc/editormanager.cpp new file mode 100644 index 00000000..e0702ab4 --- /dev/null +++ b/Source/Mocha.Host/Misc/editormanager.cpp @@ -0,0 +1,167 @@ +#include "editormanager.h" + +#include +#include +#include +#include + +void EditorManager::Startup() {} +void EditorManager::Shutdown() {} + +void* EditorManager::GetContextPointer() +{ + auto ctx = ImGui::GetCurrentContext(); + return ( void* )ctx; +} + +void EditorManager::TextBold( const char* text ) +{ + ImGui::PushFont( Globals::m_renderContext->m_boldFont ); + ImGui::Text( "%s", text ); + ImGui::PopFont(); +} + +void EditorManager::TextSubheading( const char* text ) +{ + ImGui::PushFont( Globals::m_renderContext->m_subheadingFont ); + ImGui::Text( "%s", text ); + ImGui::Dummy( ImVec2( 0, 2 ) ); + ImGui::PopFont(); +} + +void EditorManager::TextHeading( const char* text ) +{ + ImGui::PushFont( Globals::m_renderContext->m_headingFont ); + ImGui::Text( "%s", text ); + ImGui::Dummy( ImVec2( 0, 2 ) ); + ImGui::PopFont(); +} + +void EditorManager::TextMonospace( const char* text ) +{ + ImGui::PushFont( Globals::m_renderContext->m_monospaceFont ); + ImGui::Text( "%s", text ); + ImGui::PopFont(); +} + +void EditorManager::TextLight( const char* text ) +{ + ImGui::PushStyleColor( ImGuiCol_Text, ImVec4( 1, 1, 1, 0.75f ) ); + ImGui::Text( "%s", text ); + ImGui::PopStyleColor(); +} + +const char* EditorManager::GetGPUName() +{ + return Globals::m_renderManager->GetGPUName(); +} + +char* EditorManager::InputText( const char* name, char* inputBuf, int inputLength ) +{ + ImGui::InputText( name, inputBuf, inputLength, ImGuiInputTextFlags_EnterReturnsTrue ); + + return inputBuf; +} + +void EditorManager::RenderViewDropdown() +{ + if ( ImGui::BeginMenu( "Debug View" ) ) + { + if ( ImGui::MenuItem( "None" ) ) + Globals::m_debugView = RenderDebugViews::NONE; + + if ( ImGui::MenuItem( "Diffuse" ) ) + Globals::m_debugView = RenderDebugViews::DIFFUSE; + + if ( ImGui::MenuItem( "Normal" ) ) + Globals::m_debugView = RenderDebugViews::NORMAL; + + if ( ImGui::MenuItem( "Ambient Occlusion" ) ) + Globals::m_debugView = RenderDebugViews::AMBIENTOCCLUSION; + + if ( ImGui::MenuItem( "Metalness" ) ) + Globals::m_debugView = RenderDebugViews::METALNESS; + + if ( ImGui::MenuItem( "Roughness" ) ) + Globals::m_debugView = RenderDebugViews::ROUGHNESS; + + if ( ImGui::MenuItem( "Other" ) ) + Globals::m_debugView = RenderDebugViews::OTHER; + + ImGui::EndMenu(); + } +} + +void EditorManager::Image( Texture* texture, uint32_t textureWidth, uint32_t textureHeight, int x, int y ) +{ + void* imguiTextureID; + Globals::m_renderContext->GetImGuiTextureID( &texture->m_image, &imguiTextureID ); + + // Calculate new UVs based on reported textureWidth, textureHeight vs texture->m_size + // This is done because the C++ side isn't aware of any padding applied in order to get + // the image to become POT + float u = ( float )textureWidth / ( float )texture->m_size.x; + float v = ( float )textureWidth / ( float )texture->m_size.y; + + ImGui::Image( imguiTextureID, { ( float )x, ( float )y }, { 0, 0 }, { u, v } ); +} + +bool EditorManager::BeginMainStatusBar() +{ + ImGuiViewportP* viewport = ( ImGuiViewportP* )( void* )ImGui::GetMainViewport(); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; + float height = ImGui::GetFrameHeight(); + + if ( ImGui::BeginViewportSideBar( "##MainStatusBar", viewport, ImGuiDir_Down, height, window_flags ) ) + { + if ( ImGui::BeginMenuBar() ) + { + return true; + } + } + + return false; +} + +void EditorManager::DrawGraph( const char* name, Vector4 color, UtilArray values ) +{ + const std::vector plotValues = values.GetData(); + const float MARKERS[] = { 30.0f, 60.0f, 144.0f }; + const int MARKER_COUNT = 3; + const int sampleCount = static_cast( plotValues.size() ); + + auto startPos = ImGui::GetCursorPos(); + + ImPlot::PushStyleVar( ImPlotStyleVar_PlotPadding, { 0, 0 } ); + ImPlot::PushStyleVar( ImPlotStyleVar_LineWeight, 1.0f ); + ImPlot::PushStyleColor( ImPlotCol_PlotBg, { color.x, color.y, color.z, color.w } ); + ImPlot::PushStyleColor( ImPlotCol_Line, { color.x, color.y, color.z, color.w * 4.0f } ); + ImPlot::PushStyleColor( ImPlotCol_InlayText, { color.x, color.y, color.z, color.w * 4.0f } ); + + if ( ImPlot::BeginPlot( + name, { -1, 128 }, ImPlotFlags_NoInputs | ImPlotFlags_NoMenus | ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText ) ) + { + ImPlot::SetupAxis( ImAxis_X1, 0, ImPlotAxisFlags_NoDecorations ); + ImPlot::SetupAxis( ImAxis_Y1, 0, ImPlotAxisFlags_NoDecorations ); + ImPlot::SetupAxisLimits( ImAxis_Y1, 0.0, MARKERS[MARKER_COUNT - 1] + 20.0f, ImPlotCond_Always ); + ImPlot::SetupAxisLimits( ImAxis_X1, 0.0, sampleCount, ImPlotCond_Always ); + + ImPlot::PlotInfLines( "##reference", MARKERS, MARKER_COUNT, ImPlotInfLinesFlags_Horizontal ); + + for ( auto& marker : MARKERS ) + { + std::string str = std::to_string( ( int )marker ) + "fps"; + float x = sampleCount - 40.0f; + float y = marker - 15.0f; + ImPlot::PlotText( str.c_str(), x, y ); + } + + ImPlot::PushStyleVar( ImPlotStyleVar_LineWeight, 2.0f ); + ImPlot::PlotLine( name, plotValues.data(), sampleCount ); + ImPlot::PopStyleVar(); + ImPlot::EndPlot(); + } + + ImPlot::PopStyleColor( 3 ); + ImPlot::PopStyleVar( 2 ); +} diff --git a/Source/Mocha.Host/Misc/editormanager.h b/Source/Mocha.Host/Misc/editormanager.h new file mode 100644 index 00000000..e9a8d7b1 --- /dev/null +++ b/Source/Mocha.Host/Misc/editormanager.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class EditorManager : ISubSystem +{ +public: + void Startup() override; + void Shutdown() override; + + /// + /// Get the current pointer to an ImGUI context. + /// This is used in order to effectively "link" managed ImGUI + /// to our native ImGUI instance. + /// + GENERATE_BINDINGS void* GetContextPointer(); + GENERATE_BINDINGS void TextBold( const char* text ); + GENERATE_BINDINGS void TextSubheading( const char* text ); + GENERATE_BINDINGS void TextHeading( const char* text ); + GENERATE_BINDINGS void TextMonospace( const char* text ); + GENERATE_BINDINGS void TextLight( const char* text ); + GENERATE_BINDINGS const char* GetGPUName(); + GENERATE_BINDINGS char* InputText( const char* name, char* inputBuf, int inputLength ); + GENERATE_BINDINGS void RenderViewDropdown(); + GENERATE_BINDINGS void Image( Texture* texture, uint32_t textureWidth, uint32_t textureHeight, int x, int y ); + GENERATE_BINDINGS bool BeginMainStatusBar(); + GENERATE_BINDINGS void DrawGraph( const char* name, Vector4 color, UtilArray values ); +}; diff --git a/Source/Mocha.Host/Misc/globalvars.cpp b/Source/Mocha.Host/Misc/globalvars.cpp new file mode 100644 index 00000000..4c329263 --- /dev/null +++ b/Source/Mocha.Host/Misc/globalvars.cpp @@ -0,0 +1,57 @@ +#include "globalvars.h" + +#include +#include +#include +#include + +// +// Engine features +// +namespace EngineProperties +{ + StringCVar LoadedProject( + "project.current", "Samples\\mocha-minimal\\project.json", CVarFlags::Archive, "Which project should we load?" ); + BoolCVar Raytracing( "render.raytracing", true, CVarFlags::Archive, "Enable raytracing" ); + BoolCVar Renderdoc( "render.renderdoc", false, CVarFlags::Archive, "Enable renderdoc" ); + + StringCVar ServerName( "server.name", "Mocha Dedicated Server", CVarFlags::None, "Server name" ); + StringCVar ServerPassword( "server.password", "", CVarFlags::None, "Server password" ); + IntCVar ServerPort( "server.port", 7777, CVarFlags::None, "Server port" ); + IntCVar ServerMaxPlayers( "server.maxplayers", 16, CVarFlags::None, "Server max players" ); + FloatCVar timescale( "game.timescale", 1.0f, CVarFlags::Archive, "The speed at which the game world runs." ); +} // namespace EngineProperties + +namespace Globals +{ + RenderManager* m_renderManager; + LogManager* m_logManager; + HostManager* m_hostManager; + RenderdocManager* m_renderdocManager; + EntityManager* m_entityManager; + PhysicsManager* m_physicsManager; + EditorManager* m_editorManager; + InputManager* m_inputManager; + BaseRenderContext* m_renderContext; + CVarManager* m_cvarManager; + ProjectManager* m_projectManager; + NetworkingManager* m_networkingManager; + + float m_curTime; + float m_frameDeltaTime; + float m_tickDeltaTime; + int m_curTick; + + Vector3 m_cameraPos; + Quaternion m_cameraRot; + float m_cameraFov; + float m_cameraZNear; + float m_cameraZFar; + + RenderDebugViews m_debugView; + Realm m_executingRealm; + + bool m_isDedicatedServer; + + char* m_activeProjectPath; +} // namespace Globals \ No newline at end of file diff --git a/Source/Mocha.Host/Misc/globalvars.h b/Source/Mocha.Host/Misc/globalvars.h new file mode 100644 index 00000000..163cca2f --- /dev/null +++ b/Source/Mocha.Host/Misc/globalvars.h @@ -0,0 +1,77 @@ +#pragma once + +// +// Engine features +// +class StringCVar; +class BoolCVar; +class IntCVar; +class FloatCVar; + +// TODO: move +namespace EngineProperties +{ + extern StringCVar LoadedProject; + extern BoolCVar Raytracing; + extern BoolCVar Renderdoc; + + extern StringCVar ServerHostname; + extern StringCVar ServerPassword; + extern IntCVar ServerPort; + extern IntCVar ServerMaxPlayers; +}; // namespace EngineProperties + +// TODO: Server / client +extern FloatCVar maxFramerate; + +class RenderManager; +class LogManager; +class HostManager; +class RenderdocManager; +class EntityManager; +class PhysicsManager; +class EditorManager; +class InputManager; +class BaseRenderContext; +class CVarManager; +class ProjectManager; +class NetworkingManager; + +struct Vector3; +struct Quaternion; +enum RenderDebugViews; +enum Realm; + +namespace Globals +{ + extern RenderManager* m_renderManager; + extern LogManager* m_logManager; + extern HostManager* m_hostManager; + extern RenderdocManager* m_renderdocManager; + extern EntityManager* m_entityManager; + extern PhysicsManager* m_physicsManager; + extern EditorManager* m_editorManager; + extern InputManager* m_inputManager; + extern BaseRenderContext* m_renderContext; + extern CVarManager* m_cvarManager; + extern ProjectManager* m_projectManager; + extern NetworkingManager* m_networkingManager; + + extern float m_curTime; + extern float m_frameDeltaTime; + extern float m_tickDeltaTime; + extern int m_curTick; + + extern Vector3 m_cameraPos; + extern Quaternion m_cameraRot; + extern float m_cameraFov; + extern float m_cameraZNear; + extern float m_cameraZFar; + + extern RenderDebugViews m_debugView; + extern Realm m_executingRealm; + + extern bool m_isDedicatedServer; + + extern char* m_activeProjectPath; +} // namespace Globals \ No newline at end of file diff --git a/Source/Mocha.Host/handlemap.h b/Source/Mocha.Host/Misc/handlemap.h similarity index 79% rename from Source/Mocha.Host/handlemap.h rename to Source/Mocha.Host/Misc/handlemap.h index 777f7765..c81512f1 100644 --- a/Source/Mocha.Host/handlemap.h +++ b/Source/Mocha.Host/Misc/handlemap.h @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include -#include +#include #include +#include #include +#include +#include #include -#include +#include // A class that manages a collection of objects of type T, indexed by a handle. template @@ -27,6 +27,15 @@ class HandleMap // Adds the specified object to the map and returns a handle to it. Handle Add( T object ); + // Removes the specified object from the map, based on a handle. + void RemoveAt( Handle handle ); + + // Removes the first instance of the specified object from the map. + void Remove( T object ); + + // Finds the first instance of the specified object from the map. + Handle Find( T object ); + // Returns a pointer to the object associated with the specified handle. std::shared_ptr Get( Handle handle ); @@ -53,7 +62,7 @@ inline Handle HandleMap::Add( T object ) std::unique_lock lock( m_mutex ); Handle handle = m_nextIndex; - + // Create a shared pointer to the object. auto objectPtr = std::make_shared( object ); @@ -66,12 +75,48 @@ inline Handle HandleMap::Add( T object ) return handle; } +template +inline void HandleMap::RemoveAt( Handle handle ) +{ + std::unique_lock lock( m_mutex ); + + m_objects.erase( handle ); +} + +template +inline void HandleMap::Remove( T object ) +{ + std::unique_lock lock( m_mutex ); + + Handle targetHandle = Find( object ); + if ( targetHandle != HANDLE_INVALID ) + { + RemoveAt( targetHandle ); + } +} + +template +inline Handle HandleMap::Find( T object ) +{ + std::unique_lock lock( m_mutex ); + + for ( const auto& [handle, object] : m_objects ) + { + if ( typeid( *object ) == typeid( T ) ) + { + return handle; + } + } + + return HANDLE_INVALID; +} + // Returns a pointer to the object associated with the specified handle. template inline std::shared_ptr HandleMap::Get( Handle handle ) { std::shared_lock lock( m_mutex ); - + std::shared_ptr object = m_objects[handle]; return object; @@ -85,7 +130,7 @@ inline std::shared_ptr HandleMap::GetSpecific( Handle handle ) static_assert( std::is_base_of::value, "T1 must be derived from T" ); std::shared_ptr object = Get( handle ); - + return std::dynamic_pointer_cast( object ); } diff --git a/Source/Mocha.Host/inputmanager.cpp b/Source/Mocha.Host/Misc/inputmanager.cpp similarity index 80% rename from Source/Mocha.Host/inputmanager.cpp rename to Source/Mocha.Host/Misc/inputmanager.cpp index 7f32c6b8..2ea2f3e7 100644 --- a/Source/Mocha.Host/inputmanager.cpp +++ b/Source/Mocha.Host/Misc/inputmanager.cpp @@ -1,9 +1,14 @@ #include "inputmanager.h" +#include +#include +#include + #if _IMGUI #include -#define WANTS_CAPTURE ImGui::GetIO().WantCaptureKeyboard || ImGui::GetIO().WantCaptureMouse +#define WANTS_CAPTURE \ + Globals::m_executingRealm == REALM_CLIENT && ( ImGui::GetIO().WantCaptureKeyboard || ImGui::GetIO().WantCaptureMouse ) #else diff --git a/Source/Mocha.Host/inputmanager.h b/Source/Mocha.Host/Misc/inputmanager.h similarity index 61% rename from Source/Mocha.Host/inputmanager.h rename to Source/Mocha.Host/Misc/inputmanager.h index 8c5d8fcd..0fd992c0 100644 --- a/Source/Mocha.Host/inputmanager.h +++ b/Source/Mocha.Host/Misc/inputmanager.h @@ -1,8 +1,9 @@ #pragma once +#include +#include +#include #include -#include #include -#include #include struct InputState @@ -27,8 +28,8 @@ class InputManager : ISubSystem InputState GetState(); void SetState( InputState newState ); - bool IsButtonDown( int button ); - bool IsKeyDown( int key ); - Vector2 GetMousePosition(); - Vector2 GetMouseDelta(); + GENERATE_BINDINGS bool IsButtonDown( int button ); + GENERATE_BINDINGS bool IsKeyDown( int key ); + GENERATE_BINDINGS Vector2 GetMousePosition(); + GENERATE_BINDINGS Vector2 GetMouseDelta(); }; diff --git a/Source/Mocha.Host/Misc/logmanager.cpp b/Source/Mocha.Host/Misc/logmanager.cpp new file mode 100644 index 00000000..2c6114a1 --- /dev/null +++ b/Source/Mocha.Host/Misc/logmanager.cpp @@ -0,0 +1,51 @@ +#include "logmanager.h" + +#include "spdlog/spdlog.h" + +#include + +void LogManager::Startup() +{ + // Only do this once per-app - loggers are shared between + // roots, so we don't want to create multiple loggers + if ( IsInitialized.load() ) + return; + + IsInitialized.store( true ); + + // Setup spdlog + m_sink = std::make_shared(); + + // Register loggers if they don't exist + if ( !spdlog::get( "core" ) ) + { + auto coreLogger = std::make_shared( "core", m_sink ); + spdlog::register_logger( coreLogger ); + spdlog::set_default_logger( coreLogger ); + } + + spdlog::set_level( spdlog::level::trace ); + + // Set pattern "time logger,8 type,8 message" + spdlog::set_pattern( "%H:%M:%S %-8n %^%-8l%$ %v" ); +} + +void LogManager::ManagedInfo( std::string loggerName, std::string str ) +{ + GetLogger( loggerName )->info( str ); +} + +void LogManager::ManagedWarning( std::string loggerName, std::string str ) +{ + GetLogger( loggerName )->warn( str ); +} + +void LogManager::ManagedError( std::string loggerName, std::string str ) +{ + GetLogger( loggerName )->error( str ); +} + +void LogManager::ManagedTrace( std::string loggerName, std::string str ) +{ + GetLogger( loggerName )->trace( str ); +} \ No newline at end of file diff --git a/Source/Mocha.Host/logmanager.h b/Source/Mocha.Host/Misc/logmanager.h similarity index 56% rename from Source/Mocha.Host/logmanager.h rename to Source/Mocha.Host/Misc/logmanager.h index ed358b6b..50d19a11 100644 --- a/Source/Mocha.Host/logmanager.h +++ b/Source/Mocha.Host/Misc/logmanager.h @@ -1,6 +1,9 @@ #pragma once -#include -#include +#include +#include +#include +#include +#include #include #include #include @@ -8,7 +11,6 @@ #include #include #include -#include #define MAX_LOG_MESSAGES 50 @@ -26,24 +28,50 @@ struct LogHistory LogEntryInterop* items; }; +inline std::atomic IsInitialized = false; + +template +class MochaSink; + +using MochaSinkMT = MochaSink; +using MochaSinkST = MochaSink; + class LogManager : ISubSystem { +private: + std::shared_ptr m_sink; + + inline std::shared_ptr GetLogger( std::string loggerName ) + { + std::shared_ptr existingLogger = spdlog::get( loggerName ); + + if ( !existingLogger ) + { + auto logger = std::make_shared( loggerName, m_sink ); + spdlog::register_logger( logger ); + + return logger; + } + + return existingLogger; + } + public: void Startup(); void Shutdown(){}; std::vector m_logHistory; - GENERATE_BINDINGS static void ManagedInfo( std::string str ); - GENERATE_BINDINGS static void ManagedWarning( std::string str ); - GENERATE_BINDINGS static void ManagedError( std::string str ); - GENERATE_BINDINGS static void ManagedTrace( std::string str ); + GENERATE_BINDINGS void ManagedInfo( std::string loggerName, std::string str ); + GENERATE_BINDINGS void ManagedWarning( std::string loggerName, std::string str ); + GENERATE_BINDINGS void ManagedError( std::string loggerName, std::string str ); + GENERATE_BINDINGS void ManagedTrace( std::string loggerName, std::string str ); - GENERATE_BINDINGS inline static LogHistory GetLogHistory() + GENERATE_BINDINGS inline LogHistory GetLogHistory() { LogHistory logHistory = {}; - logHistory.count = static_cast( g_logManager->m_logHistory.size() ); - logHistory.items = g_logManager->m_logHistory.data(); + logHistory.count = static_cast( Globals::m_logManager->m_logHistory.size() ); + logHistory.items = Globals::m_logManager->m_logHistory.data(); return logHistory; } @@ -81,7 +109,17 @@ class MochaSink : public spdlog::sinks::base_sink { spdlog::memory_buf_t formatted; spdlog::sinks::base_sink::formatter_->format( msg, formatted ); - OutputDebugStringA( fmt::to_string( formatted ).c_str() ); + + if ( Globals::m_isDedicatedServer ) + { + // Servers use the console + std::cout << fmt::to_string( formatted ); + } + else + { + // In client, use visual studio's output window + OutputDebugStringA( fmt::to_string( formatted ).c_str() ); + } // Format everything to std::string std::string time = TimePointToString( msg.time ); @@ -95,17 +133,14 @@ class MochaSink : public spdlog::sinks::base_sink CopyString( &logEntry.level, level ); CopyString( &logEntry.message, message ); - g_logManager->m_logHistory.emplace_back( logEntry ); + Globals::m_logManager->m_logHistory.emplace_back( logEntry ); // If we have more than 128 messages in the log history, start getting rid - if ( g_logManager->m_logHistory.size() > MAX_LOG_MESSAGES ) + if ( Globals::m_logManager->m_logHistory.size() > MAX_LOG_MESSAGES ) { - g_logManager->m_logHistory.erase( g_logManager->m_logHistory.begin() ); + Globals::m_logManager->m_logHistory.erase( Globals::m_logManager->m_logHistory.begin() ); } } void flush_() override { std::cout << std::flush; } }; - -using MochaSinkMT = MochaSink; -using MochaSinkST = MochaSink; diff --git a/Source/Mocha.Host/mathtypes.h b/Source/Mocha.Host/Misc/mathtypes.h similarity index 100% rename from Source/Mocha.Host/mathtypes.h rename to Source/Mocha.Host/Misc/mathtypes.h diff --git a/Source/Mocha.Host/projectmanager.cpp b/Source/Mocha.Host/Misc/projectmanager.cpp similarity index 76% rename from Source/Mocha.Host/projectmanager.cpp rename to Source/Mocha.Host/Misc/projectmanager.cpp index cf0b677c..36490c50 100644 --- a/Source/Mocha.Host/projectmanager.cpp +++ b/Source/Mocha.Host/Misc/projectmanager.cpp @@ -1,7 +1,16 @@ #include "ProjectManager.h" +#include +#include + void ProjectManager::Startup() { + // If the command-line has passed a project, use that instead. + if ( Globals::m_activeProjectPath ) + EngineProperties::LoadedProject.SetValue( Globals::m_activeProjectPath ); + else + Globals::m_activeProjectPath = EngineProperties::LoadedProject.GetValue().data(); + // Load project from json m_project = Project( EngineProperties::LoadedProject ); diff --git a/Source/Mocha.Host/projectmanager.h b/Source/Mocha.Host/Misc/projectmanager.h similarity index 71% rename from Source/Mocha.Host/projectmanager.h rename to Source/Mocha.Host/Misc/projectmanager.h index e7a37e39..50894179 100644 --- a/Source/Mocha.Host/projectmanager.h +++ b/Source/Mocha.Host/Misc/projectmanager.h @@ -1,8 +1,8 @@ #pragma once -#include -#include -#include +#include +#include +#include class ProjectManager : public ISubSystem { diff --git a/Source/Mocha.Host/projectmanifest.h b/Source/Mocha.Host/Misc/projectmanifest.h similarity index 100% rename from Source/Mocha.Host/projectmanifest.h rename to Source/Mocha.Host/Misc/projectmanifest.h diff --git a/Source/Mocha.Host/subsystem.h b/Source/Mocha.Host/Misc/subsystem.h similarity index 88% rename from Source/Mocha.Host/subsystem.h rename to Source/Mocha.Host/Misc/subsystem.h index 75d11a25..0d0b1ddd 100644 --- a/Source/Mocha.Host/subsystem.h +++ b/Source/Mocha.Host/Misc/subsystem.h @@ -1,5 +1,7 @@ #pragma once +class Root; + class ISubSystem { public: diff --git a/Source/Mocha.Host/Mocha.Host.vcxproj b/Source/Mocha.Host/Mocha.Host.vcxproj index 88c726a6..44d5b0a7 100644 --- a/Source/Mocha.Host/Mocha.Host.vcxproj +++ b/Source/Mocha.Host/Mocha.Host.vcxproj @@ -40,12 +40,12 @@ Unicode - Application + StaticLibrary true v143 - Application + StaticLibrary false v143 true @@ -71,8 +71,8 @@ $(SolutionDir)..\build - Mocha - $(VULKAN_SDK)\Include;$(ProjectDir)ThirdParty\volk;$(ProjectDir)ThirdParty\Renderdoc;$(ProjectDir)ThirdParty\vk-bootstrap\src;$(ProjectDir)ThirdParty\imgui;$(ProjectDir)ThirdParty\implot;$(ProjectDir)ThirdParty\JoltPhysics;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath) + Mocha.Host + $(VULKAN_SDK)\Include;$(ProjectDir)ThirdParty\volk;$(ProjectDir)ThirdParty\Renderdoc;$(ProjectDir)ThirdParty\FontAwesome;$(ProjectDir)ThirdParty\vk-bootstrap\src;$(ProjectDir)ThirdParty\imgui;$(ProjectDir)ThirdParty\implot;$(ProjectDir)ThirdParty\JoltPhysics;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath) $(LibraryPath) $(VC_SourcePath) $(ProjectDir);$(IncludePath) @@ -82,8 +82,8 @@ $(SolutionDir)..\build - Mocha - $(VULKAN_SDK)\Include;$(ProjectDir)ThirdParty\volk;$(ProjectDir)ThirdParty\Renderdoc;$(ProjectDir)ThirdParty\vk-bootstrap\src;$(ProjectDir)ThirdParty\imgui;$(ProjectDir)ThirdParty\implot;$(ProjectDir)ThirdParty\JoltPhysics;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath) + Mocha.Host + $(VULKAN_SDK)\Include;$(ProjectDir)ThirdParty\volk;$(ProjectDir)ThirdParty\Renderdoc;$(ProjectDir)ThirdParty\FontAwesome;$(ProjectDir)ThirdParty\vk-bootstrap\src;$(ProjectDir)ThirdParty\imgui;$(ProjectDir)ThirdParty\implot;$(ProjectDir)ThirdParty\JoltPhysics;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath) $(LibraryPath) $(VC_SourcePath) $(ProjectDir);$(IncludePath) @@ -157,6 +157,9 @@ git rev-parse --verify --short HEAD >> gitdefs.h echo | set /p dummyName=#define GIT_BRANCH >> gitdefs.h git rev-parse --abbrev-ref HEAD >> gitdefs.h + + GameNetworkingSockets.lib + @@ -185,25 +188,34 @@ git rev-parse --verify --short HEAD >> gitdefs.h echo | set /p dummyName=#define GIT_BRANCH >> gitdefs.h git rev-parse --abbrev-ref HEAD >> gitdefs.h + + GameNetworkingSockets.lib + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -342,44 +354,47 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -647,12 +662,8 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h - - - - - - + + diff --git a/Source/Mocha.Host/Mocha.Host.vcxproj.filters b/Source/Mocha.Host/Mocha.Host.vcxproj.filters index 455f1212..9fc5c6b6 100644 --- a/Source/Mocha.Host/Mocha.Host.vcxproj.filters +++ b/Source/Mocha.Host/Mocha.Host.vcxproj.filters @@ -58,32 +58,11 @@ {adff9618-14a2-4b36-b617-c90e0464d6a3} + + {a6ab7305-4387-4823-9506-9137221613fd} + - - Entities - - - Rendering - - - Physics - - - Misc - - - Rendering - - - Misc - - - Root - - - Root - Thirdparty\ImGUI @@ -486,24 +465,6 @@ Thirdparty\VkBootstrap - - Misc - - - Rendering\Assets - - - Rendering\Assets - - - Rendering - - - Rendering\Assets - - - Managed - Thirdparty\ImPlot @@ -516,59 +477,75 @@ Thirdparty\Volk - - Rendering\Platform\Vulkan - - - Rendering\Platform + + Entities - - Rendering\Platform\Vulkan + + Managed - - Rendering + + Misc - + Misc - - - - Entities - - - Entities - - - Entities - - - Rendering - - - Physics - - + Misc - - + + Misc - - + + Misc - - + + Misc - - + + + Physics + + + Rendering\Assets + + + Rendering\Assets + + + Rendering\Assets + + + Rendering\Platform\Vulkan + + + Rendering\Platform\Vulkan + + Rendering - - - Misc - - + + + Rendering + + + Rendering + + + Rendering + + + Rendering + + Root - + + + Root + + + Root + + + + Thirdparty\ImGUI @@ -1361,99 +1338,134 @@ Thirdparty\VkBootstrap - + + Thirdparty\FontAwesome + + + Thirdparty\ImPlot + + + Thirdparty\ImPlot + + + Thirdparty\Volk + + + Entities + + + Entities + + + Entities + + + Managed\Bindings + + + Managed\Bindings + + + Managed + + Misc - + Misc - + Misc - - Rendering\Assets + + Misc - - Rendering\Assets + + Misc - - Rendering + + Misc - - Rendering\Assets + + Misc - + Misc - - Managed + + Misc - - Thirdparty\FontAwesome + + Misc - - Thirdparty\ImPlot + + Misc - - Thirdparty\ImPlot + + Physics - - Thirdparty\Volk + + Rendering\Assets - - Rendering\Platform\Vulkan + + Rendering\Assets - - Rendering\Platform + + Rendering\Assets - - Rendering\Platform\Vulkan + + Rendering\Assets - - Util + + Rendering\Platform\Null - - Util + + Rendering\Platform\Vulkan - - Rendering + + Rendering\Platform\Vulkan - + Rendering\Platform\Vulkan - + Rendering\Platform\Vulkan - - Rendering\Assets + + Rendering - - Managed\Bindings + + Rendering - - Managed\Bindings + + Rendering - - Managed\Bindings + + Rendering - - Managed\Bindings + + Rendering - - Managed\Bindings + + Rendering - - Managed\Bindings + + Root - - Rendering + + Root - - Misc + + Root - - Misc + + Util + + + Util + + diff --git a/Source/Mocha.Host/physicsmanager.cpp b/Source/Mocha.Host/Physics/physicsmanager.cpp similarity index 94% rename from Source/Mocha.Host/physicsmanager.cpp rename to Source/Mocha.Host/Physics/physicsmanager.cpp index 5c8d6797..b0a9d157 100644 --- a/Source/Mocha.Host/physicsmanager.cpp +++ b/Source/Mocha.Host/Physics/physicsmanager.cpp @@ -1,8 +1,9 @@ #include "physicsmanager.h" +#include +#include +#include #include -#include -#include #include #include @@ -21,10 +22,14 @@ static void TraceImpl( const char* inFMT, ... ) PhysicsManager::PhysicsManager() { - JPH::RegisterDefaultAllocator(); - JPH::Trace = TraceImpl; - JPH::Factory::sInstance = new JPH::Factory(); - JPH::RegisterTypes(); + if ( !AreTypesRegistered.load() ) + { + AreTypesRegistered.store( true ); + JPH::RegisterDefaultAllocator(); + JPH::Trace = TraceImpl; + JPH::Factory::sInstance = new JPH::Factory(); + JPH::RegisterTypes(); + } m_physicsInstance = std::make_shared(); @@ -70,7 +75,7 @@ void PhysicsManager::Update() const int integrationSubSteps = 1; // Retrieve properties that were saved off last frame - g_entityDictionary->ForEach( [&]( std::shared_ptr entity ) { + Globals::m_entityManager->ForEach( [&]( std::shared_ptr entity ) { // Is this a valid entity to do physics stuff on? auto modelEntity = std::dynamic_pointer_cast( entity ); @@ -97,10 +102,10 @@ void PhysicsManager::Update() } ); // Step the world - m_physicsInstance->m_physicsSystem.Update( - g_tickDeltaTime, collisionSteps, integrationSubSteps, m_physicsInstance->m_tempAllocator, m_physicsInstance->m_jobSystem ); + m_physicsInstance->m_physicsSystem.Update( Globals::m_tickDeltaTime, collisionSteps, integrationSubSteps, + m_physicsInstance->m_tempAllocator, m_physicsInstance->m_jobSystem ); - g_entityDictionary->ForEach( [&]( std::shared_ptr entity ) { + Globals::m_entityManager->ForEach( [&]( std::shared_ptr entity ) { // Is this a valid entity to do physics stuff on? auto modelEntity = std::dynamic_pointer_cast( entity ); @@ -270,7 +275,7 @@ uint32_t PhysicsManager::FindEntityHandleForBodyId( JPH::BodyID bodyId ) // Step 2: find entity handle // uint32_t entityHandle = UINT32_MAX; - g_entityDictionary->For( [&]( Handle handle, std::shared_ptr entity ) { + Globals::m_entityManager->For( [&]( Handle handle, std::shared_ptr entity ) { auto modelEntity = std::dynamic_pointer_cast( entity ); if ( modelEntity == nullptr ) diff --git a/Source/Mocha.Host/physicsmanager.h b/Source/Mocha.Host/Physics/physicsmanager.h similarity index 96% rename from Source/Mocha.Host/physicsmanager.h rename to Source/Mocha.Host/Physics/physicsmanager.h index 4dd91a6f..7df4a5b8 100644 --- a/Source/Mocha.Host/physicsmanager.h +++ b/Source/Mocha.Host/Physics/physicsmanager.h @@ -1,11 +1,12 @@ #pragma once +#include #include -#include -#include +#include +#include +#include +#include #include -#include #include -#include #include // Jolt includes @@ -232,6 +233,8 @@ namespace JoltConversions } }; // namespace JoltConversions +inline static std::atomic AreTypesRegistered = false; + class PhysicsManager : HandleMap, ISubSystem { private: @@ -260,5 +263,5 @@ class PhysicsManager : HandleMap, ISubSystem void Update(); uint32_t AddBody( ModelEntity* entity, PhysicsBody body ); - TraceResult Trace( TraceInfo traceInfo ); + GENERATE_BINDINGS TraceResult Trace( TraceInfo traceInfo ); }; diff --git a/Source/Mocha.Host/material.cpp b/Source/Mocha.Host/Rendering/Assets/material.cpp similarity index 80% rename from Source/Mocha.Host/material.cpp rename to Source/Mocha.Host/Rendering/Assets/material.cpp index 1a23e53a..78930ebb 100644 --- a/Source/Mocha.Host/material.cpp +++ b/Source/Mocha.Host/Rendering/Assets/material.cpp @@ -1,17 +1,18 @@ #include "material.h" -#include -#include -#include -#include -#include - -Material::Material( const char* name, UtilArray vertexShaderData, UtilArray fragmentShaderData, UtilArray vertexAttributes, UtilArray textures, - SamplerType samplerType, bool ignoreDepth ) +#include +#include +#include +#include +#include +#include + +Material::Material( const char* name, UtilArray vertexShaderData, UtilArray fragmentShaderData, + UtilArray vertexAttributes, UtilArray textures, SamplerType samplerType, bool ignoreDepth ) { - m_vertexShaderData = vertexShaderData.GetData(); + m_vertexShaderData = vertexShaderData.GetData(); m_fragmentShaderData = fragmentShaderData.GetData(); - + m_isDirty.store( true ); auto texturePtrs = textures.GetData(); diff --git a/Source/Mocha.Host/material.h b/Source/Mocha.Host/Rendering/Assets/material.h similarity index 87% rename from Source/Mocha.Host/material.h rename to Source/Mocha.Host/Rendering/Assets/material.h index cb046ce4..e642e55c 100644 --- a/Source/Mocha.Host/material.h +++ b/Source/Mocha.Host/Rendering/Assets/material.h @@ -1,9 +1,11 @@ #pragma once -#include -#include -#include +#include +#include +#include #include +class Root; + struct InteropVertexAttributeInfo { const char* name; @@ -42,8 +44,8 @@ class Material bool m_ignoreDepth; bool IsDirty() { return m_isDirty.load( std::memory_order_relaxed ); } - GENERATE_BINDINGS Material( const char* name, UtilArray vertexShaderData, UtilArray fragmentShaderData, UtilArray vertexAttributes, - UtilArray textures, SamplerType samplerType, bool ignoreDepth ); + GENERATE_BINDINGS Material( const char* name, UtilArray vertexShaderData, UtilArray fragmentShaderData, + UtilArray vertexAttributes, UtilArray textures, SamplerType samplerType, bool ignoreDepth ); Material( const Material& other ) noexcept : m_isDirty( other.m_isDirty.load() ) diff --git a/Source/Mocha.Host/mesh.h b/Source/Mocha.Host/Rendering/Assets/mesh.h similarity index 82% rename from Source/Mocha.Host/mesh.h rename to Source/Mocha.Host/Rendering/Assets/mesh.h index e0d64094..abf6ab19 100644 --- a/Source/Mocha.Host/mesh.h +++ b/Source/Mocha.Host/Rendering/Assets/mesh.h @@ -1,8 +1,8 @@ #pragma once -#include -#include -#include +#include +#include +#include struct Mesh { diff --git a/Source/Mocha.Host/model.cpp b/Source/Mocha.Host/Rendering/Assets/model.cpp similarity index 90% rename from Source/Mocha.Host/model.cpp rename to Source/Mocha.Host/Rendering/Assets/model.cpp index a7e2c5c9..11130012 100644 --- a/Source/Mocha.Host/model.cpp +++ b/Source/Mocha.Host/Rendering/Assets/model.cpp @@ -1,9 +1,10 @@ #include "model.h" -#include -#include -#include -#include +#include +#include +#include +#include +#include void Model::UploadMesh( Mesh& mesh ) { diff --git a/Source/Mocha.Host/model.h b/Source/Mocha.Host/Rendering/Assets/model.h similarity index 66% rename from Source/Mocha.Host/model.h rename to Source/Mocha.Host/Rendering/Assets/model.h index a267a706..0a9ad28c 100644 --- a/Source/Mocha.Host/model.h +++ b/Source/Mocha.Host/Rendering/Assets/model.h @@ -1,16 +1,18 @@ #pragma once +#include +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include #include -#include #include +class Root; + class Model { private: @@ -21,6 +23,8 @@ class Model bool m_hasIndexBuffer; bool m_isInitialized; + GENERATE_BINDINGS Model() {} + GENERATE_BINDINGS void AddMesh( const char* name, UtilArray vertices, UtilArray indices, Material* material ); const std::vector GetMeshes() { return m_meshes; } diff --git a/Source/Mocha.Host/texture.cpp b/Source/Mocha.Host/Rendering/Assets/texture.cpp similarity index 87% rename from Source/Mocha.Host/texture.cpp rename to Source/Mocha.Host/Rendering/Assets/texture.cpp index 7b484e94..821d5b7a 100644 --- a/Source/Mocha.Host/texture.cpp +++ b/Source/Mocha.Host/Rendering/Assets/texture.cpp @@ -1,8 +1,9 @@ #include "texture.h" -#include -#include -#include +#include +#include +#include +#include Texture::Texture( const char* name, uint32_t width, uint32_t height ) { diff --git a/Source/Mocha.Host/texture.h b/Source/Mocha.Host/Rendering/Assets/texture.h similarity index 58% rename from Source/Mocha.Host/texture.h rename to Source/Mocha.Host/Rendering/Assets/texture.h index 4d03a247..692a83b2 100644 --- a/Source/Mocha.Host/texture.h +++ b/Source/Mocha.Host/Rendering/Assets/texture.h @@ -1,6 +1,8 @@ #pragma once -#include -#include +#include +#include + +class Root; class Texture { @@ -12,5 +14,6 @@ class Texture GENERATE_BINDINGS Texture( const char* name, uint32_t width, uint32_t height ); GENERATE_BINDINGS void SetData( uint32_t width, uint32_t height, uint32_t mipCount, UtilArray mipData, int imageFormat ); - GENERATE_BINDINGS void Copy( uint32_t srcX, uint32_t srcY, uint32_t dstX, uint32_t dstY, uint32_t width, uint32_t height, Texture* src ); + GENERATE_BINDINGS void Copy( + uint32_t srcX, uint32_t srcY, uint32_t dstX, uint32_t dstY, uint32_t width, uint32_t height, Texture* src ); }; \ No newline at end of file diff --git a/Source/Mocha.Host/Rendering/Platform/Null/nullrendercontext.h b/Source/Mocha.Host/Rendering/Platform/Null/nullrendercontext.h new file mode 100644 index 00000000..efeba07c --- /dev/null +++ b/Source/Mocha.Host/Rendering/Platform/Null/nullrendercontext.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------------------------------------------------------- + +class NullRenderContext : public BaseRenderContext +{ +protected: + // ---------------------------------------- + + RenderStatus CreateImageTexture( ImageTextureInfo_t textureInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus CreateRenderTexture( RenderTextureInfo_t textureInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus SetImageTextureData( Handle handle, TextureData_t pipelineInfo ) override { return RENDER_STATUS_OK; } + RenderStatus CopyImageTexture( Handle handle, TextureCopyData_t pipelineInfo ) override { return RENDER_STATUS_OK; } + + RenderStatus CreateBuffer( BufferInfo_t bufferInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus CreateVertexBuffer( BufferInfo_t bufferInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus CreateIndexBuffer( BufferInfo_t bufferInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus UploadBuffer( Handle handle, BufferUploadInfo_t pipelineInfo ) override { return RENDER_STATUS_OK; } + + RenderStatus CreatePipeline( PipelineInfo_t pipelineInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus CreateDescriptor( DescriptorInfo_t pipelineInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + RenderStatus CreateShader( ShaderInfo_t pipelineInfo, Handle* outHandle ) override { return RENDER_STATUS_OK; } + +public: + // ---------------------------------------- + + /// + RenderStatus Startup() override { return RENDER_STATUS_OK; } + /// + RenderStatus Shutdown() override { return RENDER_STATUS_OK; } + /// + RenderStatus BeginRendering() override { return RENDER_STATUS_OK; } + /// + RenderStatus EndRendering() override { return RENDER_STATUS_OK; } + + // ---------------------------------------- + + /// + RenderStatus BindPipeline( Pipeline p ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus BindDescriptor( Descriptor d ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus UpdateDescriptor( Descriptor d, DescriptorUpdateInfo_t updateInfo ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus BindVertexBuffer( VertexBuffer vb ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus BindIndexBuffer( IndexBuffer ib ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus BindConstants( RenderPushConstants p ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus Draw( uint32_t vertexCount, uint32_t indexCount, uint32_t instanceCount ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus BindRenderTarget( RenderTexture rt ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus GetRenderSize( Size2D* outSize ) override { return RENDER_STATUS_OK; } + + /// + RenderStatus GetWindowSize( Size2D* outSize ) override { return RENDER_STATUS_OK; } + + /// + void UpdateWindow() override {} + + /// + bool GetWindowCloseRequested() override { return false; } + + /// + RenderStatus GetGPUInfo( GPUInfo* outInfo ) override { return RENDER_STATUS_OK; } + + // ---------------------------------------- + + /// + RenderStatus BeginImGui() override { return RENDER_STATUS_OK; } + /// + RenderStatus EndImGui() override { return RENDER_STATUS_OK; } + + /// + RenderStatus GetImGuiTextureID( ImageTexture* texture, void** outTextureId ) override { return RENDER_STATUS_OK; } +}; diff --git a/Source/Mocha.Host/pipeline.cpp b/Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.cpp similarity index 97% rename from Source/Mocha.Host/pipeline.cpp rename to Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.cpp index 10c72daf..844132e5 100644 --- a/Source/Mocha.Host/pipeline.cpp +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.cpp @@ -22,7 +22,7 @@ VkPipeline PipelineBuilder::Build( VkDevice device, VkFormat colorFormat, VkForm viewportState.pViewports = &viewport; viewportState.scissorCount = 1; viewportState.pScissors = &scissor; - + VkPipelineDynamicStateCreateInfo dynamicStateInfo = {}; std::vector dynamicStates = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; @@ -34,7 +34,7 @@ VkPipeline PipelineBuilder::Build( VkDevice device, VkFormat colorFormat, VkForm VkPipelineColorBlendStateCreateInfo colorBlending = {}; colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; colorBlending.pNext = nullptr; - + colorBlending.logicOpEnable = VK_FALSE; colorBlending.logicOp = VK_LOGIC_OP_COPY; colorBlending.attachmentCount = 1; @@ -60,13 +60,13 @@ VkPipeline PipelineBuilder::Build( VkDevice device, VkFormat colorFormat, VkForm pipelineInfo.pRasterizationState = &m_rasterizer; pipelineInfo.pMultisampleState = &m_multisampling; pipelineInfo.pColorBlendState = &colorBlending; - pipelineInfo.pDepthStencilState = &m_depthStencil; + pipelineInfo.pDepthStencilState = &m_depthStencil; pipelineInfo.pDynamicState = &dynamicStateInfo; pipelineInfo.layout = m_pipelineLayout; pipelineInfo.subpass = 0; pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.renderPass = VK_NULL_HANDLE; - + VkPipeline newPipeline; VK_CHECK( vkCreateGraphicsPipelines( device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &newPipeline ) ); return newPipeline; diff --git a/Source/Mocha.Host/pipeline.h b/Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.h similarity index 93% rename from Source/Mocha.Host/pipeline.h rename to Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.h index 81d26634..8bc83078 100644 --- a/Source/Mocha.Host/pipeline.h +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/pipeline.h @@ -1,6 +1,6 @@ #pragma once +#include #include -#include class PipelineBuilder { diff --git a/Source/Mocha.Host/vkinit.h b/Source/Mocha.Host/Rendering/Platform/Vulkan/vkinit.h similarity index 99% rename from Source/Mocha.Host/vkinit.h rename to Source/Mocha.Host/Rendering/Platform/Vulkan/vkinit.h index 4518187a..85c8061c 100644 --- a/Source/Mocha.Host/vkinit.h +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/vkinit.h @@ -1,6 +1,6 @@ #pragma once -#include -#include +#include +#include namespace VKInit { diff --git a/Source/Mocha.Host/vkmacros.h b/Source/Mocha.Host/Rendering/Platform/Vulkan/vkmacros.h similarity index 92% rename from Source/Mocha.Host/vkmacros.h rename to Source/Mocha.Host/Rendering/Platform/Vulkan/vkmacros.h index dd719810..480ac66e 100644 --- a/Source/Mocha.Host/vkmacros.h +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/vkmacros.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include #include @@ -12,6 +12,6 @@ { \ std::string result = std::string( "Vulkan error: " ) + std::string( string_VkResult( err ) ); \ ErrorMessage( result ); \ - __debugbreak(); \ + abort(); \ } \ } while ( 0 ) \ No newline at end of file diff --git a/Source/Mocha.Host/vulkanrendercontext.cpp b/Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.cpp similarity index 99% rename from Source/Mocha.Host/vulkanrendercontext.cpp rename to Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.cpp index c47027b6..9ecddafe 100644 --- a/Source/Mocha.Host/vulkanrendercontext.cpp +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.cpp @@ -1,18 +1,21 @@ #include "vulkanrendercontext.h" -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include +#if 0 #define VMA_DEBUG_LOG( format, ... ) \ { \ - /* Use snprintf->spdlog::trace */ \ char buffer[1024]; \ snprintf( buffer, 1024, format, ##__VA_ARGS__ ); \ spdlog::trace( buffer ); \ } +#endif #define VMA_IMPLEMENTATION #include @@ -149,7 +152,7 @@ VulkanRenderTexture::VulkanRenderTexture( VulkanRenderContext* parent, RenderTex VmaAllocationCreateInfo allocInfo = {}; allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; allocInfo.requiredFlags = VkMemoryPropertyFlags( VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT ); - + vmaCreateImage( m_parent->m_allocator, &imageInfo, &allocInfo, &image, &allocation, nullptr ); VkImageViewCreateInfo viewInfo = VKInit::ImageViewCreateInfo( format, image, GetAspectFlags( textureInfo.type ), 1 ); @@ -178,7 +181,7 @@ void VulkanImageTexture::SetData( TextureData_t textureData ) { // Destroy old image Delete(); - + VkFormat imageFormat = ( VkFormat )textureData.imageFormat; uint32_t imageSize = 0; @@ -215,7 +218,7 @@ void VulkanImageTexture::SetData( TextureData_t textureData ) VmaAllocationCreateInfo allocInfo = {}; allocInfo.usage = VMA_MEMORY_USAGE_AUTO; - + vmaCreateImage( m_parent->m_allocator, &imageCreateInfo, &allocInfo, &image, &allocation, nullptr ); vmaSetAllocationName( m_parent->m_allocator, allocation, textureInfo.name.c_str() ); @@ -604,7 +607,8 @@ vkb::Instance VulkanRenderContext::CreateInstanceAndSurface() vkb::InstanceBuilder builder; vkb::Instance vkbInstance; - auto ret = builder.set_app_name( g_projectManager->GetProject().name.c_str() ) + auto ret = builder + .set_app_name( Globals::m_projectManager->GetProject().name.c_str() ) // Fuck this .set_engine_name( ENGINE_NAME ) .request_validation_layers( true ) .require_api_version( 1, 3, 0 ) @@ -729,8 +733,6 @@ vkb::PhysicalDevice VulkanRenderContext::CreatePhysicalDevice( vkb::Instance vkb std::string errorStr = "Couldn't find valid physical device: " + error.type.message(); ErrorMessage( errorStr ); - - // Exit exit( error.type.value() ); } @@ -746,7 +748,7 @@ void VulkanRenderContext::CreateSwapchain() m_window->m_onWindowResized = [&]( Size2D newSize ) { m_swapchain.Update( newSize ); CreateRenderTargets(); - g_hostManager->FireEvent( "Event.Window.Resized" ); + Globals::m_hostManager->FireEvent( "Event.Window.Resized" ); }; } diff --git a/Source/Mocha.Host/vulkanrendercontext.h b/Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.h similarity index 98% rename from Source/Mocha.Host/vulkanrendercontext.h rename to Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.h index 4d9ca85c..356f55c6 100644 --- a/Source/Mocha.Host/vulkanrendercontext.h +++ b/Source/Mocha.Host/Rendering/Platform/Vulkan/vulkanrendercontext.h @@ -1,18 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include #include -#include -#include -#include -#include -#include #include #include #include -#include #include -#include // ---------------------------------------------------------------------------------------------------------------------------- diff --git a/Source/Mocha.Host/baserendercontext.cpp b/Source/Mocha.Host/Rendering/baserendercontext.cpp similarity index 70% rename from Source/Mocha.Host/baserendercontext.cpp rename to Source/Mocha.Host/Rendering/baserendercontext.cpp index b057b655..1e65478a 100644 --- a/Source/Mocha.Host/baserendercontext.cpp +++ b/Source/Mocha.Host/Rendering/baserendercontext.cpp @@ -1,5 +1,7 @@ #include "baserendercontext.h" +#include + float lastRenderScale = -1.0f; // TODO: Cvar hooks so that we can change things when cvars change (i.e. re-create render targets when @@ -10,69 +12,69 @@ FloatCVar renderScale( "render.scale", 1.0f, CVarFlags::Archive, "Multiplier for ImageTexture::ImageTexture( ImageTextureInfo_t info ) { - g_renderContext->CreateImageTexture( info, &m_handle ); + Globals::m_renderContext->CreateImageTexture( info, &m_handle ); } void ImageTexture::SetData( TextureData_t textureData ) { - g_renderContext->SetImageTextureData( m_handle, textureData ); + Globals::m_renderContext->SetImageTextureData( m_handle, textureData ); } void ImageTexture::Copy( TextureCopyData_t copyData ) { - g_renderContext->CopyImageTexture( m_handle, copyData ); + Globals::m_renderContext->CopyImageTexture( m_handle, copyData ); } // ---------------------------------------------------------------------------------------------------- BaseBuffer::BaseBuffer( BufferInfo_t info ) { - g_renderContext->CreateBuffer( info, &m_handle ); + Globals::m_renderContext->CreateBuffer( info, &m_handle ); } void BaseBuffer::Upload( BufferUploadInfo_t uploadInfo ) { - g_renderContext->UploadBuffer( m_handle, uploadInfo ); + Globals::m_renderContext->UploadBuffer( m_handle, uploadInfo ); } // ---------------------------------------------------------------------------------------------------- VertexBuffer::VertexBuffer( BufferInfo_t info ) { - g_renderContext->CreateVertexBuffer( info, &m_handle ); + Globals::m_renderContext->CreateVertexBuffer( info, &m_handle ); } IndexBuffer::IndexBuffer( BufferInfo_t info ) { - g_renderContext->CreateIndexBuffer( info, &m_handle ); + Globals::m_renderContext->CreateIndexBuffer( info, &m_handle ); } // ---------------------------------------------------------------------------------------------------- RenderTexture::RenderTexture( RenderTextureInfo_t info ) { - g_renderContext->CreateRenderTexture( info, &m_handle ); + Globals::m_renderContext->CreateRenderTexture( info, &m_handle ); } // ---------------------------------------------------------------------------------------------------- Descriptor::Descriptor( DescriptorInfo_t info ) { - g_renderContext->CreateDescriptor( info, &m_handle ); + Globals::m_renderContext->CreateDescriptor( info, &m_handle ); } // ---------------------------------------------------------------------------------------------------- Pipeline::Pipeline( PipelineInfo_t info ) { - g_renderContext->CreatePipeline( info, &m_handle ); + Globals::m_renderContext->CreatePipeline( info, &m_handle ); } // ---------------------------------------------------------------------------------------------------- Shader::Shader( ShaderInfo_t info ) { - g_renderContext->CreateShader( info, &m_handle ); + Globals::m_renderContext->CreateShader( info, &m_handle ); } // ---------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Source/Mocha.Host/baserendercontext.h b/Source/Mocha.Host/Rendering/baserendercontext.h similarity index 99% rename from Source/Mocha.Host/baserendercontext.h rename to Source/Mocha.Host/Rendering/baserendercontext.h index cc6b1571..477ceac7 100644 --- a/Source/Mocha.Host/baserendercontext.h +++ b/Source/Mocha.Host/Rendering/baserendercontext.h @@ -1,12 +1,12 @@ #pragma once -#include +#include +#include +#include +#include #include -#include #include -#include // ---------------------------------------------------------------------------------------------------- - extern FloatCVar renderScale; extern float lastRenderScale; @@ -28,6 +28,7 @@ struct Mesh; class BaseRenderContext; class ImageTexture; class Descriptor; +class Root; // ---------------------------------------------------------------------------------------------------- @@ -235,7 +236,6 @@ class BaseBuffer : public RenderObject { public: BaseBuffer() {} - BaseBuffer( BufferInfo_t info ); void Upload( BufferUploadInfo_t uploadInfo ); }; @@ -323,6 +323,7 @@ inline void ErrorIf( bool condition, RenderStatus status ) { std::string error = "RenderContext Error: " + GetRenderContextStatusString( status ); ErrorMessage( error ); + abort(); } } @@ -348,7 +349,6 @@ class BaseRenderContext // The current vertex buffer VertexBuffer* m_currentVertexBuffer; - // ---------------------------------------- // Objects // ---------------------------------------- @@ -386,7 +386,6 @@ class BaseRenderContext virtual RenderStatus Startup() = 0; virtual RenderStatus Shutdown() = 0; - // ---------------------------------------- // Rendering commands // ---------------------------------------- @@ -420,7 +419,6 @@ class BaseRenderContext /// virtual RenderStatus EndRendering() = 0; - // ---------------------------------------- // Low-level rendering // ---------------------------------------- @@ -485,7 +483,6 @@ class BaseRenderContext /// RENDER_STATUS_OK if successful, otherwise an error code virtual RenderStatus GetGPUInfo( GPUInfo* outInfo ) = 0; - // ---------------------------------------- // High-level rendering // ---------------------------------------- @@ -532,7 +529,6 @@ class BaseRenderContext /// virtual bool GetWindowCloseRequested() = 0; - // ---------------------------------------- // ImGui // ---------------------------------------- diff --git a/Source/Mocha.Host/renderdocmanager.cpp b/Source/Mocha.Host/Rendering/renderdocmanager.cpp similarity index 94% rename from Source/Mocha.Host/renderdocmanager.cpp rename to Source/Mocha.Host/Rendering/renderdocmanager.cpp index cb7d8d21..66319e6c 100644 --- a/Source/Mocha.Host/renderdocmanager.cpp +++ b/Source/Mocha.Host/Rendering/renderdocmanager.cpp @@ -1,6 +1,8 @@ #include "renderdocmanager.h" -#include +#include +#include +#include #include #include diff --git a/Source/Mocha.Host/renderdocmanager.h b/Source/Mocha.Host/Rendering/renderdocmanager.h similarity index 77% rename from Source/Mocha.Host/renderdocmanager.h rename to Source/Mocha.Host/Rendering/renderdocmanager.h index 21917583..57fec9a0 100644 --- a/Source/Mocha.Host/renderdocmanager.h +++ b/Source/Mocha.Host/Rendering/renderdocmanager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include class RenderdocManager : ISubSystem { diff --git a/Source/Mocha.Host/Rendering/rendering.h b/Source/Mocha.Host/Rendering/rendering.h new file mode 100644 index 00000000..cb6f31b7 --- /dev/null +++ b/Source/Mocha.Host/Rendering/rendering.h @@ -0,0 +1,3 @@ +#pragma once + +#include \ No newline at end of file diff --git a/Source/Mocha.Host/rendermanager.cpp b/Source/Mocha.Host/Rendering/rendermanager.cpp similarity index 67% rename from Source/Mocha.Host/rendermanager.cpp rename to Source/Mocha.Host/Rendering/rendermanager.cpp index 2aaf5983..2e1b0c14 100644 --- a/Source/Mocha.Host/rendermanager.cpp +++ b/Source/Mocha.Host/Rendering/rendermanager.cpp @@ -3,26 +3,25 @@ // // // -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include // // // +#include #include #include #include #include -#include // // @@ -30,6 +29,12 @@ #include #include +// +// +// +#include +#include + // // // @@ -86,10 +91,20 @@ void RenderManager::RenderMesh( RenderPushConstants constants, Mesh* mesh ) void RenderManager::Startup() { - g_renderManager = this; + Globals::m_renderManager = this; - m_renderContext = std::make_unique(); - g_renderContext = m_renderContext.get(); + if ( Globals::m_executingRealm == REALM_CLIENT ) + { + // Client uses Vulkan for rendering + m_renderContext = std::make_unique(); + } + else + { + // Server is headless - use a null render context + m_renderContext = std::make_unique(); + } + + Globals::m_renderContext = m_renderContext.get(); m_renderContext->Startup(); } @@ -105,9 +120,9 @@ void RenderManager::RenderEntity( ModelEntity* entity ) RenderPushConstants constants = {}; constants.modelMatrix = entity->m_transform.GetModelMatrix(); constants.renderMatrix = CalculateViewProjMatrix() * constants.modelMatrix; - constants.cameraPos = g_cameraPos.ToGLM(); - constants.time = g_curTime; - constants.data.x = ( int )g_debugView; + constants.cameraPos = Globals::m_cameraPos.ToGLM(); + constants.time = Globals::m_curTime; + constants.data.x = ( int )Globals::m_debugView; std::vector lightPositions = {}; lightPositions.push_back( { 0, 4, 4 } ); @@ -134,18 +149,26 @@ void RenderManager::RenderEntity( ModelEntity* entity ) void RenderManager::DrawOverlaysAndEditor() { + // Server is headless - no overlays or editor + if ( Globals::m_executingRealm == REALM_SERVER ) + return; + m_renderContext->BeginImGui(); ImGui::NewFrame(); ImGui::DockSpaceOverViewport( nullptr, ImGuiDockNodeFlags_PassthruCentralNode ); - g_hostManager->Render(); - g_hostManager->DrawEditor(); + Globals::m_hostManager->Render(); + Globals::m_hostManager->DrawEditor(); m_renderContext->EndImGui(); } void RenderManager::DrawGame() { + // Server is headless - don't render + if ( Globals::m_executingRealm == REALM_SERVER ) + return; + RenderStatus res = m_renderContext->BeginRendering(); if ( res == RENDER_STATUS_WINDOW_SIZE_INVALID ) @@ -154,7 +177,7 @@ void RenderManager::DrawGame() auto viewProjMatrix = CalculateViewProjMatrix(); auto viewmodelViewProjMatrix = CalculateViewmodelViewProjMatrix(); - g_entityDictionary->ForEachSpecific( [&]( std::shared_ptr entity ) { + Globals::m_entityManager->ForEachSpecific( [&]( std::shared_ptr entity ) { if ( !entity->HasFlag( EntityFlags::ENTITY_VIEWMODEL ) && !entity->HasFlag( EntityFlags::ENTITY_UI ) ) RenderEntity( entity.get() ); } ); @@ -162,7 +185,7 @@ void RenderManager::DrawGame() // // Render viewmodels // - g_entityDictionary->ForEachSpecific( [&]( std::shared_ptr entity ) { + Globals::m_entityManager->ForEachSpecific( [&]( std::shared_ptr entity ) { if ( entity->HasFlag( EntityFlags::ENTITY_VIEWMODEL ) ) RenderEntity( entity.get() ); } ); @@ -170,7 +193,7 @@ void RenderManager::DrawGame() // // Render UI last // - g_entityDictionary->ForEachSpecific( [&]( std::shared_ptr entity ) { + Globals::m_entityManager->ForEachSpecific( [&]( std::shared_ptr entity ) { if ( entity->HasFlag( EntityFlags::ENTITY_UI ) ) RenderEntity( entity.get() ); } ); @@ -186,11 +209,11 @@ glm::mat4 RenderManager::CalculateViewmodelViewProjMatrix() float aspect = ( float )extent.x / ( float )extent.y; glm::vec3 up = glm::vec3( 0, 0, -1 ); - glm::vec3 direction = glm::normalize( glm::rotate( g_cameraRot.ToGLM(), glm::vec3( 1, 0, 0 ) ) ); - glm::vec3 position = g_cameraPos.ToGLM(); + glm::vec3 direction = glm::normalize( glm::rotate( Globals::m_cameraRot.ToGLM(), glm::vec3( 1, 0, 0 ) ) ); + glm::vec3 position = Globals::m_cameraPos.ToGLM(); viewMatrix = glm::lookAt( position, position + direction, up ); - projMatrix = glm::perspective( glm::radians( 60.0f ), aspect, g_cameraZNear, g_cameraZFar ); + projMatrix = glm::perspective( glm::radians( 60.0f ), aspect, Globals::m_cameraZNear, Globals::m_cameraZFar ); return projMatrix * viewMatrix; } @@ -203,11 +226,12 @@ glm::mat4 RenderManager::CalculateViewProjMatrix() float aspect = ( float )extent.x / ( float )extent.y; glm::vec3 up = glm::vec3( 0, 0, -1 ); - glm::vec3 direction = glm::normalize( glm::rotate( g_cameraRot.ToGLM(), glm::vec3( 1, 0, 0 ) ) ); - glm::vec3 position = g_cameraPos.ToGLM(); + glm::vec3 direction = glm::normalize( glm::rotate( Globals::m_cameraRot.ToGLM(), glm::vec3( 1, 0, 0 ) ) ); + glm::vec3 position = Globals::m_cameraPos.ToGLM(); viewMatrix = glm::lookAt( position, position + direction, up ); - projMatrix = glm::perspective( glm::radians( g_cameraFov ), aspect, g_cameraZNear, g_cameraZFar ); + projMatrix = + glm::perspective( glm::radians( Globals::m_cameraFov ), aspect, Globals::m_cameraZNear, Globals::m_cameraZFar ); return projMatrix * viewMatrix; } diff --git a/Source/Mocha.Host/rendermanager.h b/Source/Mocha.Host/Rendering/rendermanager.h similarity index 89% rename from Source/Mocha.Host/rendermanager.h rename to Source/Mocha.Host/Rendering/rendermanager.h index 65795677..c8111bb5 100644 --- a/Source/Mocha.Host/rendermanager.h +++ b/Source/Mocha.Host/Rendering/rendermanager.h @@ -1,13 +1,13 @@ #pragma once -#include -#include +#include +#include +#include +#include #include #include #include -#include #include -#include class ModelEntity; diff --git a/Source/Mocha.Host/shadercompiler.cpp b/Source/Mocha.Host/Rendering/shadercompiler.cpp similarity index 100% rename from Source/Mocha.Host/shadercompiler.cpp rename to Source/Mocha.Host/Rendering/shadercompiler.cpp diff --git a/Source/Mocha.Host/shadercompiler.h b/Source/Mocha.Host/Rendering/shadercompiler.h similarity index 95% rename from Source/Mocha.Host/shadercompiler.h rename to Source/Mocha.Host/Rendering/shadercompiler.h index 862b7fbb..d309a341 100644 --- a/Source/Mocha.Host/shadercompiler.h +++ b/Source/Mocha.Host/Rendering/shadercompiler.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include // diff --git a/Source/Mocha.Host/window.cpp b/Source/Mocha.Host/Rendering/window.cpp similarity index 89% rename from Source/Mocha.Host/window.cpp rename to Source/Mocha.Host/Rendering/window.cpp index ac9c6dd1..1a8e5e87 100644 --- a/Source/Mocha.Host/window.cpp +++ b/Source/Mocha.Host/Rendering/window.cpp @@ -1,10 +1,11 @@ #include "window.h" +#include +#include +#include +#include +#include #include -#include -#include -#include -#include #ifdef _IMGUI #include @@ -23,7 +24,10 @@ Window::Window( uint32_t width, uint32_t height ) SDL_WindowFlags windowFlags = ( SDL_WindowFlags )( SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN ); m_visible = false; - m_window = SDL_CreateWindow( WINDOW_TITLE, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, windowFlags ); + std::string windowTitle = std::string( Globals::m_projectManager->GetProject().name + " [" + + Globals::m_projectManager->GetProject().version + "]" ); + m_window = + SDL_CreateWindow( windowTitle.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, windowFlags ); // Check for any window creation errors and display them to the user. This can be things like, "we failed to create a // window because you don't have a Vulkan-capable GPU", so it's good to catch this early on. @@ -60,7 +64,7 @@ void Window::Update() { SDL_Event e; - InputState inputState = g_inputManager->GetState(); + InputState inputState = Globals::m_inputManager->GetState(); // Clear mouse delta every frame inputState.mouseDelta = { 0, 0 }; @@ -172,5 +176,5 @@ void Window::Update() #endif } - g_inputManager->SetState( inputState ); + Globals::m_inputManager->SetState( inputState ); } diff --git a/Source/Mocha.Host/window.h b/Source/Mocha.Host/Rendering/window.h similarity index 96% rename from Source/Mocha.Host/window.h rename to Source/Mocha.Host/Rendering/window.h index f680d626..abe09b38 100644 --- a/Source/Mocha.Host/window.h +++ b/Source/Mocha.Host/Rendering/window.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include -#include #include +class Root; + class Window { private: diff --git a/Source/Mocha.Host/Root/clientroot.cpp b/Source/Mocha.Host/Root/clientroot.cpp new file mode 100644 index 00000000..1789258e --- /dev/null +++ b/Source/Mocha.Host/Root/clientroot.cpp @@ -0,0 +1,9 @@ +#include "clientroot.h" + +#include +#include + +bool ClientRoot::GetQuitRequested() +{ + return Globals::m_renderContext->GetWindowCloseRequested(); +} \ No newline at end of file diff --git a/Source/Mocha.Host/Root/clientroot.h b/Source/Mocha.Host/Root/clientroot.h new file mode 100644 index 00000000..54960b0f --- /dev/null +++ b/Source/Mocha.Host/Root/clientroot.h @@ -0,0 +1,15 @@ +#pragma once +#include + +class ClientRoot : public Root +{ +protected: + bool GetQuitRequested() override; + +public: + ClientRoot() + { + m_instance = this; + Globals::m_executingRealm = REALM_CLIENT; + } +}; \ No newline at end of file diff --git a/Source/Mocha.Host/Root/root.cpp b/Source/Mocha.Host/Root/root.cpp new file mode 100644 index 00000000..365eec1f --- /dev/null +++ b/Source/Mocha.Host/Root/root.cpp @@ -0,0 +1,215 @@ +#include "root.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void Root::Startup() +{ + Globals::m_logManager = new LogManager(); + Globals::m_logManager->Startup(); + + Globals::m_cvarManager = new CVarManager(); + Globals::m_cvarManager->Startup(); + + Globals::m_projectManager = new ProjectManager(); + Globals::m_projectManager->Startup(); + + Globals::m_entityManager = new EntityManager(); + Globals::m_entityManager->Startup(); + + Globals::m_physicsManager = new PhysicsManager(); + Globals::m_physicsManager->Startup(); + + Globals::m_renderdocManager = new RenderdocManager(); + Globals::m_renderdocManager->Startup(); + + Globals::m_inputManager = new InputManager(); + Globals::m_inputManager->Startup(); + + Globals::m_renderManager = new RenderManager(); + Globals::m_renderManager->Startup(); + + Globals::m_editorManager = new EditorManager(); + Globals::m_editorManager->Startup(); + + Globals::m_hostManager = new HostManager(); + Globals::m_hostManager->Startup(); +} + +void Root::Shutdown() +{ + Globals::m_hostManager->Shutdown(); + Globals::m_editorManager->Shutdown(); + Globals::m_renderManager->Shutdown(); + Globals::m_inputManager->Shutdown(); + Globals::m_renderdocManager->Shutdown(); + Globals::m_physicsManager->Shutdown(); + Globals::m_entityManager->Shutdown(); + Globals::m_projectManager->Shutdown(); + Globals::m_cvarManager->Shutdown(); + Globals::m_logManager->Shutdown(); +} + +const char* Root::GetProjectPath() +{ + std::string str = EngineProperties::LoadedProject.GetValue(); + + // Copy string so we can use it out-of-scope + char* cstr = new char[str.length() + 1]; + strcpy_s( cstr, str.length() + 1, str.c_str() ); + + return cstr; +} + +uint32_t Root::CreateBaseEntity() +{ + auto* entityDictionary = Globals::m_entityManager; + + BaseEntity baseEntity = {}; + baseEntity.AddFlag( ENTITY_MANAGED ); + baseEntity.m_type = "BaseEntity"; + + return entityDictionary->AddEntity( baseEntity ); +} + +uint32_t Root::CreateModelEntity() +{ + auto* entityDictionary = Globals::m_entityManager; + + ModelEntity modelEntity = {}; + modelEntity.AddFlag( ENTITY_MANAGED ); + modelEntity.AddFlag( ENTITY_RENDERABLE ); + modelEntity.m_type = "ModelEntity"; + + return entityDictionary->AddEntity( modelEntity ); +} + +double HiresTimeInSeconds() +{ + return std::chrono::duration_cast>( + std::chrono::high_resolution_clock::now().time_since_epoch() ) + .count(); +} + +void Root::Run() +{ + Globals::m_hostManager->FireEvent( "Event.Game.Load" ); + + double logicDelta = 1.0 / Globals::m_projectManager->GetProject().properties.tickRate; + + double currentTime = HiresTimeInSeconds(); + double accumulator = 0.0; + + while ( !m_shouldQuit ) + { + double newTime = HiresTimeInSeconds(); + double loopDeltaTime = newTime - currentTime; + + // How quick did we do last frame? Let's limit ourselves if (1.0f / loopDeltaTime) is more than maxLoopHz + float loopHz = 1.0f / loopDeltaTime; + + // TODO: Server / client. Perhaps abstract this and set it to the tickrate if we're a dedicated server? + float maxLoopHz = maxFramerate.GetValue(); + + if ( maxLoopHz > 0 && loopHz > maxLoopHz ) + { + continue; + } + + if ( loopDeltaTime > 1 / 30.0f ) + loopDeltaTime = 1 / 30.0f; + + currentTime = newTime; + accumulator += loopDeltaTime; + + // + // How long has it been since we last updated the game logic? + // We want to update as many times as we can in this frame in + // order to match the desired tick rate. + // + while ( accumulator >= logicDelta ) + { + // Assign previous transforms to all entities + Globals::m_entityManager->ForEach( + [&]( std::shared_ptr entity ) { entity->m_transformLastFrame = entity->m_transformCurrentFrame; } ); + + Globals::m_tickDeltaTime = ( float )logicDelta; + + // Update physics + Globals::m_physicsManager->Update(); + + // Update game + Globals::m_hostManager->Update(); + + // TODO: Server / client + // #ifndef DEDICATED_SERVER + // Update window + Globals::m_renderContext->UpdateWindow(); + // #endif + + if ( GetQuitRequested() ) + { + m_shouldQuit = true; + break; + } + + // Assign current transforms to all entities + Globals::m_entityManager->ForEach( + [&]( std::shared_ptr entity ) { entity->m_transformCurrentFrame = entity->m_transform; } ); + + Globals::m_curTime += logicDelta; + accumulator -= logicDelta; + Globals::m_curTick++; + } + + Globals::m_frameDeltaTime = ( float )loopDeltaTime; + + // TODO: Server / client + // #ifndef DEDICATED_SERVER + // Render + { + const double alpha = accumulator / logicDelta; + + // Assign interpolated transforms to all entities + Globals::m_entityManager->ForEach( [&]( std::shared_ptr entity ) { + // If this entity was spawned in just now, don't interpolate + if ( entity->m_spawnTime == Globals::m_curTick ) + return; + + entity->m_transform = + Transform::Lerp( entity->m_transformLastFrame, entity->m_transformCurrentFrame, ( float )alpha ); + } ); + + Globals::m_renderManager->DrawOverlaysAndEditor(); + + Globals::m_renderManager->DrawGame(); + } + // #endif + } +} + +Vector2 Root::GetWindowSize() +{ + Size2D size; + Globals::m_renderContext->GetWindowSize( &size ); + return { ( float )size.x, ( float )size.y }; +} + +Vector2 Root::GetRenderSize() +{ + Size2D size; + Globals::m_renderContext->GetRenderSize( &size ); + return { ( float )size.x, ( float )size.y }; +} diff --git a/Source/Mocha.Host/Root/root.h b/Source/Mocha.Host/Root/root.h new file mode 100644 index 00000000..7bf8862e --- /dev/null +++ b/Source/Mocha.Host/Root/root.h @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include +#include + +class RenderManager; +class RenderdocManager; +class HostManager; +class LogManager; +class EntityManager; +class PhysicsManager; +class InputManager; +class BaseRenderContext; +class CVarManager; +class ProjectManager; +class EditorManager; + +class Root +{ +protected: + inline static Root* m_instance; + + bool m_shouldQuit = false; + virtual bool GetQuitRequested() { return false; } + +public: + void Startup(); + void Run(); + void Shutdown(); + + static Root* GetInstance() { return m_instance; } + + // + // Managed bindings for things we want to access from C# + // + GENERATE_BINDINGS LogManager* GetLogManager() { return Globals::m_logManager; } + GENERATE_BINDINGS EntityManager* GetEntityManager() { return Globals::m_entityManager; } + GENERATE_BINDINGS InputManager* GetInputManager() { return Globals::m_inputManager; } + GENERATE_BINDINGS CVarManager* GetCVarManager() { return Globals::m_cvarManager; } + GENERATE_BINDINGS PhysicsManager* GetPhysicsManager() { return Globals::m_physicsManager; } + GENERATE_BINDINGS EditorManager* GetEditorManager() { return Globals::m_editorManager; } + + // We aren't using these: + // GENERATE_BINDINGS ProjectManager* GetProjectManager() { return Globals::m_projectManager; } + // GENERATE_BINDINGS RenderManager* GetRenderManager() { return Globals::m_renderManager; } + // GENERATE_BINDINGS RenderdocManager* GetRenderdocManager() { return Globals::m_renderdocManager; } + // GENERATE_BINDINGS HostManager* GetHostManager() { return Globals::m_hostManager; } + // GENERATE_BINDINGS BaseRenderContext* GetRenderContext() { return Globals::m_renderContext; } + + GENERATE_BINDINGS void Quit() { m_shouldQuit = true; } + + GENERATE_BINDINGS inline int GetCurrentTick() { return Globals::m_curTick; } + GENERATE_BINDINGS inline float GetFrameDeltaTime() { return Globals::m_frameDeltaTime; } + GENERATE_BINDINGS inline float GetTickDeltaTime() { return Globals::m_tickDeltaTime; } + GENERATE_BINDINGS inline float GetFramesPerSecond() { return 1.0f / Globals::m_frameDeltaTime; } + GENERATE_BINDINGS inline float GetTime() { return Globals::m_curTime; } + GENERATE_BINDINGS inline bool IsDedicatedServer() { return Globals::m_isDedicatedServer; } + + GENERATE_BINDINGS const char* GetProjectPath(); + + GENERATE_BINDINGS uint32_t CreateBaseEntity(); + GENERATE_BINDINGS uint32_t CreateModelEntity(); + + GENERATE_BINDINGS inline void SetCameraPosition( Vector3 position ) { Globals::m_cameraPos = position; } + GENERATE_BINDINGS inline Vector3 GetCameraPosition() { return Globals::m_cameraPos; } + + GENERATE_BINDINGS inline void SetCameraRotation( Quaternion rotation ) { Globals::m_cameraRot = rotation; } + GENERATE_BINDINGS inline Quaternion GetCameraRotation() { return Globals::m_cameraRot; } + + GENERATE_BINDINGS inline void SetCameraFieldOfView( float fov ) { Globals::m_cameraFov = fov; } + GENERATE_BINDINGS inline float GetCameraFieldOfView() { return Globals::m_cameraFov; } + + GENERATE_BINDINGS inline void SetCameraZNear( float znear ) { Globals::m_cameraZNear = znear; } + GENERATE_BINDINGS inline float GetCameraZNear() { return Globals::m_cameraZNear; } + + GENERATE_BINDINGS inline void SetCameraZFar( float zfar ) { Globals::m_cameraZFar = zfar; } + GENERATE_BINDINGS inline float GetCameraZFar() { return Globals::m_cameraZFar; } + + GENERATE_BINDINGS Vector2 GetWindowSize(); + GENERATE_BINDINGS Vector2 GetRenderSize(); +}; diff --git a/Source/Mocha.Host/Root/serverroot.cpp b/Source/Mocha.Host/Root/serverroot.cpp new file mode 100644 index 00000000..94ff02fa --- /dev/null +++ b/Source/Mocha.Host/Root/serverroot.cpp @@ -0,0 +1,10 @@ +#include "serverroot.h" + +#include +#include + +bool ServerRoot::GetQuitRequested() +{ + // TODO: Server quit + return false; +} \ No newline at end of file diff --git a/Source/Mocha.Host/Root/serverroot.h b/Source/Mocha.Host/Root/serverroot.h new file mode 100644 index 00000000..6d01dc4c --- /dev/null +++ b/Source/Mocha.Host/Root/serverroot.h @@ -0,0 +1,15 @@ +#pragma once +#include + +class ServerRoot : public Root +{ +protected: + bool GetQuitRequested() override; + +public: + ServerRoot() + { + m_instance = this; + Globals::m_executingRealm = REALM_SERVER; + } +}; \ No newline at end of file diff --git a/Source/Mocha.Host/fontawesome.cpp b/Source/Mocha.Host/Thirdparty/FontAwesome/fontawesome.cpp similarity index 100% rename from Source/Mocha.Host/fontawesome.cpp rename to Source/Mocha.Host/Thirdparty/FontAwesome/fontawesome.cpp diff --git a/Source/Mocha.Host/fontawesome.h b/Source/Mocha.Host/Thirdparty/FontAwesome/fontawesome.h similarity index 100% rename from Source/Mocha.Host/fontawesome.h rename to Source/Mocha.Host/Thirdparty/FontAwesome/fontawesome.h diff --git a/Source/Mocha.Host/thirdparty/JoltPhysics b/Source/Mocha.Host/Thirdparty/JoltPhysics similarity index 100% rename from Source/Mocha.Host/thirdparty/JoltPhysics rename to Source/Mocha.Host/Thirdparty/JoltPhysics diff --git a/Source/Mocha.Host/thirdparty/Renderdoc/renderdoc_app.h b/Source/Mocha.Host/Thirdparty/Renderdoc/renderdoc_app.h similarity index 100% rename from Source/Mocha.Host/thirdparty/Renderdoc/renderdoc_app.h rename to Source/Mocha.Host/Thirdparty/Renderdoc/renderdoc_app.h diff --git a/Source/Mocha.Host/thirdparty/imgui b/Source/Mocha.Host/Thirdparty/imgui similarity index 100% rename from Source/Mocha.Host/thirdparty/imgui rename to Source/Mocha.Host/Thirdparty/imgui diff --git a/Source/Mocha.Host/thirdparty/implot b/Source/Mocha.Host/Thirdparty/implot similarity index 100% rename from Source/Mocha.Host/thirdparty/implot rename to Source/Mocha.Host/Thirdparty/implot diff --git a/Source/Mocha.Host/thirdparty/vk-bootstrap b/Source/Mocha.Host/Thirdparty/vk-bootstrap similarity index 100% rename from Source/Mocha.Host/thirdparty/vk-bootstrap rename to Source/Mocha.Host/Thirdparty/vk-bootstrap diff --git a/Source/Mocha.Host/thirdparty/volk b/Source/Mocha.Host/Thirdparty/volk similarity index 100% rename from Source/Mocha.Host/thirdparty/volk rename to Source/Mocha.Host/Thirdparty/volk diff --git a/Source/Mocha.Host/Util/util.h b/Source/Mocha.Host/Util/util.h new file mode 100644 index 00000000..6a4f455a --- /dev/null +++ b/Source/Mocha.Host/Util/util.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/Source/Mocha.Host/utilarray.h b/Source/Mocha.Host/Util/utilarray.h similarity index 97% rename from Source/Mocha.Host/utilarray.h rename to Source/Mocha.Host/Util/utilarray.h index 03256062..86017fbe 100644 --- a/Source/Mocha.Host/utilarray.h +++ b/Source/Mocha.Host/Util/utilarray.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include struct UtilArray diff --git a/Source/Mocha.Host/baseentity.h b/Source/Mocha.Host/baseentity.h deleted file mode 100644 index 6a6228d8..00000000 --- a/Source/Mocha.Host/baseentity.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include -#include -#include -#include - -enum EntityFlags : int -{ - ENTITY_NONE = 1 << 0, - ENTITY_MANAGED = 1 << 1, - ENTITY_RENDERABLE = 1 << 2, - ENTITY_VIEWMODEL = 1 << 3, - ENTITY_UI = 1 << 4, -}; - -DEFINE_FLAG_OPERATORS( EntityFlags ); - -class Camera; - -class BaseEntity -{ -public: - BaseEntity() - : m_spawnTime( g_curTick ){}; - virtual ~BaseEntity() {} - - int m_spawnTime; - - EntityFlags m_flags = ENTITY_NONE; - - std::string m_type = "No type"; - std::string m_name = "Unnamed"; - - Transform m_transformLastFrame = {}; - Transform m_transformCurrentFrame = {}; - - Transform m_transform = {}; - - inline void AddFlag( EntityFlags flags ) { m_flags = m_flags | flags; } - inline void RemoveFlag( EntityFlags flags ) { m_flags = m_flags & ~flags; } - inline bool HasFlag( EntityFlags flag ) { return ( m_flags & flag ) != 0; } -}; diff --git a/Source/Mocha.Host/editor.h b/Source/Mocha.Host/editor.h deleted file mode 100644 index 21aa0fc0..00000000 --- a/Source/Mocha.Host/editor.h +++ /dev/null @@ -1,200 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Editor -{ - /// - /// Get the current pointer to an ImGUI context. - /// This is used in order to effectively "link" managed ImGUI - /// to our native ImGUI instance. - /// - /// - GENERATE_BINDINGS inline void* GetContextPointer() - { - auto ctx = ImGui::GetCurrentContext(); - return ( void* )ctx; - }; - - GENERATE_BINDINGS inline void TextBold( const char* text ) - { - ImGui::PushFont( g_renderContext->m_boldFont ); - ImGui::Text( "%s", text ); - ImGui::PopFont(); - }; - - GENERATE_BINDINGS inline void TextSubheading( const char* text ) - { - ImGui::PushFont( g_renderContext->m_subheadingFont ); - ImGui::Text( "%s", text ); - ImGui::Dummy( ImVec2( 0, 2 ) ); - ImGui::PopFont(); - }; - - GENERATE_BINDINGS inline void TextHeading( const char* text ) - { - ImGui::PushFont( g_renderContext->m_headingFont ); - ImGui::Text( "%s", text ); - ImGui::Dummy( ImVec2( 0, 2 ) ); - ImGui::PopFont(); - }; - - GENERATE_BINDINGS inline void TextMonospace( const char* text ) - { - ImGui::PushFont( g_renderContext->m_monospaceFont ); - ImGui::Text( "%s", text ); - ImGui::PopFont(); - }; - - GENERATE_BINDINGS inline void TextLight( const char* text ) - { - ImGui::PushStyleColor( ImGuiCol_Text, ImVec4( 1, 1, 1, 0.75f ) ); - ImGui::Text( "%s", text ); - ImGui::PopStyleColor(); - } - - GENERATE_BINDINGS inline const char* GetGPUName() - { - return g_renderManager->GetGPUName(); - } - - GENERATE_BINDINGS inline char* InputText( const char* name, char* inputBuf, int inputLength ) - { - ImGui::InputText( name, inputBuf, inputLength, ImGuiInputTextFlags_EnterReturnsTrue ); - - return inputBuf; - } - - GENERATE_BINDINGS inline void RenderViewDropdown() - { - if ( ImGui::BeginMenu( "Debug View" ) ) - { - if ( ImGui::MenuItem( "None" ) ) - g_debugView = RenderDebugViews::NONE; - - if ( ImGui::MenuItem( "Diffuse" ) ) - g_debugView = RenderDebugViews::DIFFUSE; - - if ( ImGui::MenuItem( "Normal" ) ) - g_debugView = RenderDebugViews::NORMAL; - - if ( ImGui::MenuItem( "Ambient Occlusion" ) ) - g_debugView = RenderDebugViews::AMBIENTOCCLUSION; - - if ( ImGui::MenuItem( "Metalness" ) ) - g_debugView = RenderDebugViews::METALNESS; - - if ( ImGui::MenuItem( "Roughness" ) ) - g_debugView = RenderDebugViews::ROUGHNESS; - - if ( ImGui::MenuItem( "Other" ) ) - g_debugView = RenderDebugViews::OTHER; - - ImGui::EndMenu(); - } - } - - GENERATE_BINDINGS inline Vector2 GetWindowSize() - { - Size2D size; - g_renderContext->GetWindowSize( &size ); - return { ( float )size.x, ( float )size.y }; - } - - GENERATE_BINDINGS inline Vector2 GetRenderSize() - { - Size2D size; - g_renderContext->GetRenderSize( &size ); - return { ( float )size.x, ( float )size.y }; - } - - GENERATE_BINDINGS inline const char* GetVersionName() - { - return GAME_VERSION; - } - - GENERATE_BINDINGS inline void Image( Texture* texture, uint32_t textureWidth, uint32_t textureHeight, int x, int y ) - { - void* imguiTextureID; - g_renderContext->GetImGuiTextureID( &texture->m_image, &imguiTextureID ); - - // Calculate new UVs based on reported textureWidth, textureHeight vs texture->m_size - // This is done because the C++ side isn't aware of any padding applied in order to get - // the image to become POT - float u = ( float )textureWidth / ( float )texture->m_size.x; - float v = ( float )textureWidth / ( float )texture->m_size.y; - - ImGui::Image( imguiTextureID, { ( float )x, ( float )y }, { 0, 0 }, { u, v } ); - } - - GENERATE_BINDINGS inline bool BeginMainStatusBar() - { - ImGuiViewportP* viewport = ( ImGuiViewportP* )( void* )ImGui::GetMainViewport(); - ImGuiWindowFlags window_flags = - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; - float height = ImGui::GetFrameHeight(); - - if ( ImGui::BeginViewportSideBar( "##MainStatusBar", viewport, ImGuiDir_Down, height, window_flags ) ) - { - if ( ImGui::BeginMenuBar() ) - { - return true; - } - } - - return false; - } - - GENERATE_BINDINGS inline void DrawGraph( const char* name, Vector4 color, UtilArray values ) - { - const std::vector plotValues = values.GetData(); - const float MARKERS[] = { 30.0f, 60.0f, 144.0f }; - const int MARKER_COUNT = 3; - const int sampleCount = static_cast( plotValues.size() ); - - auto startPos = ImGui::GetCursorPos(); - - ImPlot::PushStyleVar( ImPlotStyleVar_PlotPadding, { 0, 0 } ); - ImPlot::PushStyleVar( ImPlotStyleVar_LineWeight, 1.0f ); - ImPlot::PushStyleColor( ImPlotCol_PlotBg, { color.x, color.y, color.z, color.w } ); - ImPlot::PushStyleColor( ImPlotCol_Line, { color.x, color.y, color.z, color.w * 4.0f } ); - ImPlot::PushStyleColor( ImPlotCol_InlayText, { color.x, color.y, color.z, color.w * 4.0f } ); - - if ( ImPlot::BeginPlot( name, { -1, 128 }, - ImPlotFlags_NoInputs | ImPlotFlags_NoMenus | ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText ) ) - { - ImPlot::SetupAxis( ImAxis_X1, 0, ImPlotAxisFlags_NoDecorations ); - ImPlot::SetupAxis( ImAxis_Y1, 0, ImPlotAxisFlags_NoDecorations ); - ImPlot::SetupAxisLimits( ImAxis_Y1, 0.0, MARKERS[MARKER_COUNT - 1] + 20.0f, ImPlotCond_Always ); - ImPlot::SetupAxisLimits( ImAxis_X1, 0.0, sampleCount, ImPlotCond_Always ); - - ImPlot::PlotInfLines( "##reference", MARKERS, MARKER_COUNT, ImPlotInfLinesFlags_Horizontal ); - - for ( auto& marker : MARKERS ) - { - std::string str = std::to_string( ( int )marker ) + "fps"; - float x = sampleCount - 40.0f; - float y = marker - 15.0f; - ImPlot::PlotText( str.c_str(), x, y ); - } - - ImPlot::PushStyleVar( ImPlotStyleVar_LineWeight, 2.0f ); - ImPlot::PlotLine( name, plotValues.data(), sampleCount ); - ImPlot::PopStyleVar(); - ImPlot::EndPlot(); - } - - ImPlot::PopStyleColor( 3 ); - ImPlot::PopStyleVar( 2 ); - } -} // namespace Editor diff --git a/Source/Mocha.Host/engine.h b/Source/Mocha.Host/engine.h deleted file mode 100644 index f3abb13a..00000000 --- a/Source/Mocha.Host/engine.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -namespace Engine -{ - GENERATE_BINDINGS inline void Quit() - { - auto& root = Root::GetInstance(); - root.Quit(); - } - - GENERATE_BINDINGS inline int GetCurrentTick() - { - return g_curTick; - } - - GENERATE_BINDINGS inline float GetFrameDeltaTime() - { - return g_frameDeltaTime; - } - - GENERATE_BINDINGS inline float GetTickDeltaTime() - { - return g_tickDeltaTime; - } - - GENERATE_BINDINGS inline float GetFramesPerSecond() - { - return 1.0f / g_frameDeltaTime; - } - - GENERATE_BINDINGS inline float GetTime() - { - return g_curTime; - } - - GENERATE_BINDINGS inline const char* GetProjectPath() - { - std::string str = EngineProperties::LoadedProject.GetValue(); - - // Copy string so we can use it out-of-scope - char* cstr = new char[str.length() + 1]; - strcpy_s( cstr, str.length() + 1, str.c_str() ); - - return cstr; - }; -}; // namespace Engine diff --git a/Source/Mocha.Host/entities.h b/Source/Mocha.Host/entities.h deleted file mode 100644 index cd219653..00000000 --- a/Source/Mocha.Host/entities.h +++ /dev/null @@ -1,367 +0,0 @@ -#pragma once -#include -#include -#include -#include - -// TODO: I hate this -namespace Entities -{ - GENERATE_BINDINGS inline uint32_t CreateBaseEntity() - { - BaseEntity baseEntity = {}; - baseEntity.AddFlag( ENTITY_MANAGED ); - baseEntity.m_type = "BaseEntity"; - - return g_entityDictionary->AddEntity( baseEntity ); - } - - GENERATE_BINDINGS inline uint32_t CreateModelEntity() - { - ModelEntity modelEntity = {}; - modelEntity.AddFlag( ENTITY_MANAGED ); - modelEntity.AddFlag( ENTITY_RENDERABLE ); - modelEntity.m_type = "ModelEntity"; - - return g_entityDictionary->AddEntity( modelEntity ); - } - - GENERATE_BINDINGS inline void SetViewmodel( uint32_t handle, bool isViewmodel ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - if ( isViewmodel ) - entity->AddFlag( ENTITY_VIEWMODEL ); - else - entity->RemoveFlag( ENTITY_VIEWMODEL ); - } - - GENERATE_BINDINGS inline void SetUI( uint32_t handle, bool isUI ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - if ( isUI ) - entity->AddFlag( ENTITY_UI ); - else - entity->RemoveFlag( ENTITY_UI ); - } - - GENERATE_BINDINGS inline void SetPosition( uint32_t handle, Vector3 position ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - transform.position = position; - - entity->m_transform = transform; - } - - GENERATE_BINDINGS inline void SetRotation( uint32_t handle, Quaternion rotation ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - transform.rotation = rotation; - - entity->m_transform = transform; - } - - GENERATE_BINDINGS inline void SetScale( uint32_t handle, Vector3 scale ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - transform.scale = scale; - - entity->m_transform = transform; - } - - GENERATE_BINDINGS inline void SetName( uint32_t handle, const char* name ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - - entity->m_name = name; - } - - GENERATE_BINDINGS inline Vector3 GetPosition( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - return transform.position; - } - - GENERATE_BINDINGS inline Quaternion GetRotation( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - return transform.rotation; - } - - GENERATE_BINDINGS inline Vector3 GetScale( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - auto transform = entity->m_transform; - - return transform.scale; - } - - GENERATE_BINDINGS inline const char* GetName( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - return entity->m_name.c_str(); - } - - GENERATE_BINDINGS inline void SetModel( uint32_t handle, Model model ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetModel( model ); - } - - GENERATE_BINDINGS inline void SetCameraPosition( Vector3 position ) - { - g_cameraPos = position; - } - - GENERATE_BINDINGS inline Vector3 GetCameraPosition() - { - return g_cameraPos; - } - - GENERATE_BINDINGS inline void SetCameraRotation( Quaternion rotation ) - { - g_cameraRot = rotation; - } - - GENERATE_BINDINGS inline Quaternion GetCameraRotation() - { - return g_cameraRot; - } - - GENERATE_BINDINGS inline void SetCameraFieldOfView( float fov ) - { - g_cameraFov = fov; - } - - GENERATE_BINDINGS inline float GetCameraFieldOfView() - { - return g_cameraFov; - } - - GENERATE_BINDINGS inline void SetCameraZNear( float znear ) - { - g_cameraZNear = znear; - } - - GENERATE_BINDINGS inline float GetCameraZNear() - { - return g_cameraZNear; - } - - GENERATE_BINDINGS inline void SetCameraZFar( float zfar ) - { - g_cameraZFar = zfar; - } - - GENERATE_BINDINGS inline float GetCameraZFar() - { - return g_cameraZFar; - } - - GENERATE_BINDINGS inline void SetCubePhysics( uint32_t handle, Vector3 bounds, bool isStatic ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetCubePhysics( bounds, isStatic ); - } - - GENERATE_BINDINGS inline void SetSpherePhysics( uint32_t handle, float radius, bool isStatic ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetSpherePhysics( radius, isStatic ); - } - - GENERATE_BINDINGS inline void SetMeshPhysics( uint32_t handle, int vertexSize, void* vertexData ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - // Convert data to points - Vector3* vertices = ( Vector3* )vertexData; - size_t vertCount = vertexSize / sizeof( Vector3 ); - - std::vector vertexList = {}; - vertexList.resize( vertCount ); - vertexList.insert( vertexList.begin(), vertices, vertices + vertCount ); - - entity->SetMeshPhysics( vertexList ); - } - - GENERATE_BINDINGS inline void SetVelocity( uint32_t handle, Vector3 velocity ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetVelocity( velocity ); - } - - GENERATE_BINDINGS inline Vector3 GetVelocity( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return {}; - } - - return entity->GetVelocity(); - } - - GENERATE_BINDINGS inline float GetMass( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return 0.0f; - } - - return entity->GetMass(); - } - - GENERATE_BINDINGS inline float GetFriction( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return 0.0f; - } - - return entity->GetFriction(); - } - - GENERATE_BINDINGS inline float GetRestitution( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return 0.0f; - } - - return entity->GetRestitution(); - } - - GENERATE_BINDINGS inline void SetMass( uint32_t handle, float mass ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetMass( mass ); - } - - GENERATE_BINDINGS inline void SetFriction( uint32_t handle, float friction ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetFriction( friction ); - } - - GENERATE_BINDINGS inline void SetRestitution( uint32_t handle, float restitution ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetRestitution( restitution ); - } - - GENERATE_BINDINGS inline bool GetIgnoreRigidbodyPosition( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return false; - } - - return entity->GetIgnoreRigidbodyPosition(); - } - - GENERATE_BINDINGS inline bool GetIgnoreRigidbodyRotation( uint32_t handle ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return false; - } - - return entity->GetIgnoreRigidbodyRotation(); - } - - GENERATE_BINDINGS inline void SetIgnoreRigidbodyPosition( uint32_t handle, bool ignore ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetIgnoreRigidbodyPosition( ignore ); - } - - GENERATE_BINDINGS inline void SetIgnoreRigidbodyRotation( uint32_t handle, bool ignore ) - { - auto entity = g_entityDictionary->GetEntity( handle ); - if ( entity == nullptr ) - { - spdlog::error( "Couldn't cast {} to ModelEntity", handle ); - return; - } - - entity->SetIgnoreRigidbodyRotation( ignore ); - } -} // namespace Entities \ No newline at end of file diff --git a/Source/Mocha.Host/globalvars.h b/Source/Mocha.Host/globalvars.h deleted file mode 100644 index 893a20d0..00000000 --- a/Source/Mocha.Host/globalvars.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include - -// TODO: Remove -enum RenderDebugViews -{ - NONE = 0, - DIFFUSE = 1, - NORMAL = 2, - AMBIENTOCCLUSION = 3, - METALNESS = 4, - ROUGHNESS = 5, - - OTHER = 63 -}; - -class RenderManager; -class RenderdocManager; -class HostManager; -class LogManager; -class EntityManager; -class PhysicsManager; -class InputManager; -class BaseRenderContext; -class CVarManager; -class ProjectManager; - -struct Vector3; -struct Quaternion; - -// -// Global vars -// -extern RenderManager* g_renderManager; -extern LogManager* g_logManager; -extern HostManager* g_hostManager; -extern RenderdocManager* g_renderdocManager; -extern EntityManager* g_entityDictionary; -extern PhysicsManager* g_physicsManager; -extern InputManager* g_inputManager; -extern BaseRenderContext* g_renderContext; -extern CVarManager* g_cvarManager; -extern ProjectManager* g_projectManager; - -extern float g_curTime; -extern float g_frameDeltaTime; -extern float g_tickDeltaTime; -extern int g_curTick; - -extern Vector3 g_cameraPos; -extern Quaternion g_cameraRot; -extern float g_cameraFov; -extern float g_cameraZNear; -extern float g_cameraZFar; - -extern RenderDebugViews g_debugView; \ No newline at end of file diff --git a/Source/Mocha.Host/handlemap.cpp b/Source/Mocha.Host/handlemap.cpp deleted file mode 100644 index 6538f087..00000000 --- a/Source/Mocha.Host/handlemap.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "handlemap.h" diff --git a/Source/Mocha.Host/hostmanager.cpp b/Source/Mocha.Host/hostmanager.cpp deleted file mode 100644 index 4f4b6d84..00000000 --- a/Source/Mocha.Host/hostmanager.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#include "hostmanager.h" - -#include - -void* HostGlobals::load_library( const char_t* path ) -{ - HMODULE h = ::LoadLibraryW( path ); - assert( h != nullptr ); - return ( void* )h; -} - -void* HostGlobals::get_export( void* h, const char* name ) -{ - void* f = ::GetProcAddress( ( HMODULE )h, name ); - assert( f != nullptr ); - return f; -} - -bool HostGlobals::LoadHostFxr() -{ - // Pre-allocate a large buffer for the path to hostfxr - char_t buffer[MAX_PATH]; - size_t buffer_size = sizeof( buffer ) / sizeof( char_t ); - int rc = get_hostfxr_path( buffer, &buffer_size, nullptr ); - if ( rc != 0 ) - return false; - - // Load hostfxr and get desired exports - void* lib = load_library( buffer ); - init_fptr = ( hostfxr_initialize_for_runtime_config_fn )get_export( lib, "hostfxr_initialize_for_runtime_config" ); - get_delegate_fptr = ( hostfxr_get_runtime_delegate_fn )get_export( lib, "hostfxr_get_runtime_delegate" ); - set_property_fptr = ( hostfxr_set_runtime_property_value_fn )get_export( lib, "hostfxr_set_runtime_property_value" ); - close_fptr = ( hostfxr_close_fn )get_export( lib, "hostfxr_close" ); - - return ( init_fptr && get_delegate_fptr && close_fptr ); -} - -load_assembly_and_get_function_pointer_fn HostGlobals::GetDotnetLoadAssembly( const char_t* configPath ) -{ - LoadHostFxr(); - - // Load .NET Core - void* load_assembly_and_get_function_pointer = nullptr; - hostfxr_handle cxt = nullptr; - int rc = init_fptr( configPath, nullptr, &cxt ); - if ( rc != 0 || cxt == nullptr ) - { - spdlog::error( "Failed to initialize: 0x{:x}", rc ); - close_fptr( cxt ); - return nullptr; - } - - // Get current working directory - char_t cwd[MAX_PATH]; - DWORD cwd_len = GetCurrentDirectoryW( MAX_PATH, cwd ); - if ( cwd_len == 0 ) - { - spdlog::error( "Failed to get current directory" ); - close_fptr( cxt ); - return nullptr; - } - - // Add "build" to cwd - std::wstring buildPath = cwd; - buildPath += L"\\build"; - - // Set CoreCLR properties - set_property_fptr( cxt, L"APP_CONTEXT_BASE_DIRECTORY", buildPath.c_str() ); - set_property_fptr( cxt, L"APP_PATHS", buildPath.c_str() ); - set_property_fptr( cxt, L"APP_NI_PATHS", buildPath.c_str() ); - set_property_fptr( cxt, L"NATIVE_DLL_SEARCH_DIRECTORIES", buildPath.c_str() ); - set_property_fptr( cxt, L"PLATFORM_RESOURCE_ROOTS", buildPath.c_str() ); - - // Get the load assembly function pointer - rc = get_delegate_fptr( cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer ); - if ( rc != 0 || load_assembly_and_get_function_pointer == nullptr ) - spdlog::error( "Get delegate failed: 0x{:x}", rc ); - - close_fptr( cxt ); - return ( load_assembly_and_get_function_pointer_fn )load_assembly_and_get_function_pointer; -} - -HostManager::HostManager() -{ - // TODO: Hardcoding these might be a bad idea? - std::wstring basePath = L".\\build\\Mocha.Hotload"; - std::wstring signature = L"Mocha.Hotload.Main, Mocha.Hotload"; - - m_dllPath = basePath + L".dll"; - m_configPath = basePath + L".runtimeconfig.json"; - m_signature = signature; - - m_lagfp = HostGlobals::GetDotnetLoadAssembly( m_configPath.c_str() ); -} - -void HostManager::Update() -{ - Invoke( "Update" ); -} - -void HostManager::Render() -{ - Invoke( "Render" ); -} - -void HostManager::Startup() -{ - Invoke( "Run", ( void* )&args ); -} - -void HostManager::DrawEditor() -{ - Invoke( "DrawEditor" ); -} - -void HostManager::Shutdown() {} - -void HostManager::FireEvent( std::string eventName ) -{ - Invoke( "FireEvent", ( void* )eventName.c_str() ); -} - -void HostManager::DispatchCommand( CVarManagedCmdDispatchInfo info ) -{ - Invoke( "DispatchCommand", &info ); -} - -void HostManager::DispatchStringCVarCallback( CVarManagedVarDispatchInfo info ) -{ - Invoke( "DispatchStringCVarCallback", &info ); -} - -void HostManager::DispatchFloatCVarCallback( CVarManagedVarDispatchInfo info ) -{ - Invoke( "DispatchFloatCVarCallback", &info ); -} - -void HostManager::DispatchBoolCVarCallback( CVarManagedVarDispatchInfo info ) -{ - Invoke( "DispatchBoolCVarCallback", &info ); -} - -inline void HostManager::Invoke( std::string _method, void* params, const char_t* delegateTypeName ) -{ - // Convert to std::wstring - std::wstring method( _method.begin(), _method.end() ); - - // Function pointer to managed delegate - void* fnPtr = nullptr; - - int rc = m_lagfp( m_dllPath.c_str(), m_signature.c_str(), method.c_str(), delegateTypeName, nullptr, ( void** )&fnPtr ); - - if ( fnPtr == nullptr ) - { - spdlog::error( "Failed to load managed method {}", _method ); - } - - // Invoke method - ( ( void ( * )( void* ) )fnPtr )( params ); -} \ No newline at end of file diff --git a/Source/Mocha.Host/input.h b/Source/Mocha.Host/input.h deleted file mode 100644 index c3be93dd..00000000 --- a/Source/Mocha.Host/input.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace Input -{ - GENERATE_BINDINGS inline bool IsButtonDown( int button ) - { - return g_inputManager->IsButtonDown( button ); - } - - GENERATE_BINDINGS inline Vector2 GetMousePosition() - { - return g_inputManager->GetMousePosition(); - } - - GENERATE_BINDINGS inline Vector2 GetMouseDelta() - { - return g_inputManager->GetMouseDelta(); - } - - GENERATE_BINDINGS inline bool IsKeyDown( int key ) - { - return g_inputManager->IsKeyDown( key ); - } -} // namespace Input \ No newline at end of file diff --git a/Source/Mocha.Host/logmanager.cpp b/Source/Mocha.Host/logmanager.cpp deleted file mode 100644 index af5af1ea..00000000 --- a/Source/Mocha.Host/logmanager.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "logmanager.h" - -#include "spdlog/spdlog.h" - -#include - -void LogManager::Startup() -{ - // Setup spdlog - auto mochaSink = std::make_shared(); - auto managed = std::make_shared( "managed", mochaSink ); - auto main = std::make_shared( "main", mochaSink ); - auto renderer = std::make_shared( "renderer", mochaSink ); - - spdlog::register_logger( managed ); - spdlog::register_logger( main ); - spdlog::register_logger( renderer ); - - spdlog::set_default_logger( main ); - spdlog::set_level( spdlog::level::trace ); - - // Set pattern "time logger,8 type,8 message" - spdlog::set_pattern( "%H:%M:%S %-8n %^%-8l%$ %v" ); -} - -void LogManager::ManagedInfo( std::string str ) -{ - spdlog::get( "managed" )->info( str ); -} - -void LogManager::ManagedWarning( std::string str ) -{ - spdlog::get( "managed" )->warn( str ); -} - -void LogManager::ManagedError( std::string str ) -{ - spdlog::get( "managed" )->error( str ); -} - -void LogManager::ManagedTrace( std::string str ) -{ - spdlog::get( "managed" )->trace( str ); -} \ No newline at end of file diff --git a/Source/Mocha.Host/main.cpp b/Source/Mocha.Host/main.cpp deleted file mode 100644 index fb3bf1f2..00000000 --- a/Source/Mocha.Host/main.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include -#undef main - -int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow ) -{ - // Set thread name - HRESULT hr = SetThreadDescription( GetCurrentThread(), L"Mocha Native Thread" ); - - auto& root = Root::GetInstance(); - - root.Startup(); - root.Run(); - root.Shutdown(); - - return 0; -} \ No newline at end of file diff --git a/Source/Mocha.Host/mesh.cpp b/Source/Mocha.Host/mesh.cpp deleted file mode 100644 index 31da09b0..00000000 --- a/Source/Mocha.Host/mesh.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "mesh.h" \ No newline at end of file diff --git a/Source/Mocha.Host/modelentity.h b/Source/Mocha.Host/modelentity.h deleted file mode 100644 index 8e6a59ee..00000000 --- a/Source/Mocha.Host/modelentity.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -#include -#include -#include -#include - -struct PhysicsBody; - -class ModelEntity : public BaseEntity -{ -private: - Model m_model; - - // - // Physics values - // - uint32_t m_physicsHandle = UINT32_MAX; - - Vector3 m_velocity = {}; - float m_friction = 0.5f; - float m_mass = 10.0f; - float m_restitution = 0.5f; - - bool m_ignoreRigidbodyRotation; - bool m_ignoreRigidbodyPosition; - -public: - void SetModel( Model model ) { m_model = model; } - Model* GetModel() { return &m_model; } - - // - // Getters & setters - // - void SetSpherePhysics( float radius, bool isStatic ); - void SetCubePhysics( Vector3 bounds, bool isStatic ); - void SetMeshPhysics( std::vector vertices ); - - // If this model has no physics, this function will return UINT32_MAX. - uint32_t GetPhysicsHandle() { return m_physicsHandle; }; - - Vector3 GetVelocity() { return m_velocity; } - void SetVelocity( Vector3 velocity ) { m_velocity = velocity; } - - float GetFriction() { return m_friction; } - void SetFriction( float friction ) { m_friction = friction; } - - float GetMass() { return m_mass; } - void SetMass( float mass ) { m_mass = mass; } - - float GetRestitution() { return m_restitution; } - void SetRestitution( float restitution ) { m_restitution = restitution; } - - bool GetIgnoreRigidbodyRotation() { return m_ignoreRigidbodyRotation; } - void SetIgnoreRigidbodyRotation( bool ignore ) { m_ignoreRigidbodyRotation = ignore; } - - bool GetIgnoreRigidbodyPosition() { return m_ignoreRigidbodyPosition; } - void SetIgnoreRigidbodyPosition( bool ignore ) { m_ignoreRigidbodyPosition = ignore; } -}; diff --git a/Source/Mocha.Host/physics.h b/Source/Mocha.Host/physics.h deleted file mode 100644 index 43d6c199..00000000 --- a/Source/Mocha.Host/physics.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include -#include - -namespace Physics -{ - GENERATE_BINDINGS inline TraceResult Trace( TraceInfo traceInfo ) - { - return g_physicsManager->Trace( traceInfo ); - } -}; // namespace Physics \ No newline at end of file diff --git a/Source/Mocha.Host/rendering.h b/Source/Mocha.Host/rendering.h deleted file mode 100644 index f6be5c37..00000000 --- a/Source/Mocha.Host/rendering.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include \ No newline at end of file diff --git a/Source/Mocha.Host/root.cpp b/Source/Mocha.Host/root.cpp deleted file mode 100644 index d067d2a7..00000000 --- a/Source/Mocha.Host/root.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "root.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// These global variables are all defined in globalvars.h, -// because the naming makes more sense (imagine if we -// included Root.h everywhere!) -RenderManager* g_renderManager; -LogManager* g_logManager; -HostManager* g_hostManager; -RenderdocManager* g_renderdocManager; -EntityManager* g_entityDictionary; -PhysicsManager* g_physicsManager; -InputManager* g_inputManager; -BaseRenderContext* g_renderContext; // TODO: Remove -CVarManager* g_cvarManager; -ProjectManager* g_projectManager; - -float g_curTime; -float g_frameDeltaTime; -float g_tickDeltaTime; -int g_curTick; -Vector3 g_cameraPos; -Quaternion g_cameraRot; -float g_cameraFov; -float g_cameraZNear; -float g_cameraZFar; -RenderDebugViews g_debugView; - -namespace EngineProperties -{ - StringCVar LoadedProject( - "project.current", "Samples\\mocha-minimal\\project.json", CVarFlags::Archive, "Which project should we load?" ); - BoolCVar Raytracing( "render.raytracing", true, CVarFlags::Archive, "Enable raytracing" ); - BoolCVar Renderdoc( "render.renderdoc", false, CVarFlags::Archive, "Enable renderdoc" ); -} // namespace EngineProperties - -FloatCVar timescale( "game.timescale", 1.0f, CVarFlags::Archive, "The speed at which the game world runs." ); - -// TODO: Server / client -extern FloatCVar maxFramerate; - -void Root::Startup() -{ - g_logManager = new LogManager(); - g_logManager->Startup(); - - g_cvarManager = new CVarManager(); - g_cvarManager->Startup(); - - g_projectManager = new ProjectManager(); - g_projectManager->Startup(); - - g_renderdocManager = new RenderdocManager(); - g_renderdocManager->Startup(); - - g_entityDictionary = new EntityManager(); - g_entityDictionary->Startup(); - - g_physicsManager = new PhysicsManager(); - g_physicsManager->Startup(); - - g_inputManager = new InputManager(); - g_inputManager->Startup(); - - g_renderManager = new RenderManager(); - g_renderManager->Startup(); - - g_hostManager = new HostManager(); - g_hostManager->Startup(); -} - -void Root::Shutdown() -{ - g_hostManager->Shutdown(); - g_renderManager->Shutdown(); - g_inputManager->Shutdown(); - g_physicsManager->Shutdown(); - g_entityDictionary->Shutdown(); - g_renderdocManager->Shutdown(); - g_projectManager->Shutdown(); - g_cvarManager->Shutdown(); - g_logManager->Shutdown(); -} - -double HiresTimeInSeconds() -{ - return std::chrono::duration_cast>( - std::chrono::high_resolution_clock::now().time_since_epoch() ) - .count(); -} - -void Root::Run() -{ - g_hostManager->FireEvent( "Event.Game.Load" ); - - double logicDelta = 1.0 / g_projectManager->GetProject().properties.tickRate; - - double currentTime = HiresTimeInSeconds(); - double accumulator = 0.0; - - while ( !m_shouldQuit ) - { - double newTime = HiresTimeInSeconds(); - double loopDeltaTime = newTime - currentTime; - - // How quick did we do last frame? Let's limit ourselves if (1.0f / loopDeltaTime) is more than maxLoopHz - float loopHz = 1.0f / loopDeltaTime; - - // TODO: Server / client. Perhaps abstract this and set it to the tickrate if we're a dedicated server? - float maxLoopHz = maxFramerate.GetValue(); - - if ( maxLoopHz > 0 && loopHz > maxLoopHz ) - { - continue; - } - - if ( loopDeltaTime > 1 / 30.0f ) - loopDeltaTime = 1 / 30.0f; - - currentTime = newTime; - accumulator += loopDeltaTime; - - // - // How long has it been since we last updated the game logic? - // We want to update as many times as we can in this frame in - // order to match the desired tick rate. - // - while ( accumulator >= logicDelta ) - { - // Assign previous transforms to all entities - g_entityDictionary->ForEach( - [&]( std::shared_ptr entity ) { entity->m_transformLastFrame = entity->m_transformCurrentFrame; } ); - - g_tickDeltaTime = ( float )logicDelta; - - // Update physics - g_physicsManager->Update(); - - // Update game - g_hostManager->Update(); - -// TODO: Server / client -// #ifndef DEDICATED_SERVER - // Update window - g_renderContext->UpdateWindow(); -// #endif - - if ( GetQuitRequested() ) - { - m_shouldQuit = true; - break; - } - - // Assign current transforms to all entities - g_entityDictionary->ForEach( - [&]( std::shared_ptr entity ) { entity->m_transformCurrentFrame = entity->m_transform; } ); - - g_curTime += logicDelta; - accumulator -= logicDelta; - g_curTick++; - } - - g_frameDeltaTime = ( float )loopDeltaTime; - -// TODO: Server / client -// #ifndef DEDICATED_SERVER - // Render - { - const double alpha = accumulator / logicDelta; - - // Assign interpolated transforms to all entities - g_entityDictionary->ForEach( [&]( std::shared_ptr entity ) { - // If this entity was spawned in just now, don't interpolate - if ( entity->m_spawnTime == g_curTick ) - return; - - entity->m_transform = - Transform::Lerp( entity->m_transformLastFrame, entity->m_transformCurrentFrame, ( float )alpha ); - } ); - - g_renderManager->DrawOverlaysAndEditor(); - - g_renderManager->DrawGame(); - } -// #endif - } -} - -bool Root::GetQuitRequested() -{ -// TODO: Server / client -// #ifdef DEDICATED_SERVER -// ... -// #else - return g_renderContext->GetWindowCloseRequested(); -// #endif -} diff --git a/Source/Mocha.Host/root.h b/Source/Mocha.Host/root.h deleted file mode 100644 index b1dba021..00000000 --- a/Source/Mocha.Host/root.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include - -class Root : ISubSystem -{ -private: - bool m_shouldQuit = false; - - bool GetQuitRequested(); - -public: - inline static Root& GetInstance() - { - static Root instance; - return instance; - } - - void Startup(); - void Run(); - void Shutdown(); - - void Quit() { m_shouldQuit = true; } -}; diff --git a/Source/Mocha.Host/util.h b/Source/Mocha.Host/util.h deleted file mode 100644 index e64cca30..00000000 --- a/Source/Mocha.Host/util.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include diff --git a/Source/Mocha.Host/utilhandlemap.h b/Source/Mocha.Host/utilhandlemap.h deleted file mode 100644 index 16988595..00000000 --- a/Source/Mocha.Host/utilhandlemap.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include - -// -// A generic version of HandleMap that can be used anywhere. -// -template -class UtilHandleMap : public HandleMap -{ -public: - // Adds the specified object to the map and returns a handle to it. - Handle Add( T object ); - - // Returns a pointer to the object associated with the specified handle. - std::shared_ptr Get( Handle handle ); - - // Use this if you want to get a derived type. - template - std::shared_ptr GetSpecific( Handle handle ); - - // Use this if you want to add a derived type. - template - Handle AddSpecific( T1 object ); - - // Calls the specified function for each object managed by this HandleMap. - // The function should take a std::unique_ptr as its argument. - void ForEach( std::function object )> func ); - - // Calls the specified function for each object managed by this HandleMap. - // The function should take a Handle and a std::unique_ptr as its arguments. - void For( std::function object )> func ); -}; \ No newline at end of file diff --git a/Source/Mocha.Hotload/Assembly/ProjectAssembly.cs b/Source/Mocha.Hotload/Assembly/ProjectAssembly.cs deleted file mode 100644 index b2413922..00000000 --- a/Source/Mocha.Hotload/Assembly/ProjectAssembly.cs +++ /dev/null @@ -1,272 +0,0 @@ -using Mocha.Common; -using System.Reflection; -using System.Runtime.Loader; -using System.Runtime.Serialization; - -namespace Mocha.Hotload; - -/// -/// A wrapper for an external assembly to be loaded. -/// -/// The type to retrieve from the assembly as its entry point. -internal sealed class ProjectAssembly where TEntryPoint : IGame -{ - /// - /// The loaded assembly. - /// - internal Assembly Assembly { get; private set; } = null!; - /// - /// The found entry point into the assembly. - /// - internal TEntryPoint EntryPoint { get; private set; } = default!; - - private readonly ProjectAssemblyInfo _projectAssemblyInfo; - private FileSystemWatcher _projWatcher = null!; - private FileSystemWatcher _codeWatcher = null!; - private AssemblyLoadContext _loadContext; - - private TimeSince _timeSinceCsProjChange; - private TimeSince _timeSinceLastFileChange; - private Task _buildTask; - private bool _buildRequested; - - internal ProjectAssembly( in ProjectAssemblyInfo assemblyInfo ) - { - _projectAssemblyInfo = assemblyInfo; - _loadContext = new AssemblyLoadContext( null, isCollectible: true ); - - /* - * Mocha.Common must always be "Private" (ie. "Copy Local" as "No" in the reference's properties) - * otherwise **the engine will load Mocha.Common twice incorrectly, causing a mismatch between types**. - * If you ever need to delete and re-add this reference please make sure this is always the case. - * - * If you didn't do that then you'll get an exception around here! - */ - _buildTask = Build(); - _buildTask.Wait(); - CreateFileWatchers(); - } - - /// - /// Unloads the current assembly and swaps it with a new one. - /// - /// The new assembly to swap in. - /// The new entry point to the assembly. - private void Swap( Assembly newAssembly, TEntryPoint newEntryPoint ) - { - _loadContext.Unload(); - _loadContext = new AssemblyLoadContext( null, isCollectible: true ); - - Assembly = newAssembly; - EntryPoint = newEntryPoint; - } - - /// - /// Builds the projects assembly. - /// - /// A task that represents the asynchronous operation. - private async Task Build() - { - Notify.AddNotification( $"Building...", $"Compiling '{_projectAssemblyInfo.AssemblyName}'", FontAwesome.Spinner ); - var compileResult = await Compiler.Compile( _projectAssemblyInfo ); - - if ( !compileResult.WasSuccessful ) - { - var errorStr = string.Join( '\n', compileResult.Errors! ); - - foreach ( var error in compileResult.Errors! ) - Log.Error( error ); - - Notify.AddError( $"Build failed", $"Failed to compile '{_projectAssemblyInfo.AssemblyName}'\n{errorStr}", FontAwesome.FaceSadTear ); - return; - } - - // Keep old assembly as reference. Should be destroyed once out of scope - var oldAssembly = Assembly; - var oldGameInterface = EntryPoint; - - // Load new assembly - var assemblyStream = new MemoryStream( compileResult.CompiledAssembly! ); - var symbolsStream = compileResult.HasSymbols ? new MemoryStream( compileResult.CompiledAssemblySymbols! ) : null; - - var newAssembly = _loadContext.LoadFromStream( assemblyStream, symbolsStream ); - var newInterface = CreateEntryPointFromAssembly( newAssembly ); - - // Invoke upgrader to move values from oldAssembly into assembly - if ( oldAssembly != null && oldGameInterface != null ) - { - Upgrader.UpgradedReferences.Clear(); - - UpgradeEntities( oldAssembly, newAssembly ); - - Upgrader.UpgradeInstance( oldGameInterface, newInterface ); - - // Unregister events for old interface - Event.Unregister( oldGameInterface ); - - ConsoleSystem.Internal.ClearGameCVars(); - } - - // Now that everything's been upgraded, swap the new interface - // and assembly in - Swap( newAssembly, newInterface ); - - Notify.AddNotification( $"Build successful!", $"Compiled '{_projectAssemblyInfo.AssemblyName}'!", FontAwesome.FaceGrinStars ); - - ConsoleSystem.Internal.RegisterAssembly( newAssembly, extraFlags: CVarFlags.Game ); - - Event.Run( Event.Game.HotloadAttribute.Name ); - - if ( !_buildRequested ) - return; - - _buildRequested = false; - await Build(); - } - - /// - /// Upgrades all entities that were affected by the swap. - /// - /// The old assembly being unloaded. - /// The new assembly being loaded. - private static void UpgradeEntities( Assembly oldAssembly, Assembly newAssembly ) - { - var entityRegistryCopy = EntityRegistry.Instance.ToList(); - - for ( int i = 0; i < entityRegistryCopy.Count; i++ ) - { - var entity = entityRegistryCopy[i]; - var entityType = entity.GetType(); - - // Do we actually want to upgrade this? If not, skip. - if ( entityType.Assembly != oldAssembly ) - continue; - - // Unregister the old entity - EntityRegistry.Instance.UnregisterEntity( entity ); - - // Find new type for entity in new assembly - var newType = newAssembly.GetType( entityType.FullName ?? entityType.Name )!; - var newEntity = (IEntity)FormatterServices.GetUninitializedObject( newType )!; - - // Have we already upgraded this? - if ( Upgrader.UpgradedReferences.TryGetValue( entity.GetHashCode(), out var upgradedValue ) ) - { - newEntity = (IEntity)upgradedValue; - } - else - { - Upgrader.UpgradedReferences[entity.GetHashCode()] = newEntity; - Upgrader.UpgradeInstance( entity, newEntity ); - } - - // If we created a new entity successfully, register it - if ( newEntity is not null ) - EntityRegistry.Instance.RegisterEntity( newEntity ); - } - } - - /// - /// Finds and creates an entry point from the project assembly. - /// - /// The assembly to search. - /// The created entry point from the project assembly. - /// Thrown when no valid entry point was found. - private static TEntryPoint CreateEntryPointFromAssembly( Assembly assembly ) - { - var tType = typeof( TEntryPoint ); - // Find first type that derives from interface T - foreach ( var type in assembly.GetTypes() ) - { - if ( type.GetInterface( tType.FullName ?? tType.Name ) is not null ) - return (TEntryPoint)Activator.CreateInstance( type )!; - } - - throw new EntryPointNotFoundException( $"Could not find implementation of '{tType.Name}'" ); - } - - /// - /// Creates the file system watcher to check for file changes in the project. - /// - /// - private void CreateFileWatchers() - { - _projWatcher = new FileSystemWatcher( - Path.GetDirectoryName( _projectAssemblyInfo.ProjectPath )!, - Path.GetFileName( _projectAssemblyInfo.ProjectPath ) ) - { - NotifyFilter = NotifyFilters.Attributes - | NotifyFilters.CreationTime - | NotifyFilters.DirectoryName - | NotifyFilters.FileName - | NotifyFilters.LastAccess - | NotifyFilters.LastWrite - | NotifyFilters.Security - | NotifyFilters.Size - }; - - _projWatcher.Changed += OnCsProjChanged; - _projWatcher.EnableRaisingEvents = true; - - _codeWatcher = new FileSystemWatcher( _projectAssemblyInfo.SourceRoot, "*.cs" ) - { - NotifyFilter = NotifyFilters.Attributes - | NotifyFilters.CreationTime - | NotifyFilters.DirectoryName - | NotifyFilters.FileName - | NotifyFilters.LastAccess - | NotifyFilters.LastWrite - | NotifyFilters.Security - | NotifyFilters.Size - }; - - // Visual Studio will create a new temporary file, write to it, - // delete the cs file and then rename the temporary file to - // match the deleted file - // The Renamed event will catch this nicely for us, because renaming - // is the last thing that happens in the order of operations - - // This will typically happen twice, so we'll gate it with a TimeSince too - _codeWatcher.Renamed += OnFileChanged; - _codeWatcher.IncludeSubdirectories = true; - _codeWatcher.EnableRaisingEvents = true; - } - - /// - /// Invoked when the csproj has changed. - /// - private void OnCsProjChanged( object sender, FileSystemEventArgs e ) - { - // This will typically fire twice, so gate it with a TimeSince - if ( _timeSinceCsProjChange < 1 ) - return; - - _timeSinceCsProjChange = 0; - - if ( _buildTask.IsCompleted ) - _buildTask = Build(); - else - _buildRequested = true; - } - - /// - /// Invoked when a file system change has occurred. - /// - private void OnFileChanged( object sender, FileSystemEventArgs e ) - { - // This will typically fire twice, so gate it with a TimeSince - if ( _timeSinceLastFileChange < 1 ) - return; - - // This might be a directory - if it is then skip - if ( string.IsNullOrEmpty( Path.GetExtension( e.FullPath ) ) ) - return; - - _timeSinceLastFileChange = 0f; - - if ( _buildTask.IsCompleted ) - _buildTask = Build(); - else - _buildRequested = true; - } -} diff --git a/Source/Mocha.Hotload/AssemblyInfo.cs b/Source/Mocha.Hotload/AssemblyInfo.cs new file mode 100644 index 00000000..2f43e851 --- /dev/null +++ b/Source/Mocha.Hotload/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible( false )] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid( "dab0ab0f-03a6-4fe4-9b7e-4f952eff181f" )] +[assembly: InternalsVisibleTo( "Mocha.Tests" )] diff --git a/Source/Mocha.Hotload/Compilation/CompileOptions.cs b/Source/Mocha.Hotload/Compilation/CompileOptions.cs index fe8a538d..df1438ea 100644 --- a/Source/Mocha.Hotload/Compilation/CompileOptions.cs +++ b/Source/Mocha.Hotload/Compilation/CompileOptions.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Compilation; /// /// A container for all options to give to the compiler. diff --git a/Source/Mocha.Hotload/Compilation/CompileResult.cs b/Source/Mocha.Hotload/Compilation/CompileResult.cs index b842ff2e..f3c0db11 100644 --- a/Source/Mocha.Hotload/Compilation/CompileResult.cs +++ b/Source/Mocha.Hotload/Compilation/CompileResult.cs @@ -1,4 +1,6 @@ -namespace Mocha.Hotload; +using Microsoft.CodeAnalysis; + +namespace Mocha.Hotload.Compilation; /// /// Represents a final compilation result. @@ -10,6 +12,11 @@ internal readonly struct CompileResult /// internal bool WasSuccessful { get; } + /// + /// The workspace that can be used for incremental builds. + /// + internal AdhocWorkspace? Workspace { get; } + /// /// The bytes of the compiled assembly. /// @@ -28,10 +35,19 @@ internal readonly struct CompileResult /// internal bool HasSymbols => CompiledAssemblySymbols is not null; - private CompileResult( bool wasSuccessful, byte[]? compiledAssembly = null, byte[]? compiledAssemblySymbols = null, string[]? errors = null ) + /// + /// Initializes a new instance of . + /// + /// Whether or not the compilation was successful. + /// The workspace that was created/updated. Null if is false. + /// The compiled assembly in a byte array. Null if is false. + /// The compiled assembly's debug symbols. Null if no symbols or if is false. + /// An array containing all errors that occurred during compilation. Null if is true. + private CompileResult( bool wasSuccessful, AdhocWorkspace? workspace, byte[]? compiledAssembly = null, byte[]? compiledAssemblySymbols = null, string[]? errors = null ) { WasSuccessful = wasSuccessful; + Workspace = workspace; CompiledAssembly = compiledAssembly; CompiledAssemblySymbols = compiledAssemblySymbols; Errors = errors; @@ -46,6 +62,7 @@ internal static CompileResult Failed( string[] errors ) { return new CompileResult( wasSuccessful: false, + workspace: null, errors: errors ); } @@ -54,12 +71,13 @@ internal static CompileResult Failed( string[] errors ) /// Shorthand method to create a successful . /// /// The bytes of the compiled assembly. - /// The bytes of the symbols contained in the compiled assembly. + /// The bytes of the symbols contained in the compiled assembly. Null if no debug symbols. /// The newly created . - internal static CompileResult Successful( byte[] compiledAssembly, byte[]? compiledAssemblySymbols ) + internal static CompileResult Successful( AdhocWorkspace workspace, byte[] compiledAssembly, byte[]? compiledAssemblySymbols ) { return new CompileResult( wasSuccessful: true, + workspace: workspace, compiledAssembly: compiledAssembly, compiledAssemblySymbols: compiledAssemblySymbols ); diff --git a/Source/Mocha.Hotload/Compilation/Compiler.cs b/Source/Mocha.Hotload/Compilation/Compiler.cs index 75eca1c7..5069fd88 100644 --- a/Source/Mocha.Hotload/Compilation/Compiler.cs +++ b/Source/Mocha.Hotload/Compilation/Compiler.cs @@ -1,14 +1,11 @@ -using Microsoft.Build.Evaluation; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Text; -using Mocha.Common; -using NuGet.Versioning; -using System.Reflection; -using System.Runtime.Versioning; +using Mocha.Hotload.Projects; +using System.Text; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Compilation; /// /// Contains the core functionality for compilation of C# assemblies. @@ -62,142 +59,196 @@ internal static class Compiler "System.Threading.Channels.dll", + "System.Net.Http.dll", "System.Web.HttpUtility.dll", "System.Xml.ReaderWriter.dll", }; + private static readonly Dictionary s_referenceCache = new(); + /// /// Compiles a given project assembly. /// /// The project assembly to compile. /// The options to give to the C# compilation. - /// A task that represents the asynchronous operation. The tasks return value is the result of the compilation. - internal static async Task Compile( ProjectAssemblyInfo assemblyInfo, CompileOptions? compileOptions = null ) + /// A that represents the asynchronous operation. The s return value is the result of the compilation. + /// Thrown when the final workspace project becomes invalid unexpectedly. + internal static async Task CompileAsync( ProjectAssemblyInfo assemblyInfo, CompileOptions? compileOptions = null ) { - using var _ = new Stopwatch( $"{assemblyInfo.AssemblyName} compile" ); - compileOptions ??= new CompileOptions { OptimizationLevel = OptimizationLevel.Debug, - GenerateSymbols = true, + GenerateSymbols = true }; + // Setup a basic list of tasks. + var basicTasks = new List(); + // - // Fetch the project and all source files + // Fetch the project and all source files. // - ProjectCollection projectCollection = new(); - var project = new Project( assemblyInfo.ProjectPath, null, null, projectCollection ); + var csproj = CSharpProject.FromFile( assemblyInfo.ProjectPath ); + var parseOptions = CSharpParseOptions.Default + .WithPreprocessorSymbols( csproj.PreProcessorSymbols ); var syntaxTrees = new List(); - var embeddedTexts = new List(); - // Global namespaces, etc. - var globalUsings = string.Empty; - foreach ( var usingEntry in project.GetItems( "Using" ) ) + // Build syntax trees. { - var isStatic = bool.Parse( usingEntry.GetMetadataValue( "Static" ) ); - globalUsings += $"global using{(isStatic ? " static " : " ")}{usingEntry.EvaluatedInclude};{Environment.NewLine}"; - } - - if ( globalUsings != string.Empty ) - syntaxTrees.Add( CSharpSyntaxTree.ParseText( globalUsings ) ); + // Global namespaces. + var globalUsings = string.Empty; + foreach ( var usingEntry in csproj.Usings ) + globalUsings += $"global using{(usingEntry.Value ? " static " : " ")}{usingEntry.Key};{Environment.NewLine}"; - // For each source file, create a syntax tree we can use to compile it - foreach ( var item in project.GetItems( "Compile" ) ) - { - var filePath = item.EvaluatedInclude; + if ( globalUsings != string.Empty ) + syntaxTrees.Add( CSharpSyntaxTree.ParseText( globalUsings, options: parseOptions, encoding: Encoding.UTF8 ) ); - // Get path based on project root - filePath = Path.Combine( assemblyInfo.SourceRoot, filePath ); + // For each source file, create a syntax tree we can use to compile it. + foreach ( var filePath in csproj.CSharpFiles ) + { + // Add the parsed syntax tree. + basicTasks.Add( Task.Run( async () => + { + var text = await File.ReadAllTextAsync( filePath ); + syntaxTrees.Add( CSharpSyntaxTree.ParseText( text, options: parseOptions, encoding: Encoding.UTF8, path: filePath ) ); + } ) ); + } + + // Wait for all tasks to finish before continuing. + await Task.WhenAll( basicTasks ); + // Clear this list for any users later on. + basicTasks.Clear(); + } - // File path should be absolute - filePath = Path.GetFullPath( filePath ); + // Stripping syntax trees. + { + // Strip out methods marked with [ServerOnly] or [ClientOnly] attribute based on the current realm. + var newSyntaxTrees = new List(); + // Which attribute do we want to remove (or, in other words, which realm are we not in). + var targetAttribute = assemblyInfo.IsServer ? "ClientOnly" : "ServerOnly"; + var stripTasks = new List>(); - var encoding = System.Text.Encoding.Default; + // Walk all syntax trees and strip them. + foreach ( var tree in syntaxTrees ) + stripTasks.Add( StripSyntaxTreeAsync( tree, targetAttribute ) ); - var fileText = File.ReadAllText( filePath ); - var sourceText = SourceText.From( fileText, encoding ); + // Wait for all tasks to finish before continuing. + await Task.WhenAll( stripTasks ); - var syntaxTree = CSharpSyntaxTree.ParseText( sourceText, path: filePath ); + // Add all stripped syntax trees. + foreach ( var stripTask in stripTasks ) + { + if ( stripTask.Result is null ) + continue; - syntaxTrees.Add( syntaxTree ); + newSyntaxTrees.Add( stripTask.Result ); + } - if ( compileOptions.GenerateSymbols ) - embeddedTexts.Add( EmbeddedText.FromSource( filePath, sourceText ) ); + syntaxTrees = newSyntaxTrees; } // - // Build up references + // Build up references. // var references = new List(); - - // System references - string dotnetBaseDir = Path.GetDirectoryName( typeof( object ).Assembly.Location )!; - foreach ( var systemReference in s_systemReferences ) - references.Add( CreateMetadataReferenceFromPath( Path.Combine( dotnetBaseDir, systemReference ) ) ); - - // NuGet references - foreach ( var packageReference in project.GetItems( "PackageReference" ) ) - await NuGetHelper.FetchPackage( packageReference.EvaluatedInclude, new NuGetVersion( packageReference.GetMetadataValue( "Version" ) ), references ); - - // Project references - // TODO: This is nightmare fuel, need a better solution long-term. - foreach ( var projectReference in project.GetItems( "ProjectReference" ) ) { - var referenceCsprojPath = Path.GetFullPath( Path.Combine( Path.GetDirectoryName( assemblyInfo.ProjectPath )!, projectReference.EvaluatedInclude ) ); - var referenceProject = new Project( referenceCsprojPath, null, null, projectCollection ); - var assemblyName = referenceProject.GetPropertyValue( "AssemblyName" ); - - if ( !string.IsNullOrEmpty( assemblyName ) ) - references.Add( CreateMetadataReferenceFromPath( "build\\" + assemblyName + ".dll" ) ); - else - references.Add( CreateMetadataReferenceFromPath( "build\\" + Path.GetFileNameWithoutExtension( referenceCsprojPath ) + ".dll" ) ); + // System references. + var dotnetBaseDir = Path.GetDirectoryName( typeof( object ).Assembly.Location )!; + foreach ( var systemReference in s_systemReferences ) + references.Add( CreateMetadataReferenceFromPath( Path.Combine( dotnetBaseDir, systemReference ) ) ); + + // NuGet references. + foreach ( var packageReference in csproj.PackageReferences ) + basicTasks.Add( NuGetHelper.FetchPackageAsync( packageReference.Key, packageReference.Value, references ) ); + + // Wait for all tasks to finish before continuing. + await Task.WhenAll( basicTasks ); + // Clear this list for any users later on. + basicTasks.Clear(); + + // Project references. + // TODO: This is nightmare fuel, need a better solution long-term. + foreach ( var projectReference in csproj.ProjectReferences ) + { + var referenceCsprojPath = Path.GetFullPath( Path.Combine( Path.GetDirectoryName( assemblyInfo.ProjectPath )!, projectReference ) ); + var referenceProject = CSharpProject.FromFile( referenceCsprojPath ); + var assemblyName = referenceProject.AssemblyName; + + if ( !string.IsNullOrEmpty( assemblyName ) ) + references.Add( CreateMetadataReferenceFromPath( "build\\" + assemblyName + ".dll" ) ); + else + references.Add( CreateMetadataReferenceFromPath( "build\\" + Path.GetFileNameWithoutExtension( referenceCsprojPath ) + ".dll" ) ); + } + + // Literal references. + foreach ( var reference in csproj.DllReferences ) + references.Add( CreateMetadataReferenceFromPath( Path.GetFullPath( reference ) ) ); } - // Literal references - foreach ( var reference in project.GetItems( "Reference" ) ) - references.Add( CreateMetadataReferenceFromPath( Path.GetFullPath( reference.EvaluatedInclude ) ) ); - // - // Set up compiler + // Setup compilation. // + // Setup compile options. var options = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) .WithPlatform( Platform.X64 ) .WithOptimizationLevel( compileOptions.OptimizationLevel ) - .WithConcurrentBuild( true ); + .WithConcurrentBuild( true ) + .WithAllowUnsafe( csproj.AllowUnsafeBlocks ); - var unsafeBlocksAllowed = project.GetPropertyValue( "AllowUnsafeBlocks" ); - if ( unsafeBlocksAllowed != string.Empty ) - options = options.WithAllowUnsafe( bool.Parse( unsafeBlocksAllowed ) ); - else - options = options.WithAllowUnsafe( false ); + // Setup incremental workspace. + var workspace = new AdhocWorkspace(); - var compilation = CSharpCompilation.Create( + // Setup project. + var projectInfo = Microsoft.CodeAnalysis.ProjectInfo.Create( + ProjectId.CreateNewId( assemblyInfo.AssemblyName ), + VersionStamp.Create(), assemblyInfo.AssemblyName, - syntaxTrees, - references, - options - ); + assemblyInfo.AssemblyName, + LanguageNames.CSharp, + compilationOptions: options, + parseOptions: parseOptions, + metadataReferences: references ); + var project = workspace.AddProject( projectInfo ); + + // Add documents to workspace. + foreach ( var syntaxTree in syntaxTrees ) + { + var documentInfo = DocumentInfo.Create( + DocumentId.CreateNewId( project.Id ), + Path.GetFileName( syntaxTree.FilePath ), + filePath: syntaxTree.FilePath, + sourceCodeKind: SourceCodeKind.Regular, + loader: TextLoader.From( TextAndVersion.Create( syntaxTree.GetText(), VersionStamp.Create() ) ) ); + + workspace.AddDocument( documentInfo ); + } + + project = workspace.CurrentSolution.GetProject( project.Id ); - // Unload projects - projectCollection.UnloadAllProjects(); + // Panic if project became invalid. + if ( project is null ) + throw new System.Diagnostics.UnreachableException(); // - // Compile assembly into memory + // Compile. // + using var assemblyStream = new MemoryStream(); using var symbolsStream = compileOptions.GenerateSymbols ? new MemoryStream() : null; + // Setup emit options. EmitOptions? emitOptions = null; if ( compileOptions.GenerateSymbols ) { emitOptions = new EmitOptions( - debugInformationFormat: DebugInformationFormat.PortablePdb, - pdbFilePath: $"{assemblyInfo.AssemblyName}.pdb" ); + debugInformationFormat: DebugInformationFormat.PortablePdb, + pdbFilePath: $"{assemblyInfo.AssemblyName}.pdb" ); } + // Compile. Panic if compilation becomes invalid. + var compilation = await project.GetCompilationAsync() ?? throw new System.Diagnostics.UnreachableException(); var result = compilation.Emit( assemblyStream, symbolsStream, @@ -223,7 +274,184 @@ internal static async Task Compile( ProjectAssemblyInfo assemblyI Log.Info( $"Compiled {assemblyInfo.AssemblyName} successfully" ); - return CompileResult.Successful( assemblyStream.ToArray(), symbolsStream?.ToArray() ); + return CompileResult.Successful( workspace, assemblyStream.ToArray(), symbolsStream?.ToArray() ); + } + + /// + /// Compiles a assembly with incremental changes. + /// + /// The that contains the code. + /// A dictionary of absolute file paths mapped to the type of change it has experienced. + /// The projects . + /// The to give to the C# compilation. + /// A that represents the asynchronous operation. The s return value is the result of the compilation. + /// Thrown when applying changes to the failed. + internal static async Task IncrementalCompileAsync( AdhocWorkspace workspace, IReadOnlyDictionary changedFilePaths, ProjectAssemblyInfo assemblyInfo, CompileOptions? compileOptions = null ) + { + compileOptions ??= new CompileOptions + { + OptimizationLevel = OptimizationLevel.Debug, + GenerateSymbols = true, + }; + var parseOptions = (CSharpParseOptions)workspace.CurrentSolution.Projects.First().ParseOptions!; + + // Which attribute do we want to remove (or, in other words, which realm are we not in). + var targetAttribute = assemblyInfo.IsServer ? "ClientOnly" : "ServerOnly"; + + // Update each changed file. + foreach ( var (filePath, changeType) in changedFilePaths ) + { + switch ( changeType ) + { + case WatcherChangeTypes.Created: + { + var syntaxTree = CSharpSyntaxTree.ParseText( + await File.ReadAllTextAsync( filePath ), + options: parseOptions, + encoding: Encoding.UTF8, + path: filePath ); + syntaxTree = await StripSyntaxTreeAsync( syntaxTree, targetAttribute ); + + if ( syntaxTree is null ) + continue; + + var documentInfo = DocumentInfo.Create( + DocumentId.CreateNewId( workspace.CurrentSolution.ProjectIds[0] ), + Path.GetFileName( syntaxTree.FilePath ), + filePath: syntaxTree.FilePath, + sourceCodeKind: SourceCodeKind.Regular, + loader: TextLoader.From( TextAndVersion.Create( syntaxTree.GetText(), VersionStamp.Create() ) ) ); + + workspace.AddDocument( documentInfo ); + if ( !workspace.TryApplyChanges( workspace.CurrentSolution ) ) + throw new System.Diagnostics.UnreachableException(); + break; + } + case WatcherChangeTypes.Deleted: + { + // Find the existing document for the deleted file. + var document = workspace.CurrentSolution.GetDocumentIdsWithFilePath( filePath ) + .Select( workspace.CurrentSolution.GetDocument ) + .FirstOrDefault(); + + if ( document is null ) + continue; + + // Apply the removed document. + if ( !workspace.TryApplyChanges( workspace.CurrentSolution.RemoveDocument( document.Id ) ) ) + throw new System.Diagnostics.UnreachableException(); + break; + } + case WatcherChangeTypes.Changed: + case WatcherChangeTypes.Renamed: + { + // Find the existing document for the changed file. + var document = workspace.CurrentSolution.GetDocumentIdsWithFilePath( filePath ) + .Select( workspace.CurrentSolution.GetDocument ) + .FirstOrDefault(); + + if ( document is null ) + continue; + + var syntaxTree = CSharpSyntaxTree.ParseText( + await File.ReadAllTextAsync( filePath ), + options: parseOptions, + encoding: Encoding.UTF8, + path: filePath ); + syntaxTree = await StripSyntaxTreeAsync( syntaxTree, targetAttribute ); + + // TODO: Remove the document? + if ( syntaxTree is null ) + continue; + + // Apply the changed tree. + if ( !workspace.TryApplyChanges( workspace.CurrentSolution.WithDocumentSyntaxRoot( document.Id, syntaxTree.GetRoot() ) ) ) + throw new System.Diagnostics.UnreachableException(); + + break; + } + } + } + + using var assemblyStream = new MemoryStream(); + using var symbolsStream = compileOptions.GenerateSymbols ? new MemoryStream() : null; + + // Setup emit options. + EmitOptions? emitOptions = null; + if ( compileOptions.GenerateSymbols ) + { + emitOptions = new EmitOptions( + debugInformationFormat: DebugInformationFormat.PortablePdb, + pdbFilePath: $"{assemblyInfo.AssemblyName}.pdb" ); + } + + // Compile. + var compilation = await workspace.CurrentSolution.Projects.First().GetCompilationAsync() ?? throw new System.Diagnostics.UnreachableException(); + var result = compilation.Emit( + assemblyStream, + symbolsStream, + options: emitOptions + ); + + if ( !result.Success ) + { + Log.Error( $"Failed to compile {assemblyInfo.AssemblyName}!" ); + + var failures = result.Diagnostics.Where( diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error ); + + var errors = failures.Select( diagnostic => + { + var lineSpan = diagnostic.Location.GetLineSpan(); + return $"\n{diagnostic.Id}: {diagnostic.GetMessage()}\n\tat {lineSpan.Path} line {lineSpan.StartLinePosition.Line}"; + } ).ToArray(); + + return CompileResult.Failed( errors ); + } + + Log.Info( $"Compiled {assemblyInfo.AssemblyName} successfully" ); + + return CompileResult.Successful( workspace, assemblyStream.ToArray(), symbolsStream?.ToArray() ); + } + + /// + /// Walks a and strips any that use specified attributes. + /// + /// The to walk. + /// The names of all the attributes to search for and strip. + /// + /// A that represents the asynchronous operation. The s return value is the stripped . + /// A null result occurs when the is completely stripped. + /// + private async static Task StripSyntaxTreeAsync( SyntaxTree syntaxTree, params string[] attributesToStrip ) + { + var root = await syntaxTree.GetRootAsync(); + var syntaxToStrip = new List(); + + // Walk all delcarations and mark them for stripping. + foreach ( var declaration in root.DescendantNodes().OfType() ) + { + // Ignore namespace declarations. + if ( declaration is BaseNamespaceDeclarationSyntax ) + continue; + + foreach ( var attributeName in attributesToStrip ) + { + var attribute = declaration.AttributeLists + .SelectMany( x => x.Attributes ) + .FirstOrDefault( x => x.Name.ToString() == attributeName ); + + if ( attribute is null ) + continue; + + syntaxToStrip.Add( declaration ); + break; + } + } + + // Strip all syntax and return the final tree. + return root.RemoveNodes( syntaxToStrip, SyntaxRemoveOptions.KeepNoTrivia )?.SyntaxTree; } /// @@ -244,24 +472,11 @@ internal static IEnumerable CreateMetadataReference /// A from a relative path. internal static PortableExecutableReference CreateMetadataReferenceFromPath( string assemblyPath ) { - return MetadataReference.CreateFromFile( assemblyPath ); - } - - /// - /// Returns the target framework of the application. - /// - /// The target framework of the application. - internal static string GetTargetFrameworkName() - { - // AppContext.TargetFrameworkName will always be null since the starting process is native code. - // Leave it here anyway in case this changes. - if ( !string.IsNullOrEmpty( AppContext.TargetFrameworkName ) ) - return AppContext.TargetFrameworkName; - - // Fallback on the TargetFrameworkAttribute of the Hotload assembly - if ( Assembly.GetExecutingAssembly().GetCustomAttribute() is not TargetFrameworkAttribute frameworkAttribute ) - return string.Empty; + if ( s_referenceCache.TryGetValue( assemblyPath, out var reference ) ) + return reference; - return frameworkAttribute.FrameworkName; + var newReference = MetadataReference.CreateFromFile( assemblyPath ); + s_referenceCache.Add( assemblyPath, newReference ); + return newReference; } } diff --git a/Source/Mocha.Hotload/Compilation/CompilerHelper.cs b/Source/Mocha.Hotload/Compilation/CompilerHelper.cs new file mode 100644 index 00000000..fa9046bd --- /dev/null +++ b/Source/Mocha.Hotload/Compilation/CompilerHelper.cs @@ -0,0 +1,59 @@ +using Mocha.Common; +using System.Reflection; +using System.Runtime.Versioning; + +namespace Mocha.Hotload.Compilation; + +/// +/// A collection of helper members for C# runtime projects and compilation. +/// +internal static class CompilerHelper +{ + /// + /// Defines the way the process was compiled. Used for parsing which csproj items to select in projects. + /// +#if DEBUG + internal const string Build = "Debug"; +#else + internal const string Build = "Release"; +#endif + + /// + /// Returns the realm the runtime is operating in. + /// NOTE: You should only access this on the main thread. + /// + internal static string Realm => NativeEngine.IsDedicatedServer() || Core.IsServer ? "Server" : "Client"; + + /// + /// Returns the target framework of the application. + /// + /// The target framework of the application. + internal static string GetTargetFrameworkName() + { + // AppContext.TargetFrameworkName will always be null since the starting process is native code. + // Leave it here anyway in case this changes. + if ( !string.IsNullOrEmpty( AppContext.TargetFrameworkName ) ) + return AppContext.TargetFrameworkName; + + // Fallback on the TargetFrameworkAttribute of the Hotload assembly + if ( Assembly.GetExecutingAssembly().GetCustomAttribute() is not TargetFrameworkAttribute frameworkAttribute ) + return string.Empty; + + return frameworkAttribute.FrameworkName; + } + + /// + /// Returns the C# target framework moniker in a format that the csproj standard supports. + /// https://learn.microsoft.com/en-us/dotnet/standard/frameworks + /// + /// The C# target framework moniker in a format that the csproj standard supports. + internal static string GetCSharpProjectMoniker() + { + var frameworkName = GetTargetFrameworkName(); + var parts = frameworkName.Split( ',' ); + var shortName = parts[0].Replace( ".NETCoreApp", "net" ); + var version = parts[1]["Version=v".Length..]; + + return shortName + version; + } +} diff --git a/Source/Mocha.Hotload/Compilation/NuGetHelper.cs b/Source/Mocha.Hotload/Compilation/NuGetHelper.cs index d0e32164..96c4a9ab 100644 --- a/Source/Mocha.Hotload/Compilation/NuGetHelper.cs +++ b/Source/Mocha.Hotload/Compilation/NuGetHelper.cs @@ -6,7 +6,7 @@ using NuGet.Protocol.Core.Types; using NuGet.Versioning; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Compilation; /// /// A collection of helper methods for the NuGet.Protocol package. @@ -19,8 +19,8 @@ internal static class NuGetHelper /// The ID of the NuGet package. /// The version of the NuGet package. /// The references to append the NuGet package to. - /// A task that represents the asynchronous operation. - internal static async Task FetchPackage( string id, NuGetVersion version, ICollection references ) + /// A that represents the asynchronous operation. + internal static async Task FetchPackageAsync( string id, NuGetVersion version, ICollection references ) { // Setup. var logger = NullLogger.Instance; @@ -45,7 +45,7 @@ await resource.CopyNupkgToStreamAsync( var nuspecReader = await packageReader.GetNuspecReaderAsync( cancellationToken ); // Find the framework target we want. - var currentFramework = NuGetFramework.ParseFrameworkName( Compiler.GetTargetFrameworkName(), DefaultFrameworkNameProvider.Instance ); + var currentFramework = NuGetFramework.ParseFrameworkName( CompilerHelper.GetTargetFrameworkName(), DefaultFrameworkNameProvider.Instance ); var targetFrameworkGroup = NuGetFrameworkExtensions.GetNearest( packageReader.GetLibItems(), currentFramework ); var dependencies = nuspecReader.GetDependencyGroups().First( group => group.TargetFramework == targetFrameworkGroup.TargetFramework ).Packages.ToArray(); @@ -53,7 +53,7 @@ await resource.CopyNupkgToStreamAsync( if ( dependencies.Length > 0 ) { foreach ( var dependency in dependencies ) - await FetchPackageWithVersionRange( dependency.Id, dependency.VersionRange, references ); + await FetchPackageWithVersionRangeAsync( dependency.Id, dependency.VersionRange, references ); } if ( !targetFrameworkGroup.Items.Any() ) @@ -74,8 +74,8 @@ await resource.CopyNupkgToStreamAsync( /// The ID of the NuGet package. /// The range of versions to look at. /// The references to append the NuGet package to. - /// A task that represents the asynchronous operation. - internal static async Task FetchPackageWithVersionRange( string id, VersionRange versionRange, ICollection references ) + /// A that represents the asynchronous operation. + internal static async Task FetchPackageWithVersionRangeAsync( string id, VersionRange versionRange, ICollection references ) { // Setup. var cache = new SourceCacheContext(); @@ -92,6 +92,6 @@ internal static async Task FetchPackageWithVersionRange( string id, VersionRange // Find the best version and get it. var bestVersion = versionRange.FindBestMatch( versions ); - await FetchPackage( id, bestVersion, references ); + await FetchPackageAsync( id, bestVersion, references ); } } diff --git a/Source/Mocha.Hotload/Generators/ProjectGenerator.cs b/Source/Mocha.Hotload/Generators/ProjectGenerator.cs deleted file mode 100644 index e8e8ac89..00000000 --- a/Source/Mocha.Hotload/Generators/ProjectGenerator.cs +++ /dev/null @@ -1,145 +0,0 @@ -using NuGet.Packaging; -using System.Text; -using System.Xml; - -namespace Mocha.Hotload; - -/// -/// Generates a .csproj based on a given ProjectManfiest -/// -internal static class ProjectGenerator -{ - /* - * - We want to generate a csproj based on the manifest given - * - * - 1. Generate the project file's contents: - * + Basic properties (output type, target framework, etc) - * + References to all the assemblies in the project - * + How do we specify files to compile? - * - 2. Save the project file to disk - * - 3. Use the path of the project file for compilation - should - * probably return this in a variable somewhere - */ - - /// - /// Generates a csproj for the given project and returns the path to the - /// generated project on disk. - /// - /// An absolute path to the generated .csproj file. - internal static string Generate( in ProjectManifest manifest ) - { - // Setup. - var destinationPath = Path.Combine( manifest.Resources.Code, "code.csproj" ); - var baseReferenceDir = Path.GetFullPath( "build\\" ); - var project = manifest.Project; - - var document = new XmlDocument(); - var rootElement = document.CreateElement( "Project" ); - rootElement.SetAttribute( "Sdk", "Microsoft.NET.Sdk" ); - document.AppendChild( rootElement ); - - // Basic configuration. - { - var basics = rootElement.CreateElement( "PropertyGroup" ); - basics.CreateElementWithInnerText( "OutputType", "Library" ); - basics.CreateElementWithInnerText( "TargetFramework", "net7.0" ); - basics.CreateElementWithInnerText( "ImplicitUsings", project.ImplicitUsings ? "enable" : "disable" ); - basics.CreateElementWithInnerText( "AllowUnsafeBlocks", project.AllowUnsafeBlocks ? "True" : "False" ); - basics.CreateElementWithInnerText( "LangVersion", project.LanguageVersion ?? "latest" ); - basics.CreateElementWithInnerText( "Platforms", "x64" ); - basics.CreateElementWithInnerText( "BaseOutputPath", "$(SolutionDir)..\\build" ); - basics.CreateElementWithInnerText( "OutputPath", "$(SolutionDir)..\\build" ); - basics.CreateElementWithInnerText( "AppendTargetFrameworkToOutputPath", "false" ); - basics.CreateElementWithInnerText( "PreserveCompilationReferences", "true" ); - basics.CreateElementWithInnerText( "PreserveCompilationContext", "true" ); - basics.CreateElementWithInnerText( "Nullable", project.Nullable ? "true" : "false" ); - basics.CreateElementWithInnerText( "AssemblyName", manifest.Name ); - basics.CreateElementWithInnerText( "RootNamespace", project.DefaultNamespace ?? "Mocha" ); - } - - // Implicit usings. - { - var usings = rootElement.CreateElement( "ItemGroup" ); - if ( project.UseMochaGlobal ?? true ) - usings.CreateElementWithAttributes( "Using", "Include", "Mocha.Common.Global", "Static", "true" ); - - // Add any custom usings. - if ( project.Usings is not null ) - { - foreach ( var usingDef in project.Usings ) - usings.CreateElementWithAttributes( "Using", "Include", usingDef.Namespace, "Static", usingDef.Static ? "true" : "false" ); - } - - // Remove the element if there were no usings. - if ( usings.ChildNodes.Count == 0 ) - rootElement.RemoveChild( usings ); - } - - // Cleanup entries. - { - var cleanup = rootElement.CreateElement( "ItemGroup" ); - cleanup.CreateElementWithAttributes( "Compile", "Remove", "bin\\**" ); - cleanup.CreateElementWithAttributes( "EmbeddedResource", "Remove", "bin\\**" ); - cleanup.CreateElementWithAttributes( "None", "Remove", "bin\\**" ); - } - - // Mocha references. - { - var references = rootElement.CreateElement( "ItemGroup" ); - references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.Common.dll" ); - references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.Engine.dll" ); - references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.UI.dll" ); - } - - // NuGet package references. - if ( project.PackageReferences is not null ) - { - var packageReferences = rootElement.CreateElement( "ItemGroup" ); - foreach ( var packageReference in project.PackageReferences ) - { - var element = packageReferences.CreateElementWithAttributes( "PackageReference", "Include", packageReference.Name, "Version", packageReference.Version ); - if ( packageReference.IncludeAssets is not null ) - element.CreateElementWithInnerText( "IncludeAssets", packageReference.IncludeAssets ); - if ( packageReference.PrivateAssets is not null ) - element.CreateElementWithInnerText( "PrivateAssets", packageReference.PrivateAssets ); - } - } - - // Project references. - if ( project.ProjectReferences is not null ) - { - var projectReferences = rootElement.CreateElement( "ItemGroup" ); - foreach ( var projectReference in project.ProjectReferences ) - { - var element = projectReferences.CreateElementWithAttributes( "PackageReference", "Include", baseReferenceDir + projectReference.Path ); - if ( projectReference.OutputItemType is not null ) - element.CreateElementWithInnerText( "IncludeAssets", projectReference.OutputItemType ); - if ( projectReference.PrivateAssets is not null ) - element.CreateElementWithInnerText( "PrivateAssets", projectReference.PrivateAssets ); - if ( projectReference.ReferenceOutputAssembly is not null ) - element.CreateElementWithInnerText( "PrivateAssets", projectReference.ReferenceOutputAssembly.Value ? "true" : "false" ); - } - } - - // Literal DLL references. - if ( project.References is not null ) - { - var references = rootElement.CreateElement( "ItemGroup" ); - foreach ( var reference in project.References ) - references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + reference ); - } - - // Write csproj to disk. - var stream = File.OpenWrite( destinationPath ); - var writer = new XmlTextWriter( stream, Encoding.UTF8 ) - { - Formatting = Formatting.Indented, - }; - document.WriteContentTo( writer ); - writer.Flush(); - writer.Close(); - - // Return the destination path. - return destinationPath; - } -} diff --git a/Source/Mocha.Hotload/Main.cs b/Source/Mocha.Hotload/Main.cs index 8bd94773..f1fdc6b8 100644 --- a/Source/Mocha.Hotload/Main.cs +++ b/Source/Mocha.Hotload/Main.cs @@ -1,45 +1,83 @@ global using static Mocha.Common.Global; using Mocha.Common; using Mocha.Common.Console; +using Mocha.Hotload.Projects; +using Mocha.Hotload.Upgrading; +using Mocha.Hotload.Util; using MochaTool.AssetCompiler; +using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; +using System.Xml; namespace Mocha.Hotload; +/// +/// Contains all of the functionality to bootstrap the C# land. +/// public static class Main { - private static ProjectAssembly s_game = null!; + /// + /// The assembly wrapper that represents the Mocha editor. + /// private static ProjectAssembly s_editor = null!; + /// + /// The assembly wrapper that represents the client side of the loaded project. + /// + private static ProjectAssembly s_client = null!; + /// + /// The assembly wrapper that represents the server side of the loaded project. + /// + private static ProjectAssembly s_server = null!; + + /// + /// The loaded project manifest. + /// private static ProjectManifest s_manifest; + /// + /// The responsible of checking when the manifest is changed on disk. + /// private static FileSystemWatcher s_manifestWatcher = null!; + /// + /// The time since the last change happened to the manifest on disk. + /// private static TimeSince s_timeSinceLastManifestChange; + /// + /// Bootstraps the C# land. + /// + /// The pointer to the for interoperability. [UnmanagedCallersOnly] public static void Run( IntPtr args ) { - // This MUST be done before everything - Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); - // Convert args to structure so we can use the function pointers. // This MUST be done before calling any native functions Global.UnmanagedArgs = Marshal.PtrToStructure( args ); + NativeEngine = new Glue.Root(); + NativeEngine.NativePtr = Global.UnmanagedArgs.__Root; // Initialize the logger Log = new NativeLogger(); + // We change some behaviour if we're on a dedicated server: + // - We don't compile for the client + // - We don't run the game in a "client" context + // - We don't render or draw anything + bool isDedicatedServer = NativeEngine.IsDedicatedServer(); + // TODO: Is there a better way to register these cvars? // Register cvars for assemblies that will never hotload - ConsoleSystem.Internal.RegisterAssembly( typeof( Mocha.Hotload.Main ).Assembly ); // Hotload - ConsoleSystem.Internal.RegisterAssembly( typeof( Mocha.Common.IGame ).Assembly ); // Common - ConsoleSystem.Internal.RegisterAssembly( typeof( Mocha.BaseGame ).Assembly ); // Engine + ConsoleSystem.Internal.RegisterAssembly( typeof( Main ).Assembly ); // Hotload + ConsoleSystem.Internal.RegisterAssembly( typeof( IGame ).Assembly ); // Common + ConsoleSystem.Internal.RegisterAssembly( typeof( BaseGame ).Assembly ); // Engine // Initialize upgrader, we do this as early as possible to prevent // slowdowns while the engine is running. Upgrader.Init(); // Get the current loaded project from native - var manifestPath = Glue.Engine.GetProjectPath(); + var manifestPath = NativeEngine.GetProjectPath(); var csprojPath = ReloadProjectManifest( manifestPath ); // Setup a watcher for the project manifest. @@ -48,20 +86,28 @@ public static void Run( IntPtr args ) Path.GetFileName( manifestPath )! ) { NotifyFilter = NotifyFilters.Attributes - | NotifyFilters.CreationTime - | NotifyFilters.DirectoryName - | NotifyFilters.FileName - | NotifyFilters.LastAccess - | NotifyFilters.LastWrite - | NotifyFilters.Security - | NotifyFilters.Size + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size }; s_manifestWatcher.Changed += OnProjectManifestChanged; s_manifestWatcher.EnableRaisingEvents = true; // Setup project assemblies. - var gameAssemblyInfo = new ProjectAssemblyInfo() + var serverAssemblyInfo = new ProjectAssemblyInfo() + { + AssemblyName = s_manifest.Name, + ProjectPath = csprojPath, + SourceRoot = s_manifest.Resources.Code, + IsServer = true + }; + + var clientAssemblyInfo = new ProjectAssemblyInfo() { AssemblyName = s_manifest.Name, ProjectPath = csprojPath, @@ -75,9 +121,6 @@ public static void Run( IntPtr args ) SourceRoot = "source\\Mocha.Editor", }; - s_game = new ProjectAssembly( gameAssemblyInfo ); - s_editor = new ProjectAssembly( editorAssemblyInfo ); - // Setup file system. FileSystem.Mounted = new FileSystem( s_manifest.Resources.Content, @@ -85,35 +128,93 @@ public static void Run( IntPtr args ) ); FileSystem.Mounted.AssetCompiler = new RuntimeAssetCompiler(); - // Start. - s_editor.EntryPoint.Startup(); - s_game.EntryPoint.Startup(); + // Create assemblies. + if ( isDedicatedServer ) + { + SetServerContext( true ); + + // TODO: Listen server logic + s_server = new ProjectAssembly( serverAssemblyInfo ); + + // Start. + s_server.EntryPoint?.Startup(); + } + else + { + SetServerContext( false ); + + s_editor = new ProjectAssembly( editorAssemblyInfo ); + // The editor should never fail to compile. + Debug.Assert( s_editor.EntryPoint is not null ); + s_client = new ProjectAssembly( clientAssemblyInfo ); + + // Start. + s_editor.EntryPoint.Startup(); + s_client.EntryPoint?.Startup(); + } } + /// + /// Update loop for the C# land. + /// [UnmanagedCallersOnly] public static void Update() { - Time.UpdateFrom( Glue.Engine.GetTickDeltaTime() ); + // Update time. + Time.UpdateFrom( NativeEngine.GetTickDeltaTime() ); + + // Update client realm. + if ( s_client is not null ) + { + SetServerContext( false ); + s_client.EntryPoint?.Update(); + + if ( s_client.Assembly is not null ) + Event.Run( s_client.Assembly, Event.TickAttribute.Name ); + } - s_game.EntryPoint.Update(); + // Update server realm. + if ( s_server is not null ) + { + SetServerContext( true ); + s_server.EntryPoint?.Update(); + + if ( s_server.Assembly is not null ) + Event.Run( s_server.Assembly, Event.TickAttribute.Name ); + } } + /// + /// Render loop for the C# land. + /// [UnmanagedCallersOnly] public static void Render() { - Time.UpdateFrom( Glue.Engine.GetTickDeltaTime() ); - Screen.UpdateFrom( Glue.Editor.GetRenderSize() ); + // Update. + Time.UpdateFrom( NativeEngine.GetFrameDeltaTime() ); + Screen.UpdateFrom( NativeEngine.GetRenderSize() ); Input.Update(); - s_game.EntryPoint.FrameUpdate(); + // Render client. + SetServerContext( false ); + s_client.EntryPoint?.FrameUpdate(); } + /// + /// Drawing editor loop for the C# land. + /// [UnmanagedCallersOnly] public static void DrawEditor() { + // Render editor. + SetServerContext( false ); s_editor.EntryPoint.FrameUpdate(); } + /// + /// Fires an event that came from unmanaged. + /// + /// The pointer to the UTF8 string that contains the event name. [UnmanagedCallersOnly] public static void FireEvent( IntPtr ptrEventName ) { @@ -124,11 +225,15 @@ public static void FireEvent( IntPtr ptrEventName ) Event.Run( eventName ); } + /// + /// Dispatches a console command that came from unmanaged code. + /// + /// The pointer to the struct. [UnmanagedCallersOnly] public static void DispatchCommand( IntPtr infoPtr ) { var info = Marshal.PtrToStructure( infoPtr ); - string? name = Marshal.PtrToStringUTF8( info.name ); + var name = Marshal.PtrToStringUTF8( info.name ); if ( name is null ) return; @@ -144,34 +249,40 @@ public static void DispatchCommand( IntPtr infoPtr ) arguments.Capacity = info.size; for ( int i = 0; i < info.size; i++ ) - { arguments.Add( Marshal.PtrToStringUTF8( stringPtrs[i] ) ?? "" ); - } } ConsoleSystem.Internal.DispatchCommand( name, arguments ); } + /// + /// Fires when a string console variable is changed in unmanaged code. + /// + /// The pointer to the struct. [UnmanagedCallersOnly] public static void DispatchStringCVarCallback( IntPtr infoPtr ) { var info = Marshal.PtrToStructure( infoPtr ); - string? name = Marshal.PtrToStringUTF8( info.name ); + var name = Marshal.PtrToStringUTF8( info.name ); if ( name is null ) return; - string oldValue = Marshal.PtrToStringUTF8( info.oldValue ) ?? ""; - string newValue = Marshal.PtrToStringUTF8( info.newValue ) ?? ""; + var oldValue = Marshal.PtrToStringUTF8( info.oldValue ) ?? ""; + var newValue = Marshal.PtrToStringUTF8( info.newValue ) ?? ""; ConsoleSystem.Internal.DispatchConVarCallback( name, oldValue, newValue ); } + /// + /// Fires when a float console variable is changed in unmanaged code. + /// + /// The pointer to the struct. [UnmanagedCallersOnly] public static void DispatchFloatCVarCallback( IntPtr infoPtr ) { var info = Marshal.PtrToStructure( infoPtr ); - string? name = Marshal.PtrToStringUTF8( info.name ); + var name = Marshal.PtrToStringUTF8( info.name ); if ( name is null ) return; @@ -179,11 +290,15 @@ public static void DispatchFloatCVarCallback( IntPtr infoPtr ) ConsoleSystem.Internal.DispatchConVarCallback( name, info.oldValue, info.newValue ); } + /// + /// Fires when a boolean console variable is changed in unmanaged code. + /// + /// The pointer to the struct. [UnmanagedCallersOnly] public static void DispatchBoolCVarCallback( IntPtr infoPtr ) { var info = Marshal.PtrToStructure( infoPtr ); - string? name = Marshal.PtrToStringUTF8( info.name ); + var name = Marshal.PtrToStringUTF8( info.name ); if ( name is null ) return; @@ -191,6 +306,37 @@ public static void DispatchBoolCVarCallback( IntPtr infoPtr ) ConsoleSystem.Internal.DispatchConVarCallback( name, info.oldValue, info.newValue ); } + /// + /// Fires when a integer console variable is changed in unmanaged code. + /// + /// The pointer to the struct. + [UnmanagedCallersOnly] + public static void DispatchIntCVarCallback( IntPtr infoPtr ) + { + var info = Marshal.PtrToStructure( infoPtr ); + var name = Marshal.PtrToStringUTF8( info.name ); + + if ( name is null ) + return; + + ConsoleSystem.Internal.DispatchConVarCallback( name, info.oldValue, info.newValue ); + } + + /// + /// Fired when a callback has been triggered in unmanaged code. + /// + /// The pointer to the struct. + [UnmanagedCallersOnly] + public static void InvokeCallback( IntPtr infoPtr ) + { + var info = Marshal.PtrToStructure( infoPtr ); + + if ( info.argsSize > 0 ) + CallbackDispatcher.Invoke( info.handle, info.args ); + else + CallbackDispatcher.Invoke( info.handle ); + } + /// /// Invoked when the game project manifest has changed. /// @@ -201,21 +347,90 @@ private static async void OnProjectManifestChanged( object sender, FileSystemEve return; s_timeSinceLastManifestChange = 0; + // Wait for the program editing the file to release it. - await Task.Delay( 10 ); + while ( FileUtil.IsFileInUse( e.FullPath ) ) + await Task.Delay( 1 ); + ReloadProjectManifest( e.FullPath ); } + /// + /// Sets the current realm context of the application. + /// + /// Whether or not the new context is the server. + private static void SetServerContext( bool isServer ) + { + Core.IsServer = isServer; + Core.IsClient = !isServer; + } + /// /// Reloads the game project manifest. /// /// The absolute path to the manifest. - /// The absolute path to the generated csproj file. + /// A task that represents the asynchronous operation. The tasks return value is the absolute path to the generated csproj file. private static string ReloadProjectManifest( string manifestPath ) { s_manifest = ProjectManifest.Load( manifestPath ); + SetGlobals(); + + var csprojPath = Path.Combine( s_manifest.Resources.Code, "code.csproj" ); + var csprojDocument = CSharpProject.FromManifest( s_manifest ).ToXml(); + + // Write csproj to disk. + var stream = File.Open( csprojPath, FileMode.Create ); + var writer = new XmlTextWriter( stream, Encoding.UTF8 ) + { + Formatting = Formatting.Indented, + }; + csprojDocument.WriteContentTo( writer ); + writer.Flush(); + writer.Close(); + + // Write launch settings for csproj to disk. + var propertiesDir = Path.Combine( Path.GetDirectoryName( s_manifest.Resources.Code )!, "Properties" ); + if ( !Directory.Exists( propertiesDir ) ) + Directory.CreateDirectory( propertiesDir ); + + var relativeManifestPath = Path.GetRelativePath( Environment.CurrentDirectory, manifestPath ).Replace( "\\", "\\\\" ); + var launchSettings = LaunchSettingsText + .Replace( "%__CUR_DIR__", Environment.CurrentDirectory ) + .Replace( "%__REL_MANIFEST_PATH__", relativeManifestPath ); + File.WriteAllText( propertiesDir + "\\launchSettings.json", launchSettings ); - var csprojPath = ProjectGenerator.Generate( s_manifest ); return csprojPath; } + + /// + /// Set the values in the global class so that developers can use values like tick rate, etc. in their games. + /// + private static void SetGlobals() + { + Core.TickRate = s_manifest.Properties.TickRate; + } + + /// + /// The JSON to write into each projects launch settings. + /// + private const string LaunchSettingsText = """ + { + "profiles": { + "Mocha": { + "commandName": "Executable", + "executablePath": "%__CUR_DIR__\\build\\Mocha.exe", + "commandLineArgs": "-project %__REL_MANIFEST_PATH__" + "workingDirectory": "%__CUR_DIR__", + "nativeDebugging": true + }, + "Mocha Dedicated Server": { + "commandName": "Executable", + "executablePath": "%__CUR_DIR__\\build\\MochaDedicatedServer.exe", + "commandLineArgs": "-project %__REL_MANIFEST_PATH__" + "workingDirectory": "%__CUR_DIR__", + "nativeDebugging": true + } + } + } + """; } diff --git a/Source/Mocha.Hotload/Mocha.Hotload.csproj b/Source/Mocha.Hotload/Mocha.Hotload.csproj index 9a03603e..37042f09 100644 --- a/Source/Mocha.Hotload/Mocha.Hotload.csproj +++ b/Source/Mocha.Hotload/Mocha.Hotload.csproj @@ -17,24 +17,11 @@ - - runtime - - - runtime - + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + diff --git a/Source/Mocha.Hotload/Project/Assembly/ProjectAssembly.cs b/Source/Mocha.Hotload/Project/Assembly/ProjectAssembly.cs new file mode 100644 index 00000000..2c999757 --- /dev/null +++ b/Source/Mocha.Hotload/Project/Assembly/ProjectAssembly.cs @@ -0,0 +1,382 @@ +using Microsoft.CodeAnalysis; +using Mocha.Common; +using Mocha.Hotload.Compilation; +using Mocha.Hotload.Upgrading; +using Mocha.Hotload.Util; +using System.Reflection; +using System.Runtime.Loader; + +namespace Mocha.Hotload.Projects; + +/// +/// A wrapper for an external assembly to be loaded. +/// +/// The type to retrieve from the assembly as its entry point. +internal sealed class ProjectAssembly where TEntryPoint : IGame +{ + /// + /// The loaded assembly. + /// + internal Assembly? Assembly { get; private set; } = null!; + /// + /// The found entry point into the . + /// + internal TEntryPoint? EntryPoint { get; private set; } = default!; + + /// + /// The information for this specific . + /// + private readonly ProjectAssemblyInfo _projectAssemblyInfo; + /// + /// The that is used for incremental compilation of the project. + /// + private AdhocWorkspace _workspace = null!; + /// + /// The that is responsible for checking when the csproj file changes. + /// + private FileSystemWatcher _projWatcher = null!; + /// + /// The that is responsible for checking when a code file changes. + /// + private FileSystemWatcher _codeWatcher = null!; + /// + /// The load context for the . + /// + private AssemblyLoadContext _loadContext; + + /// + /// The time since the last change happened to the csproj. + /// + private TimeSince _timeSinceCsProjChange; + /// + /// The that represents the current build process. + /// + private Task _buildTask; + /// + /// Whether or not a full build has been requested. + /// + private bool _buildRequested; + /// + /// Whether or not an incremental build has been requested. + /// + private bool _incrementalBuildRequested; + /// + /// A container for all the changed files and the specific change that occurred to them. + /// + private readonly Dictionary _incrementalBuildChanges = new(); + + /// + /// Initializes a new instance of . + /// + /// The information needed to create the . + internal ProjectAssembly( in ProjectAssemblyInfo assemblyInfo ) + { + _projectAssemblyInfo = assemblyInfo; + _loadContext = new AssemblyLoadContext( assemblyInfo.AssemblyName, true ); + + /* + * Mocha.Common must always be "Private" (ie. "Copy Local" as "No" in the reference's properties) + * otherwise **the engine will load Mocha.Common twice incorrectly, causing a mismatch between types**. + * If you ever need to delete and re-add this reference please make sure this is always the case. + * + * If you didn't do that then you'll get an exception around here! + */ + _buildTask = BuildAsync(); + _buildTask.Wait(); + + CreateFileWatchers(); + } + + /// + /// Initializes a new instance of . + /// NOTE: This constructor will not setup any of the hotloading features. Use this for distribution. + /// + /// The assembly + internal ProjectAssembly( Assembly assembly ) + { + Assembly = assembly; + EntryPoint = CreateEntryPointFromAssembly( assembly ); + + _loadContext = null!; + _buildTask = null!; + } + + /// + /// Builds the projects . + /// + /// Whether or not the compilation should be incremental. + /// A that represents the asynchronous operation. + private async Task BuildAsync( bool incremental = false ) + { + // Start stopwatch. + using var _ = new Stopwatch( incremental + ? $"{_projectAssemblyInfo.AssemblyName} incremental build and hotload" + : $"{_projectAssemblyInfo.AssemblyName} build and hotload" ); + + // Notify the build is starting. + var realmString = _projectAssemblyInfo.IsServer ? "Server" : "Client"; + var assemblyName = $"'{_projectAssemblyInfo.AssemblyName}' ({realmString})"; + + Notify.AddNotification( $"Building...", $"Compiling {assemblyName}", FontAwesome.Spinner ); + + // Compile. + CompileResult compileResult; + if ( incremental ) + { + var changedFiles = new Dictionary( _incrementalBuildChanges ); + _incrementalBuildChanges.Clear(); + + compileResult = await Compiler.IncrementalCompileAsync( _workspace, changedFiles, _projectAssemblyInfo ); + } + else + compileResult = await Compiler.CompileAsync( _projectAssemblyInfo ); + + // Check if compile failed. + if ( !compileResult.WasSuccessful ) + { + var errorStr = string.Join( '\n', compileResult.Errors! ); + + foreach ( var error in compileResult.Errors! ) + Log.Error( error ); + + Notify.AddError( $"Build failed", $"Failed to compile {assemblyName}\n{errorStr}", FontAwesome.FaceSadTear ); + // Check if another build is queued before bailing. + await PostBuildAsync(); + return; + } + + // Update compile workspace. + _workspace = compileResult.Workspace!; + + // Swap and upgrade the assemblies. + Swap( compileResult ); + + // Notify the build is finished. + Notify.AddNotification( $"Build successful!", $"Compiled {assemblyName}!", FontAwesome.FaceGrinStars ); + + // Check if another build is queued. + await PostBuildAsync(); + } + + /// + /// Swaps the old with the newly compiled version. + /// + /// The that contains the new assembly bytes. + private void Swap( in CompileResult compileResult ) + { + // Keep old assembly as reference. Should be destroyed once out of scope + var oldAssembly = Assembly; + var oldEntryPoint = EntryPoint; + + // Load new assembly + var assemblyStream = new MemoryStream( compileResult.CompiledAssembly! ); + var symbolsStream = compileResult.HasSymbols ? new MemoryStream( compileResult.CompiledAssemblySymbols! ) : null; + + var newAssembly = _loadContext.LoadFromStream( assemblyStream, symbolsStream ); + var newEntryPoint = CreateEntryPointFromAssembly( newAssembly ); + + // Invoke upgrader to move values from oldAssembly into assembly + if ( oldAssembly is not null && oldEntryPoint is not null ) + { + Upgrader.Upgrade( oldAssembly, newAssembly, oldEntryPoint, newEntryPoint ); + ConsoleSystem.Internal.ClearGameCVars(); + } + + // Now that everything's been upgraded, swap the new interface + // and assembly in + _loadContext.Unload(); + _loadContext = new AssemblyLoadContext( null, isCollectible: true ); + + Assembly = newAssembly; + EntryPoint = newEntryPoint; + + // Re-register the assembly to the console system. + ConsoleSystem.Internal.RegisterAssembly( Assembly, extraFlags: CVarFlags.Game ); + + Event.Run( Event.Game.HotloadAttribute.Name ); + } + + /// + /// Checks if another build needs to be ran after the previous one just finished. + /// + /// A that represents the asynchronous operation. + private async Task PostBuildAsync() + { + if ( !_buildRequested && !_incrementalBuildRequested ) + return; + + // Need a full build. + if ( _buildRequested ) + { + _buildRequested = false; + _incrementalBuildRequested = false; + await BuildAsync(); + } + // Need an incremental build. + else + { + _incrementalBuildRequested = false; + await BuildAsync( incremental: true ); + } + } + + /// + /// Finds and creates a from the project assembly. + /// + /// The to search. + /// The created from the project assembly. + /// Thrown when no valid was found. + private static TEntryPoint CreateEntryPointFromAssembly( Assembly assembly ) + { + var tType = typeof( TEntryPoint ); + // Find first type that derives from interface T + foreach ( var type in assembly.GetTypes() ) + { + if ( type.IsAssignableTo( tType ) ) + return (TEntryPoint)Activator.CreateInstance( type )!; + } + + throw new EntryPointNotFoundException( $"Could not find implementation of '{tType.Name}'" ); + } + + /// + /// Creates the to check for file changes in the project. + /// + private void CreateFileWatchers() + { + _projWatcher = new FileSystemWatcher( + Path.GetDirectoryName( _projectAssemblyInfo.ProjectPath )!, + Path.GetFileName( _projectAssemblyInfo.ProjectPath ) ) + { + NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size + }; + + _projWatcher.Changed += OnCsProjChanged; + _projWatcher.EnableRaisingEvents = true; + + _codeWatcher = new FileSystemWatcher( _projectAssemblyInfo.SourceRoot, "*.cs" ) + { + NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size + }; + + // Visual Studio will create a new temporary file, write to it, + // delete the cs file and then rename the temporary file to + // match the deleted file + // The Renamed event will catch this nicely for us, because renaming + // is the last thing that happens in the order of operations + + // This will typically happen twice, so we'll gate it with a TimeSince too + _codeWatcher.Renamed += OnFileChanged; + _codeWatcher.IncludeSubdirectories = true; + _codeWatcher.EnableRaisingEvents = true; + } + + /// + /// Invoked when the csproj has changed. + /// + private async void OnCsProjChanged( object sender, FileSystemEventArgs e ) + { + // This will typically fire twice, so gate it with a TimeSince. + if ( _timeSinceCsProjChange < 1 ) + return; + + // If a dedicated server and client are running. There's a chance the csproj is already in use. + while ( FileUtil.IsFileInUse( e.FullPath ) ) + await Task.Delay( 1 ); + + // Bail if the csproj actually didn't change. + var oldProject = CSharpProject.FromFile( e.FullPath ); + CSharpProject.RemoveCachedProject( e.FullPath ); + var newProject = CSharpProject.FromFile( e.FullPath ); + if ( oldProject == newProject ) + return; + + _timeSinceCsProjChange = 0; + + if ( _buildTask.IsCompleted ) + _buildTask = BuildAsync(); + else + _buildRequested = true; + } + + /// + /// Invoked when a file system change has occurred. + /// + private void OnFileChanged( object sender, FileSystemEventArgs e ) + { + // This might be a directory, if it is then skip. + if ( Directory.Exists( e.FullPath ) ) + return; + + switch ( e.ChangeType ) + { + // A C# file was created. + case WatcherChangeTypes.Created: + { + // If a change already exists and is not the file being created then switch to changed. + if ( _incrementalBuildChanges.TryGetValue( e.FullPath, out var val ) && val != WatcherChangeTypes.Created ) + _incrementalBuildChanges[e.FullPath] = WatcherChangeTypes.Changed; + // Add created event if it does not exist in the changes. + else if ( !_incrementalBuildChanges.ContainsKey( e.FullPath ) ) + _incrementalBuildChanges.Add( e.FullPath, WatcherChangeTypes.Created ); + + break; + } + // A C# file was deleted. + case WatcherChangeTypes.Deleted: + { + if ( _incrementalBuildChanges.TryGetValue( e.FullPath, out var val ) ) + { + // If the change that currently exists is it being created then just remove the change. + if ( val == WatcherChangeTypes.Created ) + _incrementalBuildChanges.Remove( e.FullPath ); + // Overwrite any previous change and set it to be deleted. + else + _incrementalBuildChanges[e.FullPath] = WatcherChangeTypes.Deleted; + } + else if ( !_incrementalBuildChanges.ContainsKey( e.FullPath ) ) + _incrementalBuildChanges.Add( e.FullPath, WatcherChangeTypes.Deleted ); + + break; + } + // A C# file was changed/renamed. + case WatcherChangeTypes.Changed: + case WatcherChangeTypes.Renamed: + { + if ( _incrementalBuildChanges.TryGetValue( e.FullPath, out var val ) ) + { + // If the file was previously created then keep that. + if ( val == WatcherChangeTypes.Created ) + break; + // Overwrite any other state with changed. + else + _incrementalBuildChanges[e.FullPath] = WatcherChangeTypes.Changed; + } + else + _incrementalBuildChanges.Add( e.FullPath, WatcherChangeTypes.Changed ); + + break; + } + } + + // Queue build. + if ( _buildTask.IsCompleted ) + _buildTask = BuildAsync( incremental: true ); + else + _incrementalBuildRequested = true; + } +} diff --git a/Source/Mocha.Hotload/Assembly/ProjectAssemblyInfo.cs b/Source/Mocha.Hotload/Project/Assembly/ProjectAssemblyInfo.cs similarity index 69% rename from Source/Mocha.Hotload/Assembly/ProjectAssemblyInfo.cs rename to Source/Mocha.Hotload/Project/Assembly/ProjectAssemblyInfo.cs index b46aed65..f1ccef2b 100644 --- a/Source/Mocha.Hotload/Assembly/ProjectAssemblyInfo.cs +++ b/Source/Mocha.Hotload/Project/Assembly/ProjectAssemblyInfo.cs @@ -1,4 +1,4 @@ -namespace Mocha.Hotload; +namespace Mocha.Hotload.Projects; /// /// Represents the required information for a project assembly. @@ -19,4 +19,10 @@ internal readonly struct ProjectAssemblyInfo /// The relative path to the projects csproj file. /// internal string ProjectPath { get; init; } + + /// + /// Are we building this assembly for the server? + /// If not, we can safely assume that it's for the client. + /// + internal bool IsServer { get; init; } } diff --git a/Source/Mocha.Hotload/Project/CSharpProject.cs b/Source/Mocha.Hotload/Project/CSharpProject.cs new file mode 100644 index 00000000..77070196 --- /dev/null +++ b/Source/Mocha.Hotload/Project/CSharpProject.cs @@ -0,0 +1,667 @@ +using Mocha.Hotload.Compilation; +using Mocha.Hotload.Util; +using NuGet.Versioning; +using System.Collections.Immutable; +using System.Xml; + +namespace Mocha.Hotload.Projects; + +/// +/// A representation of a C# project file (.csproj). +/// +internal sealed class CSharpProject : IEquatable +{ + /// + /// A cache containing s that have been loaded from a file. + /// + private static readonly Dictionary s_fileCache = new(); + + /// + /// Type of output to generate (WinExe, Exe, or Library). + /// + internal string OutputType { get; } = "Library"; + /// + /// Framework that this project targets. Must be a Target Framework Moniker (e.g. netcoreapp1.0). + /// + internal string TargetFramework { get; } = CompilerHelper.GetCSharpProjectMoniker(); + /// + /// Whether or not to enable implicit global usings for the C# project. + /// + internal bool ImplicitUsings { get; } = false; + /// + /// Whether or not to enable nullable annotations and warnings context for the C# project. + /// + internal bool Nullable { get; } = false; + /// + /// Whether or not to allow unsafe code in the project. + /// + internal bool AllowUnsafeBlocks { get; } = false; + /// + /// The version of the C# language to use. + /// + internal string LangVersion { get; } = "latest"; + /// + /// An array of all platforms that the C# project supports. + /// + internal ImmutableArray Platforms { get; } = ImmutableArray.Empty; + /// + /// The name of the output C# assembly. + /// + internal string AssemblyName { get; } = "MochaAssembly"; + /// + /// The default namespace of the C# project. + /// + internal string RootNamespace { get; } = "Mocha"; + /// + /// All of the pre-processor symbols defined in the project. + /// + internal ImmutableArray PreProcessorSymbols { get; } = ImmutableArray.Empty; + + /// + /// A set of entries for global using statements. + /// + internal ImmutableDictionary Usings { get; } = ImmutableDictionary.Empty; + /// + /// An array of all the C# files that are in the project. + /// + internal ImmutableArray CSharpFiles { get; } = ImmutableArray.Empty; + /// + /// A set of entries for NuGet packages. + /// + internal ImmutableDictionary PackageReferences { get; } = ImmutableDictionary.Empty; + /// + /// An array of all the C# project references that this project depends on. + /// + internal ImmutableArray ProjectReferences { get; } = ImmutableArray.Empty; + /// + /// An array of all the DLLs that this project explicitly includes. + /// + internal ImmutableArray DllReferences { get; } = ImmutableArray.Empty; + + /// + /// An array of pre-processor symbols defined in a . + /// + private ImmutableArray ProjectPreProcessorSymbols { get; set; } = ImmutableArray.Empty; + /// + /// Contains Xml meta data from . + /// + private readonly ImmutableDictionary packageReferenceMetaData = ImmutableDictionary.Empty; + /// + /// Contains Xml meta data from . + /// + private readonly ImmutableDictionary projectReferenceMetaData = ImmutableDictionary.Empty; + + /// + /// Initializes a new instance of . This instance is constructed from a csproj file on disk. + /// + /// The file path to the csproj file. + private CSharpProject( string filePath ) + { + // Setup. + using var reader = XmlReader.Create( filePath ); + + var usings = ImmutableDictionary.CreateBuilder(); + var cSharpFiles = ImmutableArray.CreateBuilder(); + var packageReferences = ImmutableDictionary.CreateBuilder(); + var projectReferences = ImmutableArray.CreateBuilder(); + var dllReferences = ImmutableArray.CreateBuilder(); + + var packageReferenceMetaDataBuilder = ImmutableDictionary.CreateBuilder(); + var projectReferenceMetaDataBuilder = ImmutableDictionary.CreateBuilder(); + + var currentMetaDataEntry = string.Empty; + var isMetaDataPackage = false; + + var shouldTraverse = true; + + // Walk over every Xml element. + while ( reader.Read() ) + { + if ( reader.NodeType != XmlNodeType.Element ) + continue; + + if ( !shouldTraverse && reader.Name != "ItemGroup" && reader.Name != "PropertyGroup" ) + continue; + + // Process specific element types. + switch ( reader.Name ) + { + case "ItemGroup": + case "PropertyGroup": + var condition = reader.GetAttribute( "Condition" ); + if ( condition is null ) + { + shouldTraverse = true; + break; + } + + shouldTraverse = AnalyzeCondition( condition ); + break; + case "OutputType": + OutputType = reader.ReadElementContentAsString(); + break; + case "TargetFramework": + TargetFramework = reader.ReadElementContentAsString(); + break; + case "ImplicitUsings": + ImplicitUsings = reader.ReadElementContentAsString() == "enable"; + break; + case "AllowUnsafeBlocks": + AllowUnsafeBlocks = reader.ReadElementContentAsString() == "True"; + break; + case "LangVersion": + LangVersion = reader.ReadElementContentAsString(); + break; + case "Platforms": + var platforms = ImmutableArray.CreateBuilder(); + platforms.AddRange( reader.ReadElementContentAsString().Split( ';' ) ); + Platforms = platforms.ToImmutableArray(); + break; + case "Nullable": + Nullable = reader.ReadElementContentAsString() == "enable"; + break; + case "AssemblyName": + AssemblyName = reader.ReadElementContentAsString() + .Replace( "$(MSBuildProjectName)", Path.GetFileNameWithoutExtension( filePath ) ); + break; + case "RootNamespace": + RootNamespace = reader.ReadElementContentAsString() + .Replace( "$(MSBuildProjectName)", Path.GetFileNameWithoutExtension( filePath ) ); + break; + case "Using": + usings.Add( reader.GetAttribute( "Include" )!, reader.GetAttribute( "Static" ) == "true" ); + break; + // Package references can have extra meta data so setup for that. + case "PackageReference": + var packageName = reader.GetAttribute( "Include" )!; + currentMetaDataEntry = packageName; + isMetaDataPackage = true; + + packageReferences.Add( packageName, new NuGetVersion( reader.GetAttribute( "Version" ) ) ); + break; + // Project references can have extra meta data so setup for that. + case "ProjectReference": + var projectPath = reader.GetAttribute( "Include" )!; + currentMetaDataEntry = projectPath; + isMetaDataPackage = false; + + projectReferences.Add( projectPath ); + break; + case "Reference": + dllReferences.Add( reader.GetAttribute( "Include" )! ); + break; + case "DefineConstants": + var preProcessorSymbols = ImmutableArray.CreateBuilder(); + preProcessorSymbols.AddRange( reader.ReadElementContentAsString().Split( ';' ) ); + PreProcessorSymbols = preProcessorSymbols.ToImmutableArray(); + break; + case "IncludeAssets": + var includeAssets = reader.ReadElementContentAsString(); + if ( isMetaDataPackage ) + { + if ( !packageReferenceMetaDataBuilder.TryAdd( currentMetaDataEntry, (includeAssets, null) ) ) + packageReferenceMetaDataBuilder[currentMetaDataEntry] = (includeAssets, + packageReferenceMetaDataBuilder[currentMetaDataEntry].Item2); + } + else + { + if ( !projectReferenceMetaDataBuilder.TryAdd( currentMetaDataEntry, (includeAssets, null, null) ) ) + projectReferenceMetaDataBuilder[currentMetaDataEntry] = (includeAssets, + projectReferenceMetaDataBuilder[currentMetaDataEntry].Item2, + projectReferenceMetaDataBuilder[currentMetaDataEntry].Item3); + } + break; + case "PrivateAssets": + var privateAssets = reader.ReadElementContentAsString(); + if ( isMetaDataPackage ) + { + if ( !packageReferenceMetaDataBuilder.TryAdd( currentMetaDataEntry, (null, privateAssets) ) ) + packageReferenceMetaDataBuilder[currentMetaDataEntry] = (packageReferenceMetaDataBuilder[currentMetaDataEntry].Item1, + privateAssets); + } + else + { + if ( !projectReferenceMetaDataBuilder.TryAdd( currentMetaDataEntry, (null, privateAssets, null) ) ) + projectReferenceMetaDataBuilder[currentMetaDataEntry] = (projectReferenceMetaDataBuilder[currentMetaDataEntry].Item1, + privateAssets, + projectReferenceMetaDataBuilder[currentMetaDataEntry].Item3); + } + break; + case "ReferenceOutputAssembly": + var referenceOutputAssembly = reader.ReadElementContentAsBoolean(); + + if ( !projectReferenceMetaDataBuilder.TryAdd( currentMetaDataEntry, (null, null, referenceOutputAssembly) ) ) + projectReferenceMetaDataBuilder[currentMetaDataEntry] = (projectReferenceMetaDataBuilder[currentMetaDataEntry].Item1, + projectReferenceMetaDataBuilder[currentMetaDataEntry].Item2, + referenceOutputAssembly); + break; + } + } + + // Get all C# files in the project. + foreach ( var file in Directory.EnumerateFiles( Path.GetDirectoryName( filePath )!, "*.cs", SearchOption.AllDirectories ) ) + { + // TODO: Filter out any directories that may have cs files we don't want. + if ( file.Contains( "\\obj\\" ) ) + { + if ( !ImplicitUsings || Path.GetFileName( file ) != "code.GlobalUsings.g.cs" ) + continue; + } + + cSharpFiles.Add( file ); + } + + // Assign. + Usings = usings.ToImmutable(); + CSharpFiles = cSharpFiles.ToImmutable(); + PackageReferences = packageReferences.ToImmutable(); + ProjectReferences = projectReferences.ToImmutable(); + DllReferences = dllReferences.ToImmutable(); + + // Add to cache. + if ( s_fileCache.ContainsKey( filePath ) ) + s_fileCache[filePath] = this; + else + s_fileCache.Add( filePath, this ); + } + + /// + /// Initializes a new instance of . This instance is constructed from a . + /// + /// The manifest to use for construction. + private CSharpProject( in ProjectManifest manifest ) + { + var project = manifest.Project; + + // Basic configuration. + ImplicitUsings = project.ImplicitUsings; + AllowUnsafeBlocks = project.AllowUnsafeBlocks; + Platforms = ImmutableArray.Create( Environment.Is64BitProcess ? "x64" : "x86" ); + Nullable = project.Nullable; + AssemblyName = manifest.Name; + RootNamespace = project.DefaultNamespace ?? "Mocha"; + if ( project.PreProcessorSymbols is not null ) + ProjectPreProcessorSymbols = project.PreProcessorSymbols.ToImmutableArray(); + + // Usings. + { + var usingsBuilder = ImmutableDictionary.CreateBuilder(); + + if ( project.UseMochaGlobal ?? true ) + usingsBuilder.Add( "Mocha.Common.Global", true ); + + if ( project.Usings is not null ) + { + foreach ( var usingEntry in project.Usings ) + usingsBuilder.Add( usingEntry.Namespace, usingEntry.Static ); + } + + Usings = usingsBuilder.ToImmutable(); + } + + // Package references. + { + var packageReferencesBuilder = ImmutableDictionary.CreateBuilder(); + var packageReferenceMetaDataBuilder = ImmutableDictionary.CreateBuilder(); + + if ( project.PackageReferences is not null ) + { + foreach ( var packageReference in project.PackageReferences ) + { + packageReferencesBuilder.Add( packageReference.Name, new NuGetVersion( packageReference.Version ) ); + packageReferenceMetaDataBuilder.Add( packageReference.Name, (packageReference.IncludeAssets, packageReference.PrivateAssets) ); + } + } + + if ( packageReferencesBuilder.Count > 0 ) + { + PackageReferences = packageReferencesBuilder.ToImmutable(); + packageReferenceMetaData = packageReferenceMetaDataBuilder.ToImmutable(); + } + } + + // Project references. + { + var projectReferencesBuilder = ImmutableArray.CreateBuilder(); + var projectReferenceMetaDataBuilder = ImmutableDictionary.CreateBuilder(); + + if ( project.ProjectReferences is not null ) + { + foreach ( var projectReference in project.ProjectReferences ) + { + projectReferencesBuilder.Add( projectReference.Path ); + projectReferenceMetaDataBuilder.Add( projectReference.Path, + (projectReference.OutputItemType, + projectReference.PrivateAssets, + projectReference.ReferenceOutputAssembly) ); + } + } + + if ( projectReferencesBuilder.Count > 0 ) + { + ProjectReferences = projectReferencesBuilder.ToImmutable(); + projectReferenceMetaData = projectReferenceMetaDataBuilder.ToImmutable(); + } + } + + // DLL references. + { + var dllReferencesBuilder = ImmutableArray.CreateBuilder(); + + if ( project.References is not null ) + { + foreach ( var reference in project.References ) + dllReferencesBuilder.Add( reference ); + } + + if ( dllReferencesBuilder.Count > 0 ) + DllReferences = dllReferencesBuilder.ToImmutable(); + } + } + + /// + /// Converts the to its Xml representation. + /// + /// The that represents the . + internal XmlDocument ToXml() + { + // Setup. + var baseReferenceDir = Path.GetFullPath( "build\\" ); + var document = new XmlDocument(); + var rootElement = document.CreateElement( "Project" ); + rootElement.SetAttribute( "Sdk", "Microsoft.NET.Sdk" ); + document.AppendChild( rootElement ); + + // Basic configuration. + { + var basics = rootElement.CreateElement( "PropertyGroup" ); + basics.CreateElementWithInnerText( "OutputType", OutputType ); + basics.CreateElementWithInnerText( "TargetFramework", TargetFramework ); + basics.CreateElementWithInnerText( "ImplicitUsings", ImplicitUsings ? "enable" : "disable" ); + basics.CreateElementWithInnerText( "AllowUnsafeBlocks", AllowUnsafeBlocks ? "True" : "False" ); + basics.CreateElementWithInnerText( "LangVersion", LangVersion ); + basics.CreateElementWithInnerText( "Platforms", string.Join( ';', Platforms ) ); + basics.CreateElementWithInnerText( "Nullable", Nullable ? "enable" : "disable" ); + basics.CreateElementWithInnerText( "AssemblyName", AssemblyName ); + basics.CreateElementWithInnerText( "RootNamespace", RootNamespace ); + basics.CreateElementWithInnerText( "Configurations", "DebugClient;DebugServer;ReleaseClient;ReleaseServer" ); + } + + // Constant definitions. + { + const string debugDefinitions = "DEBUG;TRACE;"; + const string clientDefinitions = "MOCHA;CLIENT;"; + const string serverDefinitions = "MOCHA;SERVER;"; + var customDefinitions = string.Join( ';', ProjectPreProcessorSymbols ); + + + // Mocha client. + rootElement.CreateElementWithAttributes( "PropertyGroup", "Condition", "'$(Configuration)'=='DebugClient'" ) + .CreateElementWithInnerText( "DefineConstants", (debugDefinitions + clientDefinitions + customDefinitions).Trim( ';' ) ); + + rootElement.CreateElementWithAttributes( "PropertyGroup", "Condition", "'$(Configuration)'=='ReleaseClient'" ) + .CreateElementWithInnerText( "DefineConstants", (clientDefinitions + customDefinitions).Trim( ';' ) ); + + // Mocha dedicated server. + rootElement.CreateElementWithAttributes( "PropertyGroup", "Condition", "'$(Configuration)'=='DebugServer'" ) + .CreateElementWithInnerText( "DefineConstants", (debugDefinitions + serverDefinitions + customDefinitions).Trim( ';' ) ); + + rootElement.CreateElementWithAttributes( "PropertyGroup", "Condition", "'$(Configuration)'=='ReleaseServer'" ) + .CreateElementWithInnerText( "DefineConstants", (serverDefinitions + customDefinitions).Trim( ';' ) ); + } + + // Implicit usings. + if ( Usings.Count > 0 ) + { + var usings = rootElement.CreateElement( "ItemGroup" ); + foreach ( var usingDef in Usings ) + usings.CreateElementWithAttributes( "Using", "Include", usingDef.Key, "Static", usingDef.Value ? "true" : "false" ); + } + + // Cleanup entries. + { + var cleanup = rootElement.CreateElement( "ItemGroup" ); + cleanup.CreateElementWithAttributes( "Compile", "Remove", "bin\\**" ); + cleanup.CreateElementWithAttributes( "EmbeddedResource", "Remove", "bin\\**" ); + cleanup.CreateElementWithAttributes( "None", "Remove", "bin\\**" ); + } + + // Mocha references. + { + var references = rootElement.CreateElement( "ItemGroup" ); + references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.Common.dll" ); + references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.Engine.dll" ); + references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + "Mocha.UI.dll" ); + } + + // NuGet package references. + if ( PackageReferences.Count > 0 ) + { + var packageReferences = rootElement.CreateElement( "ItemGroup" ); + foreach ( var packageReference in PackageReferences ) + { + var element = packageReferences.CreateElementWithAttributes( "PackageReference", + "Include", packageReference.Key, + "Version", packageReference.Value.Version.ToString() ); + + var metaData = packageReferenceMetaData[packageReference.Key]; + if ( metaData.Item1 is not null ) + element.CreateElementWithInnerText( "IncludeAssets", metaData.Item1 ); + if ( metaData.Item2 is not null ) + element.CreateElementWithInnerText( "PrivateAssets", metaData.Item2 ); + } + } + + // Project references. + if ( ProjectReferences.Length > 0 ) + { + var projectReferences = rootElement.CreateElement( "ItemGroup" ); + foreach ( var projectReference in ProjectReferences ) + { + var element = projectReferences.CreateElementWithAttributes( "ProjectReference", "Include", baseReferenceDir + projectReference ); + + var metaData = projectReferenceMetaData[projectReference]; + if ( metaData.Item1 is not null ) + element.CreateElementWithInnerText( "IncludeAssets", metaData.Item1 ); + if ( metaData.Item2 is not null ) + element.CreateElementWithInnerText( "PrivateAssets", metaData.Item2 ); + if ( metaData.Item3 is not null ) + element.CreateElementWithInnerText( "ReferenceOutputAssembly", metaData.Item3.Value ? "true" : "false" ); + } + } + + // Literal DLL references. + if ( DllReferences.Length > 0 ) + { + var references = rootElement.CreateElement( "ItemGroup" ); + foreach ( var reference in DllReferences ) + references.CreateElementWithAttributes( "Reference", "Include", baseReferenceDir + reference ); + } + + return document; + } + + /// + public override bool Equals( object? obj ) => Equals( obj as CSharpProject ); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + OutputType, + TargetFramework, + ImplicitUsings, + Nullable, + AllowUnsafeBlocks, + LangVersion, + Platforms, + AssemblyName ) + + + HashCode.Combine( + RootNamespace, + Usings, + CSharpFiles, + PackageReferences, + ProjectReferences, + DllReferences, + packageReferenceMetaData, + projectReferenceMetaData ); + } + + /// + public bool Equals( CSharpProject? other ) + { + if ( other is null ) + return false; + + if ( ReferenceEquals( this, other ) ) + return true; + + // Check basic configuration. + if ( OutputType != other.OutputType || + TargetFramework != other.TargetFramework || + ImplicitUsings != other.ImplicitUsings || + Nullable != other.Nullable || + AllowUnsafeBlocks != other.AllowUnsafeBlocks || + LangVersion != other.LangVersion || + !Platforms.SequenceEqual( other.Platforms ) || + AssemblyName != other.AssemblyName || + RootNamespace != other.RootNamespace ) + return false; + + // Check usings. + if ( Usings.Count != other.Usings.Count ) + return false; + + foreach ( var usingEntry in Usings ) + { + if ( !other.Usings.TryGetValue( usingEntry.Key, out var value ) ) + return false; + + if ( usingEntry.Value != value ) + return false; + } + + // Check NuGet package references. + if ( PackageReferences.Count != other.PackageReferences.Count ) + return false; + + foreach ( var packageReference in PackageReferences ) + { + if ( !other.PackageReferences.TryGetValue( packageReference.Key, out var value ) ) + return false; + + if ( packageReference.Value != value ) + return false; + } + + // Check project references. + if ( ProjectReferences.Length != other.ProjectReferences.Length ) + return false; + + foreach ( var projectReference in ProjectReferences ) + { + if ( !other.ProjectReferences.Contains( projectReference ) ) + return false; + } + + // Check DLL references. + if ( DllReferences.Length != other.DllReferences.Length ) + return false; + + foreach ( var dllReference in DllReferences ) + { + if ( !other.DllReferences.Contains( dllReference ) ) + return false; + } + + // Check NuGet package reference meta data. + if ( packageReferenceMetaData.Count != other.packageReferenceMetaData.Count ) + return false; + + foreach ( var packageReferenceMeta in packageReferenceMetaData ) + { + if ( !other.packageReferenceMetaData.TryGetValue( packageReferenceMeta.Key, out var tuple ) ) + return false; + + if ( packageReferenceMeta.Value.Item1 != tuple.Item1 || + packageReferenceMeta.Value.Item2 != tuple.Item2 ) + return false; + } + + // Check project reference meta data. + if ( projectReferenceMetaData.Count != other.projectReferenceMetaData.Count ) + return false; + + foreach ( var projectReferenceMeta in projectReferenceMetaData ) + { + if ( !other.projectReferenceMetaData.TryGetValue( projectReferenceMeta.Key, out var tuple ) ) + return false; + + if ( projectReferenceMeta.Value.Item1 != tuple.Item1 || + projectReferenceMeta.Value.Item2 != tuple.Item2 || + projectReferenceMeta.Value.Item3 != tuple.Item3 ) + return false; + } + + return true; + } + + /// + /// Returns whether or not the condition provided is true. + /// + /// The condition to parse. + /// Whether or not the condition provided is true. + private static bool AnalyzeCondition( string condition ) + { + // TODO: Do we need to flesh this out more? + condition = condition.Replace( "$(Configuration)", CompilerHelper.Build + CompilerHelper.Realm ).Replace( "'", "" ); + var operands = condition.Split( "==" ); + + return operands[0] == operands[1]; + } + + /// + /// Parses a csproj file into a to be returned. If the csproj was previously parsed then that cached version will be returned. + /// + /// The file path to the csproj file. + /// The parsed . If previously parsed, the cached version is returned. + internal static CSharpProject FromFile( string filePath ) + { + if ( s_fileCache.TryGetValue( filePath, out var csproj ) ) + return csproj; + + return new( filePath ); + } + + /// + /// Parses a into a . + /// + /// The to construct a from. + /// The parsed . + internal static CSharpProject FromManifest( in ProjectManifest manifest ) + { + return new( manifest ); + } + + /// + /// Removes a previously cached . + /// + /// The file path that was used to cache the . + /// Whether or not an entry was removed. + internal static bool RemoveCachedProject( string filePath ) + { + return s_fileCache.Remove( filePath ); + } + + /// + /// Clears the file cache. + /// + internal static void ClearCachedProjects() + { + s_fileCache.Clear(); + } + + public static bool operator ==( CSharpProject first, CSharpProject second ) => first.Equals( second ); + public static bool operator !=( CSharpProject first, CSharpProject second ) => !first.Equals( second ); +} diff --git a/Source/Mocha.Hotload/Project/ProjectManifest.Load.cs b/Source/Mocha.Hotload/Project/ProjectManifest.Load.cs index 514d7eee..787554f1 100644 --- a/Source/Mocha.Hotload/Project/ProjectManifest.Load.cs +++ b/Source/Mocha.Hotload/Project/ProjectManifest.Load.cs @@ -1,11 +1,11 @@ using System.Text.Json; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Projects; partial struct ProjectManifest { /// - /// Converts a relative path in the project manifest to an absolute path. + /// Converts a relative path in the to an absolute path. /// /// The relative path. /// The path to the directory that contained the manifest. @@ -16,10 +16,10 @@ private static string GetAbsolutePath( string path, string baseDir ) } /// - /// Loads a project manifest. + /// Loads a from a file on disk. /// /// The absolute path to the manifest file. - /// The constructed project manfifest. + /// The constructed . /// Thrown when no file exists at the given path. internal static ProjectManifest Load( string path ) { diff --git a/Source/Mocha.Hotload/Project/ProjectManifest.cs b/Source/Mocha.Hotload/Project/ProjectManifest.cs index e5dff169..0f0c8a30 100644 --- a/Source/Mocha.Hotload/Project/ProjectManifest.cs +++ b/Source/Mocha.Hotload/Project/ProjectManifest.cs @@ -1,117 +1,237 @@ using System.Text.Json.Serialization; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Projects; +/// +/// Represents a manifest containing all the required information for a game project. +/// internal partial struct ProjectManifest { + /// + /// The name of the project. + /// [JsonPropertyName( "name" )] - public string Name { get; set; } + public string Name { get; init; } + /// + /// The author of the project. + /// [JsonPropertyName( "author" )] public string Author { get; set; } + /// + /// The version of the project. + /// [JsonPropertyName( "version" )] public string Version { get; set; } + /// + /// A description of the project. + /// [JsonPropertyName( "description" )] public string Description { get; set; } + /// + /// Contains the path information of the project. + /// [JsonPropertyName( "resources" )] public ResourceInfo Resources { get; set; } + /// + /// Contains the game properties of the project. + /// [JsonPropertyName( "properties" )] public Properties Properties { get; set; } + /// + /// Contains the C# project settings. + /// [JsonPropertyName( "project" )] public ProjectInfo Project { get; set; } } +/// +/// Represents the path information in a . +/// internal struct ResourceInfo { + /// + /// The relative path to the C# code directory. + /// [JsonPropertyName( "code" )] public string Code { get; set; } + /// + /// The relative path to the asset content directory. + /// [JsonPropertyName( "content" )] public string Content { get; set; } } +/// +/// Represents the game properties in a . +/// internal struct Properties { + /// + /// The tick rate the game should run at. + /// [JsonPropertyName( "tickRate" )] public int TickRate { get; set; } } +/// +/// Represents the C# project settings in a . +/// internal struct ProjectInfo { + /// + /// The version of the C# language to use. + /// [JsonPropertyName( "languageVersion" )] public string? LanguageVersion { get; set; } + /// + /// The default namespace in the C# project. + /// [JsonPropertyName( "defaultNamespace" )] public string? DefaultNamespace { get; set; } + /// + /// Whether or not to enable nullable annotations. + /// [JsonPropertyName( "nullable" )] public bool Nullable { get; set; } + /// + /// Whether or not to enable implicit global usings. + /// [JsonPropertyName( "implicitUsings" )] public bool ImplicitUsings { get; set; } + /// + /// Whether or not to enable the use of unsafe code blocks. + /// [JsonPropertyName( "allowUnsafeBlocks" )] public bool AllowUnsafeBlocks { get; set; } + /// + /// Whether or not to use the class globally in the C# project. + /// [JsonPropertyName( "useMochaGlobal" )] public bool? UseMochaGlobal { get; set; } + /// + /// Contains any custom pre-processor symbols to include in the compilation. + /// + [JsonPropertyName( "preProcessorSymbols" )] + public string[]? PreProcessorSymbols { get; set; } + + /// + /// Contains any custom global using entries. + /// [JsonPropertyName( "usings" )] public Using[]? Usings { get; set; } + /// + /// Contains any NuGet package references. + /// [JsonPropertyName( "packageReferences" )] public PackageReference[]? PackageReferences { get; set; } + /// + /// Contains any C# project references. + /// [JsonPropertyName( "projectReferences" )] public ProjectReference[]? ProjectReferences { get; set; } + /// + /// Contains any literal DLL references. + /// [JsonPropertyName( "references" )] public string[]? References { get; set; } + /// + /// Any C# project items to append to the file after serialization. + /// [JsonPropertyName( "rawEntry" )] public string? RawEntry { get; set; } } +/// +/// Represents a using statement in a C# project. +/// internal struct Using { + /// + /// The fully qualified namespace (and class if applicable) to use. + /// [JsonPropertyName( "namespace" )] public string Namespace { get; set; } + /// + /// Whether or not to use the namespace statically. + /// [JsonPropertyName( "static" )] public bool Static { get; set; } } +/// +/// Represents a NuGet package reference in a C# project. +/// internal struct PackageReference { + /// + /// The name of the NuGet package. + /// [JsonPropertyName( "name" )] public string Name { get; set; } + /// + /// The version of the NuGet package. + /// [JsonPropertyName( "version" )] public string Version { get; set; } + /// + /// Defines the "PrivateAssets" option in the C# project. + /// [JsonPropertyName( "privateAssets" )] public string? PrivateAssets { get; set; } + /// + /// Defines the "IncludeAssets" option in the C# project. + /// [JsonPropertyName( "includeAssets" )] public string? IncludeAssets { get; set; } } +/// +/// Represents a C# project reference in a C# project. +/// internal struct ProjectReference { + /// + /// The path to the csproj file. + /// [JsonPropertyName( "path" )] public string Path { get; set; } + /// + /// Defines the "PrivateAssets" option in the C# project. + /// [JsonPropertyName( "privateAssets" )] public string? PrivateAssets { get; set; } - [JsonPropertyName( "referenceOutputAssembly" )] - public bool? ReferenceOutputAssembly { get; set; } - + /// + /// Defines the "OutputItemType" option in the C# project. + /// [JsonPropertyName( "outputItemType" )] public string? OutputItemType { get; set; } + + /// + /// Defines the "ReferenceOutputAssembly" option in the C# project. + /// + [JsonPropertyName( "referenceOutputAssembly" )] + public bool? ReferenceOutputAssembly { get; set; } } diff --git a/Source/Mocha.Hotload/Upgraders/ArrayUpgrader.cs b/Source/Mocha.Hotload/Upgraders/ArrayUpgrader.cs index 1c637893..cc0e3c39 100644 --- a/Source/Mocha.Hotload/Upgraders/ArrayUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/ArrayUpgrader.cs @@ -1,9 +1,9 @@ using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// -/// A member upgrader for arrays. +/// A member upgrader for s. /// internal sealed class ArrayUpgrader : IMemberUpgrader { @@ -19,7 +19,7 @@ internal sealed class ArrayUpgrader : IMemberUpgrader }; /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/ClassUpgrader.cs b/Source/Mocha.Hotload/Upgraders/ClassUpgrader.cs index f0456e4c..78d04bd1 100644 --- a/Source/Mocha.Hotload/Upgraders/ClassUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/ClassUpgrader.cs @@ -1,7 +1,8 @@ -using System.Reflection; +using Mocha.Hotload.Util; +using System.Reflection; using System.Runtime.Serialization; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// /// A member upgrader for any classes and interfaces. @@ -18,7 +19,7 @@ public bool CanUpgrade( MemberInfo memberInfo ) } /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/CollectionUpgrader.cs b/Source/Mocha.Hotload/Upgraders/CollectionUpgrader.cs index 2e2bcfe5..7995d982 100644 --- a/Source/Mocha.Hotload/Upgraders/CollectionUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/CollectionUpgrader.cs @@ -1,11 +1,12 @@ -using System.Collections; +using Mocha.Hotload.Util; +using System.Collections; using System.Reflection; using System.Runtime.Serialization; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// -/// A member upgrader for any collections. +/// A member upgrader for any s. /// internal sealed class CollectionUpgrader : IMemberUpgrader { @@ -19,7 +20,7 @@ public bool CanUpgrade( MemberInfo memberInfo ) } /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/IMemberUpgrader.cs b/Source/Mocha.Hotload/Upgraders/IMemberUpgrader.cs index a639a05e..16ce55e4 100644 --- a/Source/Mocha.Hotload/Upgraders/IMemberUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/IMemberUpgrader.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading; /// /// A contract defining an object that can upgrade members from two different assemblies. @@ -13,13 +13,13 @@ internal interface IMemberUpgrader int Priority { get; } /// - /// Does this upgrader support upgrading for this member? + /// Returns whether or not the upgrader supports upgrading the member. /// + /// Whether or not the upgrader supports upgrading the member. bool CanUpgrade( MemberInfo memberInfo ); /// - /// Performs the upgrade for this member: copies the contents of - /// oldInstance into newInstance. + /// Performs the upgrade for this member. Copies the contents of into . /// - void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ); + void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ); } diff --git a/Source/Mocha.Hotload/Upgraders/PrimitiveUpgrader.cs b/Source/Mocha.Hotload/Upgraders/PrimitiveUpgrader.cs index f27a58d8..c68b3a4d 100644 --- a/Source/Mocha.Hotload/Upgraders/PrimitiveUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/PrimitiveUpgrader.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// /// A member upgrader for primitives. @@ -19,7 +19,7 @@ internal sealed class PrimitiveUpgrader : IMemberUpgrader }; /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/StringUpgrader.cs b/Source/Mocha.Hotload/Upgraders/StringUpgrader.cs index 1eac8684..00fb4fe9 100644 --- a/Source/Mocha.Hotload/Upgraders/StringUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/StringUpgrader.cs @@ -1,9 +1,9 @@ using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// -/// A member upgrader for strings. +/// A member upgrader for s. /// internal sealed class StringUpgrader : IMemberUpgrader { @@ -19,7 +19,7 @@ internal sealed class StringUpgrader : IMemberUpgrader }; /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/StructUpgrader.cs b/Source/Mocha.Hotload/Upgraders/StructUpgrader.cs index 13b29ab2..89404c78 100644 --- a/Source/Mocha.Hotload/Upgraders/StructUpgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/StructUpgrader.cs @@ -1,7 +1,8 @@ -using System.Reflection; +using Mocha.Hotload.Util; +using System.Reflection; using System.Runtime.Serialization; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading.Upgraders; /// /// A member upgrader for structs. @@ -18,7 +19,7 @@ public bool CanUpgrade( MemberInfo memberInfo ) } /// - public void UpgradeMember( object oldInstance, UpgradableMember oldMember, object newInstance, UpgradableMember newMember ) + public void UpgradeMember( object? oldInstance, UpgradableMember oldMember, object? newInstance, UpgradableMember newMember ) { var oldValue = oldMember.GetValue( oldInstance ); if ( oldValue is null ) diff --git a/Source/Mocha.Hotload/Upgraders/UpgradableMember.cs b/Source/Mocha.Hotload/Upgraders/UpgradableMember.cs index f087d86a..c1ea08c7 100644 --- a/Source/Mocha.Hotload/Upgraders/UpgradableMember.cs +++ b/Source/Mocha.Hotload/Upgraders/UpgradableMember.cs @@ -1,9 +1,9 @@ using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading; /// -/// A wrapper class for property and field infos. +/// A wrapper class for and . /// internal sealed class UpgradableMember { @@ -17,103 +17,111 @@ internal sealed class UpgradableMember /// internal Type Type { get; } - // In object: instance - // Out object: value - private readonly Func _getter; + /// + /// Whether or not the member is static. + /// + internal bool Static { get; } - // In object: instance - // In object: value - private readonly Action _setter; + /// + /// In: instance. + /// Out: Value. + /// + private readonly Func _getter; - private UpgradableMember( Func getter, Action setter, Type type, string name ) + /// + /// In: instance, value. + /// + private readonly Action _setter; + + /// + /// Initializes a new instance of . + /// + /// The name of the member. + /// The type of the member. + /// Whether or not the member is static. + /// The getter method for the member. + /// The setter method for the member. + private UpgradableMember( string name, Type type, bool isStatic, Func getter, Action setter ) { + Name = name; + Type = type; + Static = isStatic; + _getter = getter; _setter = setter; - - Type = type; - Name = name; } /// - /// Set the value that this member represents. - /// This will bail if is not assignable from . + /// Sets the value that this member represents. This will bail if is not assignable from . /// - internal void SetValue( object instance, object value ) + internal void SetValue( object? instance, object? value ) { - if ( !Type.IsAssignableFrom( value.GetType() ) ) - { + if ( !Type.IsAssignableFrom( value?.GetType() ) ) // Bail return; - } - _setter?.Invoke( instance, value ); + _setter.Invoke( instance, value ); } /// - /// Get the value that this member represents. + /// Gets the value that this member represents. /// - internal object? GetValue( object instance ) + internal object? GetValue( object? instance ) { - return _getter?.Invoke( instance ); + return _getter.Invoke( instance ); } #region "Constructors" - /// /// Create an given a member. - /// This will internally call or - /// depending on the member type. - /// Null is returned if this cannot be made into an UpgradableMember. + /// This will internally call or depending on the member type. + /// Null is returned if this cannot be made into an . /// internal static UpgradableMember? FromMember( MemberInfo memberInfo ) { if ( memberInfo is PropertyInfo propertyInfo ) - { return FromProperty( propertyInfo ); - } - else if ( memberInfo is FieldInfo fieldInfo ) - { + else if ( memberInfo is FieldInfo fieldInfo && !fieldInfo.IsInitOnly ) return FromField( fieldInfo ); - } - // Can't upgrade this, so don't return an UpgradableMember + // Can't upgrade this, so return null. return null; } /// - /// Create an given a field + /// Creates an given a . /// + /// A that represents the . internal static UpgradableMember FromField( FieldInfo fieldInfo ) { - return new( fieldInfo.GetValue, fieldInfo.SetValue, fieldInfo.FieldType, fieldInfo.Name ); + return new( fieldInfo.Name, fieldInfo.FieldType, fieldInfo.IsStatic, fieldInfo.GetValue, fieldInfo.SetValue ); } /// - /// Create an given a property + /// Creates an given a . + /// If the does not have a getter, a setter, or the getter has parameters then null is returned. /// + /// A that represents the . Null is returned if the does not satisfy the conditions outlined. internal static UpgradableMember? FromProperty( PropertyInfo propertyInfo ) { var getMethod = propertyInfo.GetGetMethod( true ); var setMethod = propertyInfo.GetSetMethod( true ); - if ( getMethod is null ) - return null; - - if ( setMethod is null ) + if ( getMethod is null || setMethod is null ) return null; - // Some get methods (array indexers) have parameters which we don't support + // TODO: Some get methods (array indexers) have parameters which we don't support // yet. We can't upgrade these. if ( getMethod.GetParameters().Length != 0 ) return null; - var invokeGet = ( object instance ) => getMethod.Invoke( instance, null ); - var invokeSet = ( object instance, object value ) => + var invokeGet = ( object? instance ) => getMethod.Invoke( instance, null ); + var invokeSet = ( object? instance, object? value ) => { setMethod.Invoke( instance, new[] { value } ); }; - return new( invokeGet, invokeSet, propertyInfo.PropertyType, propertyInfo.Name ); + return new( propertyInfo.Name, propertyInfo.PropertyType, getMethod.IsStatic, invokeGet, invokeSet ); } #endregion } diff --git a/Source/Mocha.Hotload/Upgraders/Upgrader.cs b/Source/Mocha.Hotload/Upgraders/Upgrader.cs index f8670e6c..c3670b26 100644 --- a/Source/Mocha.Hotload/Upgraders/Upgrader.cs +++ b/Source/Mocha.Hotload/Upgraders/Upgrader.cs @@ -2,8 +2,9 @@ using System.Collections.Immutable; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.Serialization; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Upgrading; /// /// The core class for upgrading members when swapping assemblies. @@ -16,7 +17,10 @@ internal static class Upgrader /// internal static Dictionary UpgradedReferences { get; } = new(); - private static List s_upgraders { get; set; } = null!; + /// + /// An array of all the upgrader instances to use. + /// + private static ImmutableArray s_upgraders { get; set; } = ImmutableArray.Empty; /// /// This must be called before invoking any other functions. Ideally, this should be @@ -30,28 +34,132 @@ internal static void Init() // ahead-of-time rather than setting them up on-demand. var upgraderTypes = Assembly.GetExecutingAssembly().GetTypes() - .Where( t => t.GetInterface( nameof( IMemberUpgrader ) ) is not null ) - .ToImmutableArray(); + .Where( t => t.GetInterface( nameof( IMemberUpgrader ) ) is not null ); + + var upgraders = ImmutableArray.CreateBuilder(); + foreach ( var upgraderType in upgraderTypes ) + upgraders.Add( (IMemberUpgrader)Activator.CreateInstance( upgraderType )! ); + s_upgraders = upgraders.OrderByDescending( upgrader => upgrader.Priority ).ToImmutableArray(); + } + + /// + /// Upgrades a given and its entry point. + /// + /// The type of the entry points. + /// The old that is being unloaded. + /// The new that is being loaded. + /// The entry point that was created from the . + /// The entry point that was created from the . + internal static void Upgrade( Assembly oldAssembly, Assembly newAssembly, T oldEntryPoint, T newEntryPoint ) + { + UpgradedReferences.Clear(); + + // Upgrade static members. + foreach ( var oldType in oldAssembly.GetTypes() ) + { + var newType = newAssembly.GetType( oldType.FullName ?? oldType.Name ); + if ( newType is null ) + continue; + + UpgradeStaticInstance( oldType, newType ); + } + + // Upgrade entity types. + UpgradeEntities( oldAssembly, newAssembly ); + // Upgrade entry point. + UpgradeInstance( oldEntryPoint, newEntryPoint ); + } + + /// + /// Upgrades all entities that were affected by the swap. + /// + /// The old assembly being unloaded. + /// The new assembly being loaded. + internal static void UpgradeEntities( Assembly oldAssembly, Assembly newAssembly ) + { + var entityRegistryCopy = EntityRegistry.Instance.ToList(); + + for ( int i = 0; i < entityRegistryCopy.Count; i++ ) + { + var entity = entityRegistryCopy[i]; + var entityType = entity.GetType(); + + // Do we actually want to upgrade this? If not, skip. + if ( entityType.Assembly != oldAssembly ) + continue; + + // Unregister the old entity + EntityRegistry.Instance.UnregisterEntity( entity ); + + // Find new type for entity in new assembly + var newType = newAssembly.GetType( entityType.FullName ?? entityType.Name )!; + var newEntity = (IEntity)FormatterServices.GetUninitializedObject( newType )!; + + // Have we already upgraded this? + if ( UpgradedReferences.TryGetValue( entity.GetHashCode(), out var upgradedValue ) ) + { + newEntity = (IEntity)upgradedValue; + } + else + { + UpgradedReferences[entity.GetHashCode()] = newEntity; + UpgradeInstance( entity, newEntity ); + } - var upgraders = new IMemberUpgrader[upgraderTypes.Length]; - for ( var i = 0; i < upgraders.Length; i++ ) - upgraders[i] = (IMemberUpgrader)Activator.CreateInstance( upgraderTypes[i] )!; - s_upgraders = upgraders.OrderByDescending( upgrader => upgrader.Priority ).ToList(); + // If we created a new entity successfully, register it + if ( newEntity is not null ) + EntityRegistry.Instance.RegisterEntity( newEntity ); + } } + /// + /// Upgrades a s static members. + /// + /// The old version of the . + /// The new version of the . + internal static void UpgradeStaticInstance( Type oldType, Type newType ) + { + UpgradeMembers( oldType, newType, null, null ); + } + + /// + /// Upgrades an instance of an object. + /// + /// The old instance. + /// The new instance. internal static void UpgradeInstance( object? oldInstance, object? newInstance ) { // Bail if ( oldInstance is null || newInstance is null ) return; - var oldType = oldInstance.GetType(); - var newType = newInstance.GetType(); + // Unregister events for old object + Event.Unregister( oldInstance ); + + // Upgrade the members. + UpgradeMembers( oldInstance.GetType(), newInstance.GetType(), oldInstance, newInstance ); - // Get all fields from the old instance - var oldMembers = oldType.GetMembers( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); + // Register events for new object + Event.Register( newInstance ); + } - // For each field: + /// + /// Upgrades all members on a type. + /// + /// The old version of the . + /// The new version of the . + /// The old instance. + /// The new instance. + private static void UpgradeMembers( Type oldType, Type newType, object? oldInstance, object? newInstance ) + { + // If both instance are null then we're upgrading static members. + var bindingFlags = (oldInstance is null && newInstance is null) + ? BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + : BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + // Get all members from the old type + var oldMembers = oldType.GetMembers( bindingFlags ); + + // For each member: // - If it's a reference type, we will want to upgrade it if it's a class // and not a delegate // - Otherwise, copy the value @@ -60,14 +168,11 @@ internal static void UpgradeInstance( object? oldInstance, object? newInstance ) // // Old member // - if ( oldMember.GetCustomAttribute() is not null ) - continue; - - if ( oldMember.GetCustomAttribute() is not null ) + if ( oldMember.GetCustomAttribute() is not null || + oldMember.GetCustomAttribute() is not null ) continue; var oldUpgradable = UpgradableMember.FromMember( oldMember ); - // Can we upgrade this? if ( oldUpgradable is null ) continue; @@ -75,18 +180,12 @@ internal static void UpgradeInstance( object? oldInstance, object? newInstance ) // // New member // - var newMember = newType.GetMember( oldMember.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ) - .FirstOrDefault(); - + var newMember = newType.GetMember( oldMember.Name, bindingFlags ).FirstOrDefault(); // Does this member exist? (eg. might have been deleted) - if ( newMember is null ) - continue; - - if ( newMember.GetCustomAttribute() != null ) + if ( newMember is null || newMember.GetCustomAttribute() is not null ) continue; var newUpgradable = UpgradableMember.FromMember( newMember ); - // Can we upgrade this? if ( newUpgradable is null ) continue; @@ -95,7 +194,6 @@ internal static void UpgradeInstance( object? oldInstance, object? newInstance ) // Upgrade! // var wasUpgraded = false; - foreach ( var upgrader in s_upgraders ) { if ( !upgrader.CanUpgrade( oldMember ) ) @@ -103,7 +201,6 @@ internal static void UpgradeInstance( object? oldInstance, object? newInstance ) upgrader.UpgradeMember( oldInstance, oldUpgradable, newInstance, newUpgradable ); wasUpgraded = true; - break; } diff --git a/Source/Mocha.Hotload/Utils/FileUtil.cs b/Source/Mocha.Hotload/Utils/FileUtil.cs new file mode 100644 index 00000000..9e74d3b7 --- /dev/null +++ b/Source/Mocha.Hotload/Utils/FileUtil.cs @@ -0,0 +1,25 @@ +namespace Mocha.Hotload.Util; + +/// +/// Contains utility methods for file IO. +/// +internal static class FileUtil +{ + /// + /// Returns whether or not a file is in use by another process. + /// + /// The path to a file to check. + /// Whether or not a file is in use by another process. + internal static bool IsFileInUse( string filePath ) + { + try + { + using var fileStream = new FileStream( filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None ); + return false; + } + catch ( IOException ) + { + return true; + } + } +} diff --git a/Source/Mocha.Hotload/Utils/MemberInfoExtensions.cs b/Source/Mocha.Hotload/Utils/MemberInfoExtensions.cs index 79f53804..2df4ea69 100644 --- a/Source/Mocha.Hotload/Utils/MemberInfoExtensions.cs +++ b/Source/Mocha.Hotload/Utils/MemberInfoExtensions.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Reflection; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Util; /// /// A collection of extension methods for a . diff --git a/Source/Mocha.Hotload/Utils/XmlElementExtensions.cs b/Source/Mocha.Hotload/Utils/XmlElementExtensions.cs index 1917ad09..f1eb2338 100644 --- a/Source/Mocha.Hotload/Utils/XmlElementExtensions.cs +++ b/Source/Mocha.Hotload/Utils/XmlElementExtensions.cs @@ -1,6 +1,6 @@ using System.Xml; -namespace Mocha.Hotload; +namespace Mocha.Hotload.Util; /// /// A collection of extension methods for s. diff --git a/Source/Mocha.Tests/Mocha.Tests.csproj b/Source/Mocha.Tests/Mocha.Tests.csproj new file mode 100644 index 00000000..c9c47733 --- /dev/null +++ b/Source/Mocha.Tests/Mocha.Tests.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + + + + diff --git a/Source/Mocha.Tests/Usings.cs b/Source/Mocha.Tests/Usings.cs new file mode 100644 index 00000000..540383dc --- /dev/null +++ b/Source/Mocha.Tests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/Source/Mocha.sln b/Source/Mocha.sln index c6d6b35c..d2e7d714 100644 --- a/Source/Mocha.sln +++ b/Source/Mocha.sln @@ -48,7 +48,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocha.Hotload", "Mocha.Hotl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocha.Editor", "Mocha.Editor\Mocha.Editor.csproj", "{E37A990E-4041-4F9C-8202-CACA45803376}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EditorTools", "EditorTools", "{FE285D47-E211-4111-9A3B-C71F65380D60}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Editor", "Editor", "{FE285D47-E211-4111-9A3B-C71F65380D60}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mocha", "Mocha\Mocha.vcxproj", "{2BF31211-78F2-42DE-AA9A-E9718C2A9055}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MochaDedicatedServer", "MochaDedicatedServer\MochaDedicatedServer.vcxproj", "{860C57C4-6E4B-445F-9614-9084AF4CD46B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{40918016-AB8B-47EC-9B4C-EDF1532D3FAF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocha.Tests", "Mocha.Tests\Mocha.Tests.csproj", "{267A391D-CD51-4A29-A41B-11D57E9F9AAF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{72F58AEF-9202-4AD4-8D52-8AA54B30550B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -156,6 +166,42 @@ Global {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x64.Build.0 = Release|Any CPU {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x86.ActiveCfg = Release|Any CPU {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x86.Build.0 = Release|Any CPU + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|Any CPU.Build.0 = Debug|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|x64.ActiveCfg = Debug|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|x64.Build.0 = Debug|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|x86.ActiveCfg = Debug|Win32 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|x86.Build.0 = Debug|Win32 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|Any CPU.ActiveCfg = Release|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|Any CPU.Build.0 = Release|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|x64.ActiveCfg = Release|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|x64.Build.0 = Release|x64 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|x86.ActiveCfg = Release|Win32 + {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Release|x86.Build.0 = Release|Win32 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|Any CPU.Build.0 = Debug|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|x64.ActiveCfg = Debug|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|x64.Build.0 = Debug|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|x86.ActiveCfg = Debug|Win32 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Debug|x86.Build.0 = Debug|Win32 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|Any CPU.ActiveCfg = Release|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|Any CPU.Build.0 = Release|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|x64.ActiveCfg = Release|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|x64.Build.0 = Release|x64 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|x86.ActiveCfg = Release|Win32 + {860C57C4-6E4B-445F-9614-9084AF4CD46B}.Release|x86.Build.0 = Release|Win32 + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|x64.ActiveCfg = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|x64.Build.0 = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|x86.ActiveCfg = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Debug|x86.Build.0 = Debug|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|Any CPU.Build.0 = Release|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|x64.ActiveCfg = Release|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|x64.Build.0 = Release|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|x86.ActiveCfg = Release|Any CPU + {267A391D-CD51-4A29-A41B-11D57E9F9AAF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -170,6 +216,10 @@ Global {0FD2339A-B0C8-4AFC-B2AB-AF3C0DDCD6F2} = {2F5C4610-1254-4D33-A1A6-5B38197346EE} {E37A990E-4041-4F9C-8202-CACA45803376} = {FE285D47-E211-4111-9A3B-C71F65380D60} {FE285D47-E211-4111-9A3B-C71F65380D60} = {2F5C4610-1254-4D33-A1A6-5B38197346EE} + {2BF31211-78F2-42DE-AA9A-E9718C2A9055} = {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} + {860C57C4-6E4B-445F-9614-9084AF4CD46B} = {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} + {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} = {E5E9BDE7-3F7F-4044-ACFD-FE2F0F66AB53} + {267A391D-CD51-4A29-A41B-11D57E9F9AAF} = {72F58AEF-9202-4AD4-8D52-8AA54B30550B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {501E447E-DCFC-42D2-AF68-88486D3529DD} diff --git a/Source/Mocha/Mocha.vcxproj b/Source/Mocha/Mocha.vcxproj new file mode 100644 index 00000000..d957d2dd --- /dev/null +++ b/Source/Mocha/Mocha.vcxproj @@ -0,0 +1,166 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {2bf31211-78f2-42de-aa9a-e9718c2a9055} + Mocha + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(VULKAN_SDK)\Include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath);$(SolutionDir)Mocha.Host\Thirdparty\imgui;$(SolutionDir)Mocha.Host\ + $(SolutionDir)..\build + $(SolutionDir)vcpkg_installed\$(Platform)-windows\lib;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) + $(SolutionDir)..\ + WindowsLocalDebugger + NativeWithManagedCore + + + $(SolutionDir)Mocha.Host\Thirdparty\imgui;$(SolutionDir)Mocha.Host\;$(ExternalIncludePath) + $(SolutionDir)..\build + $(SolutionDir)..\ + WindowsLocalDebugger + NativeWithManagedCore + + + x64-windows + + + true + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + stdc17 + + + Windows + true + $(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + stdc17 + + + Windows + true + true + true + + + + + + + + {e07c31bc-2908-46ec-a186-d27077aa3eba} + + + + + + \ No newline at end of file diff --git a/Source/Mocha/Mocha.vcxproj.filters b/Source/Mocha/Mocha.vcxproj.filters new file mode 100644 index 00000000..4327830c --- /dev/null +++ b/Source/Mocha/Mocha.vcxproj.filters @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/Mocha/main.cpp b/Source/Mocha/main.cpp new file mode 100644 index 00000000..2aa1b7b4 --- /dev/null +++ b/Source/Mocha/main.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +/// +/// Gets a command line argument option. +/// +char* getCmdOption( char** begin, char** end, const std::string& option ); + +int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cmdshow ) +{ + Globals::m_activeProjectPath = getCmdOption( __argv, __argv + __argc, "-project" ); + Globals::m_isDedicatedServer = false; + + ClientRoot root = ClientRoot(); + + root.Startup(); + root.Run(); + root.Shutdown(); + + return 0; +} + +// From https://stackoverflow.com/a/868894 +char* getCmdOption( char** begin, char** end, const std::string& option ) +{ + char** itr = std::find( begin, end, option ); + if ( itr != end && ++itr != end ) + { + return *itr; + } + return 0; +} diff --git a/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj b/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj new file mode 100644 index 00000000..f94747a8 --- /dev/null +++ b/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj @@ -0,0 +1,167 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {860c57c4-6e4b-445f-9614-9084af4cd46b} + MochaDedicatedServer + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(VULKAN_SDK)\Include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include;$(SolutionDir)vcpkg_installed\$(Platform)-windows\include\SDL2;$(ExternalIncludePath);$(SolutionDir)Mocha.Host\Thirdparty\imgui;$(SolutionDir)Mocha.Host\ + $(SolutionDir)..\build + $(SolutionDir)vcpkg_installed\$(Platform)-windows\lib;$(LibraryPath) + $(SolutionDir)..\ + WindowsLocalDebugger + NativeWithManagedCore + + + $(SolutionDir)Mocha.Host\Thirdparty\imgui;$(SolutionDir)Mocha.Host\;$(ExternalIncludePath) + $(SolutionDir)..\build + $(SolutionDir)..\ + WindowsLocalDebugger + NativeWithManagedCore + + + x64-windows + + + true + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + DEDICATED_SERVER;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + stdc17 + + + Console + true + $(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + Level3 + true + true + true + DEDICATED_SERVER;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + stdc17 + + + Console + true + true + true + $(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + + + + {e07c31bc-2908-46ec-a186-d27077aa3eba} + + + + + + \ No newline at end of file diff --git a/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj.filters b/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj.filters new file mode 100644 index 00000000..4327830c --- /dev/null +++ b/Source/MochaDedicatedServer/MochaDedicatedServer.vcxproj.filters @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/MochaDedicatedServer/main.cpp b/Source/MochaDedicatedServer/main.cpp new file mode 100644 index 00000000..73a685d9 --- /dev/null +++ b/Source/MochaDedicatedServer/main.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include + +/// +/// Gets a command line argument option. +/// +char* getCmdOption( char** begin, char** end, const std::string& option ); + +int main( int argc, char* argv[] ) +{ + Globals::m_activeProjectPath = getCmdOption( argv, argv + argc, "-project" ); + Globals::m_isDedicatedServer = true; + + ServerRoot root = ServerRoot(); + + root.Startup(); + root.Run(); + root.Shutdown(); + + return 0; +} + +// From https://stackoverflow.com/a/868894 +char* getCmdOption( char** begin, char** end, const std::string& option ) +{ + char** itr = std::find( begin, end, option ); + if ( itr != end && ++itr != end ) + { + return *itr; + } + return 0; +} \ No newline at end of file diff --git a/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs deleted file mode 100644 index 7587c39c..00000000 --- a/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MochaTool.InteropGen; - -internal class BaseCodeGenerator -{ - protected List Units { get; } = new(); - - public BaseCodeGenerator( List units ) - { - Units = units; - } - - protected string GetHeader() - { - return $""" - //------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // InteropGen generated on {DateTime.Now} - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - //------------------------------------------------------------------------------ - """; - } -} diff --git a/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs index 1864461f..eeb6a576 100644 --- a/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs +++ b/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs @@ -1,35 +1,112 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Parsing; +using System.CodeDom.Compiler; +using System.Collections.Immutable; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.CodeGen; -sealed class ManagedCodeGenerator : BaseCodeGenerator +/// +/// Contains functionality for generating C# code. +/// +internal static class ManagedCodeGenerator { - public ManagedCodeGenerator( List units ) : base( units ) + /// + /// The namespace that all generated code will be under. + /// + private const string Namespace = "Mocha.Glue"; + + /// + /// An array containing all using declarations for generated code. + /// + private static readonly string[] Usings = new[] { - } - - private List GetUsings() + "System.Runtime.InteropServices", + "System.Runtime.Serialization", + "Mocha.Common" + }; + + /// + /// The header to be used at the top of generated code. + /// + private static string Header => $""" + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // InteropGen generated on {DateTime.Now} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + """; + + /// + /// Generates and returns C# code for a set of s. + /// + /// An enumerable list of s to generate code for. + /// C# code representing the set of s passed. + internal static string GenerateCode( IEnumerable units ) { - return new() { "System.Runtime.InteropServices", "Mocha.Common" }; - } + var (baseTextWriter, writer) = Utils.CreateWriter(); - private string GetNamespace() - { - return "Mocha.Glue"; + // Write header. + writer.WriteLine( Header ); + writer.WriteLine(); + + // Write using statements. + foreach ( var usingStatement in Usings ) + writer.WriteLine( $"using {usingStatement};" ); + + // Write namespace. + writer.WriteLine(); + writer.WriteLine( $"namespace {Namespace};" ); + writer.WriteLine(); + + // Write each unit. + foreach ( var unit in units ) + { + switch ( unit ) + { + case Class c when c.IsNamespace: + GenerateNamespaceCode( writer, c ); + break; + case Class c: + GenerateClassCode( writer, c ); + break; + case Struct s: + GenerateStructCode( writer, s ); + break; + default: + continue; + } + + writer.WriteLine(); + } + + return baseTextWriter.ToString(); } - private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) + /// + /// Generates C# code for a class. + /// + /// The writer to append the code to. + /// The class to write code for. + private static void GenerateClassCode( IndentedTextWriter writer, Class c ) { // // Gather everything we need into nice lists // List decls = new(); - foreach ( var method in sel.Methods ) + foreach ( var method in c.Methods ) { var returnType = Utils.GetManagedType( method.ReturnType ); var name = method.Name; + var returnsPointer = Utils.IsPointer( method.ReturnType ) && !method.IsConstructor && !method.IsDestructor; + + if ( returnsPointer ) + returnType = "IntPtr"; + if ( returnType == "string" ) returnType = "IntPtr"; // Strings are handled specially - they go from pointer to string using InteropUtils.GetString @@ -51,43 +128,43 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // any parameters. The return type is the last type argument passed to // the delegate. // - decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{sel.Name}_{name}MethodPtr;" ); + decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{c.Name}_{name}MethodPtr;" ); } // // Write shit // - writer.WriteLine( $"public unsafe class {sel.Name} : INativeGlue" ); + writer.WriteLine( $"public unsafe class {c.Name} : INativeGlue" ); writer.WriteLine( "{" ); writer.Indent++; - writer.WriteLine( "private IntPtr instance;" ); - writer.WriteLine( "public IntPtr NativePtr => instance;" ); + writer.WriteLine( "public IntPtr NativePtr { get; set; }" ); // Decls writer.WriteLine(); foreach ( var decl in decls ) - { writer.WriteLine( decl ); - } writer.WriteLine(); // Ctor - var ctor = sel.Methods.First( x => x.IsConstructor ); - var managedCtorArgs = string.Join( ", ", ctor.Parameters.Select( x => $"{Utils.GetManagedType( x.Type )} {x.Name}" ) ); + if ( c.Methods.Any( x => x.IsConstructor ) ) + { + var ctor = c.Methods.First( x => x.IsConstructor ); + var managedCtorArgs = string.Join( ", ", ctor.Parameters.Select( x => $"{Utils.GetManagedType( x.Type )} {x.Name}" ) ); - writer.WriteLine( $"public {sel.Name}( {managedCtorArgs} )" ); - writer.WriteLine( "{" ); - writer.Indent++; + writer.WriteLine( $"public {c.Name}( {managedCtorArgs} )" ); + writer.WriteLine( "{" ); + writer.Indent++; - var ctorCallArgs = string.Join( ", ", ctor.Parameters.Select( x => x.Name ) ); - writer.WriteLine( $"this.instance = this.Ctor( {ctorCallArgs} );" ); + var ctorCallArgs = string.Join( ", ", ctor.Parameters.Select( x => x.Name ) ); + writer.WriteLine( $"this.NativePtr = this.Ctor( {ctorCallArgs} );" ); - writer.Indent--; - writer.WriteLine( "}" ); + writer.Indent--; + writer.WriteLine( "}" ); + } // Methods - foreach ( var method in sel.Methods ) + foreach ( var method in c.Methods ) { writer.WriteLine(); @@ -101,6 +178,8 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // We return a pointer to the created object if it's a ctor/dtor, but otherwise we'll do auto-conversions to our managed types var returnType = (method.IsConstructor || method.IsDestructor) ? "IntPtr" : Utils.GetManagedType( method.ReturnType ); + var returnsPointer = Utils.IsPointer( method.ReturnType ) && !method.IsConstructor && !method.IsDestructor; + // If this is a ctor or dtor, we don't want to be able to call the method manually var accessLevel = (method.IsConstructor || method.IsDestructor) ? "private" : "public"; @@ -113,7 +192,7 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) writer.Indent++; // Spin up a MemoryContext instance - writer.WriteLine( $"using var ctx = new MemoryContext( \"{sel.Name}.{name}\" );" ); + writer.WriteLine( $"using var ctx = new MemoryContext( \"{c.Name}.{name}\" );" ); // // Gather function body @@ -122,7 +201,7 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // We need to pass the instance in if this is not a static method if ( !method.IsStatic ) - paramsAndInstance = paramsAndInstance.Prepend( new Variable( "instance", "IntPtr" ) ).ToList(); + paramsAndInstance = paramsAndInstance.Prepend( new Variable( "NativePtr", "IntPtr" ) ).ToImmutableArray(); // Gather function call arguments. Make sure that we're passing in a pointer for everything var paramNames = paramsAndInstance.Select( x => "ctx.GetPtr( " + x.Name + " )" ); @@ -130,22 +209,33 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // Function call arguments as comma-separated string var functionCallArgs = string.Join( ", ", paramNames ); - // If we want to return a value: - if ( returnType != "void" ) - writer.Write( "return " ); + if ( returnsPointer ) + { + // If we want to return a pointer: + writer.WriteLine( $"var ptr = _{name}( {functionCallArgs} );" ); + writer.WriteLine( $"var obj = FormatterServices.GetUninitializedObject( typeof( {returnType} ) ) as {returnType};" ); + writer.WriteLine( $"obj.NativePtr = ptr;" ); + writer.WriteLine( $"return obj;" ); + } + else + { + // If we want to return a value: + if ( returnType != "void" ) + writer.Write( "return " ); - // This is a pretty dumb and HACKy way of handling strings - if ( returnType == "string" ) - writer.Write( "ctx.GetString( " ); + // This is a pretty dumb and HACKy way of handling strings + if ( returnType == "string" ) + writer.Write( "ctx.GetString( " ); - // Call the function.. - writer.Write( $"_{name}( {functionCallArgs} )" ); + // Call the function.. + writer.Write( $"_{name}( {functionCallArgs} )" ); - // Finish string - if ( returnType == "string" ) - writer.Write( ")" ); + // Finish string + if ( returnType == "string" ) + writer.Write( ")" ); - writer.WriteLine( ";" ); + writer.WriteLine( ";" ); + } writer.Indent--; writer.WriteLine( "}" ); @@ -155,35 +245,45 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) writer.WriteLine( "}" ); } - private void GenerateStructCode( ref IndentedTextWriter writer, Structure sel ) + /// + /// Generates C# code for a struct. + /// + /// The writer to append the code to. + /// The struct to write code for. + private static void GenerateStructCode( IndentedTextWriter writer, Struct s ) { writer.WriteLine( $"[StructLayout( LayoutKind.Sequential )]" ); - writer.WriteLine( $"public struct {sel.Name}" ); + writer.WriteLine( $"public struct {s.Name}" ); writer.WriteLine( "{" ); writer.Indent++; - foreach ( var field in sel.Fields ) - { + foreach ( var field in s.Fields ) writer.WriteLine( $"public {Utils.GetManagedType( field.Type )} {field.Name};" ); - } writer.Indent--; writer.WriteLine( "}" ); } - private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) + /// + /// Generates C# code for a namespace. + /// + /// The writer to append the code to. + /// The namespace to write code for. + private static void GenerateNamespaceCode( IndentedTextWriter writer, Class ns ) { // // Gather everything we need into nice lists // List decls = new(); - foreach ( var method in sel.Methods ) + foreach ( var method in ns.Methods ) { var returnType = Utils.GetManagedType( method.ReturnType ); var name = method.Name; - if ( returnType == "string" ) + var returnsPointer = Utils.IsPointer( method.ReturnType ) && !method.IsConstructor && !method.IsDestructor; + + if ( returnType == "string" || returnsPointer ) returnType = "IntPtr"; // Strings are handled specially - they go from pointer to string using InteropUtils.GetString var parameterTypes = method.Parameters.Select( x => "IntPtr" ); // Everything gets passed as a pointer @@ -198,24 +298,22 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) // any parameters. The return type is the last type argument passed to // the delegate. // - decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{sel.Name}_{name}MethodPtr;" ); + decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{ns.Name}_{name}MethodPtr;" ); } // // Write shit // - writer.WriteLine( $"public static unsafe class {sel.Name}" ); + writer.WriteLine( $"public static unsafe class {ns.Name}" ); writer.WriteLine( "{" ); writer.Indent++; writer.WriteLine(); foreach ( var decl in decls ) - { writer.WriteLine( decl ); - } // Methods - foreach ( var method in sel.Methods ) + foreach ( var method in ns.Methods ) { writer.WriteLine(); @@ -223,30 +321,45 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) var name = method.Name; var returnType = Utils.GetManagedType( method.ReturnType ); var accessLevel = (method.IsConstructor || method.IsDestructor) ? "private" : "public"; + var returnsPointer = Utils.IsPointer( method.ReturnType ) && !method.IsConstructor && !method.IsDestructor; writer.WriteLine( $"{accessLevel} static {returnType} {name}( {managedCallParams} ) " ); writer.WriteLine( "{" ); writer.Indent++; // Spin up a MemoryContext instance - writer.WriteLine( $"using var ctx = new MemoryContext( \"{sel.Name}.{name}\" );" ); + writer.WriteLine( $"using var ctx = new MemoryContext( \"{ns.Name}.{name}\" );" ); - var @params = method.Parameters; - var paramNames = @params.Select( x => "ctx.GetPtr( " + x.Name + " )" ); + var paramNames = method.Parameters.Select( x => "ctx.GetPtr( " + x.Name + " )" ); var functionCallArgs = string.Join( ", ", paramNames ); - if ( returnType != "void" ) - writer.Write( "return " ); + if ( returnsPointer ) + { + // If we want to return a pointer: + writer.WriteLine( $"var ptr = _{name}( {functionCallArgs} );" ); + writer.WriteLine( $"var obj = FormatterServices.GetUninitializedObject( typeof( {returnType} ) ) as {returnType};" ); + writer.WriteLine( $"obj.instance = ptr;" ); + writer.WriteLine( $"return obj;" ); + } + else + { + // If we want to return a value: + if ( returnType != "void" ) + writer.Write( "return " ); - if ( returnType == "string" ) - writer.Write( "ctx.GetString( " ); + // This is a pretty dumb and HACKy way of handling strings + if ( returnType == "string" ) + writer.Write( "ctx.GetString( " ); - writer.Write( $"_{name}( {functionCallArgs} )" ); + // Call the function.. + writer.Write( $"_{name}( {functionCallArgs} )" ); - if ( returnType == "string" ) - writer.Write( " )" ); + // Finish string + if ( returnType == "string" ) + writer.Write( ")" ); - writer.WriteLine( ";" ); + writer.WriteLine( ";" ); + } writer.Indent--; writer.WriteLine( "}" ); @@ -255,39 +368,4 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) writer.Indent--; writer.WriteLine( "}" ); } - - public string GenerateManagedCode() - { - var (baseTextWriter, writer) = Utils.CreateWriter(); - - writer.WriteLine( GetHeader() ); - writer.WriteLine(); - - foreach ( var usingStatement in GetUsings() ) - writer.WriteLine( $"using {usingStatement};" ); - - writer.WriteLine(); - writer.WriteLine( $"namespace {GetNamespace()};" ); - writer.WriteLine(); - - foreach ( var unit in Units ) - { - if ( unit is Class c ) - { - if ( c.IsNamespace ) - GenerateNamespaceCode( ref writer, c ); - else - GenerateClassCode( ref writer, c ); - } - - if ( unit is Structure s ) - { - GenerateStructCode( ref writer, s ); - } - - writer.WriteLine(); - } - - return baseTextWriter.ToString(); - } } diff --git a/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs index 645f06ec..5721ae72 100644 --- a/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs +++ b/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs @@ -1,18 +1,40 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Parsing; +using System.CodeDom.Compiler; +using System.Collections.Immutable; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.CodeGen; -sealed class NativeCodeGenerator : BaseCodeGenerator +/// +/// Contains functionality for generating C++ code. +/// +internal static class NativeCodeGenerator { - public NativeCodeGenerator( List units ) : base( units ) - { - } - - public string GenerateNativeCode( string headerPath ) + /// + /// The header to be used at the top of generated code. + /// + private static string Header => $""" + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // InteropGen generated on {DateTime.Now} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + """; + + /// + /// Generates and returns C++ code for a set of s. + /// + /// The path to the header file that contained the units. + /// An enumerable list of s to generate code for. + /// C++ code representing the set of s passed. + internal static string GenerateCode( string headerPath, IEnumerable units ) { var (baseTextWriter, writer) = Utils.CreateWriter(); - writer.WriteLine( GetHeader() ); + writer.WriteLine( Header ); writer.WriteLine(); writer.WriteLine( "#pragma once" ); @@ -20,15 +42,15 @@ public string GenerateNativeCode( string headerPath ) writer.WriteLine(); - foreach ( var unit in Units ) + foreach ( var unit in units ) { - if ( unit is Class c ) - { - if ( c.IsNamespace ) - GenerateNamespaceCode( ref writer, c ); - else - GenerateClassCode( ref writer, c ); - } + if ( unit is not Class c ) + continue; + + if ( c.IsNamespace ) + GenerateNamespaceCode( writer, c ); + else + GenerateClassCode( writer, c ); writer.WriteLine(); } @@ -36,34 +58,47 @@ public string GenerateNativeCode( string headerPath ) return baseTextWriter.ToString(); } - private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class c ) + /// + /// Generates C++ code for a class. + /// + /// The writer to append the code to. + /// The class to write code for. + private static void GenerateClassCode( IndentedTextWriter writer, Class c ) { foreach ( var method in c.Methods ) { var args = method.Parameters; + if ( !method.IsStatic ) + args = args.Prepend( new Variable( "instance", $"{c.Name}*" ) ).ToImmutableArray(); + var argStr = string.Join( ", ", args.Select( x => { if ( x.Type == "std::string" ) - { return $"const char* {x.Name}"; - } return $"{x.Type} {x.Name}"; } ) ); var signature = $"extern \"C\" inline {method.ReturnType} __{c.Name}_{method.Name}( {argStr} )"; var body = ""; - var @params = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - - var accessor = $"{c.Name}::"; + var parameters = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - if ( method.ReturnType == "void" ) - body += $"{accessor}{method.Name}( {@params} );"; - else if ( method.ReturnType == "std::string" ) - body += $"std::string text = {accessor}{method.Name}( {@params} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + if ( method.IsConstructor ) + body += $"return new {c.Name}( {parameters} );"; + else if ( method.IsDestructor ) + body += $"instance->~{c.Name}( {parameters} );"; else - body += $"return {accessor}{method.Name}( {@params} );"; + { + var accessor = method.IsStatic ? $"{c.Name}::" : "instance->"; + + if ( method.ReturnType == "void" ) + body += $"{accessor}{method.Name}( {parameters} );"; + else if ( method.ReturnType == "std::string" ) + body += $"std::string text = {accessor}{method.Name}( {parameters} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + else + body += $"return {accessor}{method.Name}( {parameters} );"; + } writer.WriteLine( signature ); writer.WriteLine( "{" ); @@ -76,48 +111,37 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class c ) } } - private void GenerateClassCode( ref IndentedTextWriter writer, Class c ) + /// + /// Generates C++ code for a namespace. + /// + /// The writer to append the code to. + /// The namespace to write code for. + private static void GenerateNamespaceCode( IndentedTextWriter writer, Class ns ) { - foreach ( var method in c.Methods ) + foreach ( var method in ns.Methods ) { var args = method.Parameters; - if ( !method.IsStatic ) - args = args.Prepend( new Variable( "instance", $"{c.Name}*" ) ).ToList(); - var argStr = string.Join( ", ", args.Select( x => { if ( x.Type == "std::string" ) - { return $"const char* {x.Name}"; - } return $"{x.Type} {x.Name}"; } ) ); - var signature = $"extern \"C\" inline {method.ReturnType} __{c.Name}_{method.Name}( {argStr} )"; + var signature = $"extern \"C\" inline {method.ReturnType} __{ns.Name}_{method.Name}( {argStr} )"; var body = ""; - var @params = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); + var parameters = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - if ( method.IsConstructor ) - { - body += $"return new {c.Name}( {@params} );"; - } - else if ( method.IsDestructor ) - { - body += $"instance->~{c.Name}( {@params} );"; - } - else - { - var accessor = method.IsStatic ? $"{c.Name}::" : "instance->"; + var accessor = $"{ns.Name}::"; - if ( method.ReturnType == "void" ) - body += $"{accessor}{method.Name}( {@params} );"; - else if ( method.ReturnType == "std::string" ) - body += $"std::string text = {accessor}{method.Name}( {@params} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; - else - body += $"return {accessor}{method.Name}( {@params} );"; - } + if ( method.ReturnType == "void" ) + body += $"{accessor}{method.Name}( {parameters} );"; + else if ( method.ReturnType == "std::string" ) + body += $"std::string text = {accessor}{method.Name}( {parameters} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + else + body += $"return {accessor}{method.Name}( {parameters} );"; writer.WriteLine( signature ); writer.WriteLine( "{" ); diff --git a/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs b/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs new file mode 100644 index 00000000..0d385ad2 --- /dev/null +++ b/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs @@ -0,0 +1,26 @@ +using ClangSharp.Interop; + +namespace MochaTool.InteropGen.Extensions; + +/// +/// Contains extension methods for the . +/// +internal static class CXCursorExtensions +{ + /// + /// Returns whether or not the current item the cursor is over has the "generate_bindings" attribute on it. + /// + /// The cursor to check. + /// Whether or not the current item the cursor is over has the "generate_bindings" attribute on it. + internal static bool HasGenerateBindingsAttribute( this CXCursor cursor ) + { + if ( !cursor.HasAttrs ) + return false; + + var attr = cursor.GetAttr( 0 ); + if ( attr.Spelling.CString != "generate_bindings" ) + return false; + + return true; + } +} diff --git a/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs b/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs new file mode 100644 index 00000000..0a0056b9 --- /dev/null +++ b/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; + +namespace MochaTool.InteropGen.Extensions; + +/// +/// Contains extension methods for s. +/// +internal static partial class ILoggerExtensions +{ + /// + /// Logs the first message to the user. + /// + /// The instance to log to. + [LoggerMessage( EventId = 0, + Level = LogLevel.Information, + Message = "Generating C# <--> C++ interop code..." )] + internal static partial void LogIntro( this ILogger logger ); + + /// + /// Logs a timed operation to the user. + /// + /// The instance to log to. + /// The name of the timed operation. + /// The time in seconds that it took to complete the operation. + [LoggerMessage( EventId = 1, + Message = "{name} took {seconds} seconds." )] + internal static partial void ReportTime( this ILogger logger, LogLevel logLevel, string name, double seconds ); + + /// + /// Logs to the user that a header is being processed by the parser. + /// + /// The instance to log to. + /// The absolute path to the header file being processed. + [LoggerMessage( EventId = 2, + Level = LogLevel.Debug, + Message = "Processing header {path}..." )] + internal static partial void ProcessingHeader( this ILogger logger, string path ); + + /// + /// Logs a fatal C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 3, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void FatalDiagnostic( this ILogger logger, string diagnostic ); + + /// + /// Logs an error C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 4, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void ErrorDiagnostic( this ILogger logger, string diagnostic ); + + /// + /// Logs a warning C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 5, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void WarnDiagnostic( this ILogger logger, string diagnostic ); +} diff --git a/Source/MochaTool.InteropGen/Global.cs b/Source/MochaTool.InteropGen/Global.cs new file mode 100644 index 00000000..9049ac6e --- /dev/null +++ b/Source/MochaTool.InteropGen/Global.cs @@ -0,0 +1,26 @@ +global using static MochaTool.InteropGen.Global; +using Microsoft.Extensions.Logging; + +namespace MochaTool.InteropGen; + +/// +/// Contains globally used items in the project. +/// +internal static class Global +{ + /// + /// The instance of to use when logging. + /// + internal static readonly ILogger Log; + + /// + /// Initializes the instance. + /// + static Global() + { + using var factory = LoggerFactory.Create( builder => builder + .AddConsole() + .SetMinimumLevel( LogLevel.Information ) ); + Log = factory.CreateLogger( "InteropGen" ); + } +} diff --git a/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj b/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj index cf8ae20a..8a1951e0 100644 --- a/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj +++ b/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj @@ -15,6 +15,9 @@ + + + @@ -24,7 +27,7 @@ - + diff --git a/Source/MochaTool.InteropGen/Parser.cs b/Source/MochaTool.InteropGen/Parser.cs deleted file mode 100644 index 3544077c..00000000 --- a/Source/MochaTool.InteropGen/Parser.cs +++ /dev/null @@ -1,194 +0,0 @@ -using ClangSharp.Interop; -namespace MochaTool.InteropGen; - -public static class Parser -{ - /// - /// Cached launch arguments so that we don't have to regenerate them every time - /// - private static string[] s_launchArgs = GetLaunchArgs(); - private static string[] GetLaunchArgs() - { - // Generate includes from vcxproj - var includeDirs = VcxprojParser.ParseIncludes( "../Mocha.Host/Mocha.Host.vcxproj" ); - - var args = new List - { - "-x", - "c++", - "-fparse-all-comments", - "-std=c++20", - "-DVK_NO_PROTOTYPES", - "-DNOMINMAX", - "-DVK_USE_PLATFORM_WIN32_KHR" - }; - - args.AddRange( includeDirs.Select( x => "-I" + x ) ); - - return args.ToArray(); - } - - public unsafe static List GetUnits( string path ) - { - List units = new(); - - using var index = CXIndex.Create(); - using var unit = CXTranslationUnit.Parse( index, path, s_launchArgs, ReadOnlySpan.Empty, CXTranslationUnit_Flags.CXTranslationUnit_None ); - - for ( int i = 0; i < unit.NumDiagnostics; ++i ) - { - var diagnostics = unit.GetDiagnostic( (uint)i ); - Console.WriteLine( $"{diagnostics.Format( CXDiagnostic.DefaultDisplayOptions )}" ); - } - - var cursor = unit.Cursor; - - CXCursorVisitor cursorVisitor = ( CXCursor cursor, CXCursor parent, void* data ) => - { - if ( !cursor.Location.IsFromMainFile ) - return CXChildVisitResult.CXChildVisit_Continue; - - bool HasGenerateBindingsAttribute() - { - if ( !cursor.HasAttrs ) - return false; - - var attr = cursor.GetAttr( 0 ); - if ( attr.Spelling.CString != "generate_bindings" ) - return false; - - return true; - } - - switch ( cursor.Kind ) - { - // - // Struct / class / namespace - // - case CXCursorKind.CXCursor_ClassDecl: - units.Add( new Class( cursor.Spelling.ToString() ) ); - break; - case CXCursorKind.CXCursor_StructDecl: - units.Add( new Structure( cursor.Spelling.ToString() ) ); - break; - case CXCursorKind.CXCursor_Namespace: - units.Add( new Class( cursor.Spelling.ToString() ) - { - IsNamespace = true - } ); - break; - - // - // Methods - // - case CXCursorKind.CXCursor_Constructor: - case CXCursorKind.CXCursor_CXXMethod: - case CXCursorKind.CXCursor_FunctionDecl: - { - if ( !HasGenerateBindingsAttribute() ) - return CXChildVisitResult.CXChildVisit_Continue; - - var oName = cursor.LexicalParent.Spelling.ToString(); - var o = units.FirstOrDefault( x => x.Name == oName ); - var m = new Method( cursor.Spelling.ToString(), cursor.ReturnType.Spelling.ToString() ) - { - IsStatic = cursor.IsStatic - }; - - if ( o == null ) - { - Console.WriteLine( "No unit" ); - break; - } - - CXCursorVisitor methodChildVisitor = ( CXCursor cursor, CXCursor parent, void* data ) => - { - if ( cursor.Kind == CXCursorKind.CXCursor_ParmDecl ) - { - var type = cursor.Type.ToString(); - var name = cursor.Spelling.ToString(); - - var parameter = new Variable( name, type ); - - m.Parameters.Add( parameter ); - } - - return CXChildVisitResult.CXChildVisit_Recurse; - }; - - cursor.VisitChildren( methodChildVisitor, default ); - - if ( cursor.Kind == CXCursorKind.CXCursor_Constructor ) - { - // Constructor specific stuff here - m.ReturnType = $"{o.Name}*"; - m.Name = "Ctor"; - m.IsConstructor = true; - } - - if ( cursor.CXXAccessSpecifier == CX_CXXAccessSpecifier.CX_CXXPublic || cursor.Kind == CXCursorKind.CXCursor_FunctionDecl ) - o.Methods.Add( m ); - - break; - } - - // - // Field - // - case CXCursorKind.CXCursor_FieldDecl: - { - if ( !HasGenerateBindingsAttribute() ) - return CXChildVisitResult.CXChildVisit_Continue; - - var oName = cursor.LexicalParent.Spelling.ToString(); - var s = units.FirstOrDefault( x => x.Name == oName ); - - if ( s == null ) - break; - - s.Fields.Add( new Variable( cursor.Spelling.ToString(), cursor.Type.ToString() ) ); - break; - } - - default: - break; - } - - return CXChildVisitResult.CXChildVisit_Recurse; - }; - - cursor.VisitChildren( cursorVisitor, default ); - - // - // Remove all items with duplicate names - // - for ( int i = 0; i < units.Count; i++ ) - { - var o = units[i]; - o.Methods = o.Methods.GroupBy( x => x.Name ).Select( x => x.First() ).ToList(); - o.Fields = o.Fields.GroupBy( x => x.Name ).Select( x => x.First() ).ToList(); - } - - // - // Remove any units that have no methods or fields - // - units = units.Where( x => x.Methods.Count > 0 || x.Fields.Count > 0 ).ToList(); - - // - // Post-processing - // - foreach ( var o in units ) - { - // Create a default constructor if one wasn't already defined - if ( !o.Methods.Any( x => x.IsConstructor ) && o is not Class { IsNamespace: true } ) - { - o.Methods.Add( new Method( "Ctor", $"{o.Name}*" ) - { - IsConstructor = true - } ); - } - } - - return units; - } -} diff --git a/Source/MochaTool.InteropGen/Parsing/Class.cs b/Source/MochaTool.InteropGen/Parsing/Class.cs new file mode 100644 index 00000000..0ec9c643 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Class.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a class or namespace in C++. +/// +internal sealed class Class : IUnit +{ + /// + public string Name { get; } + /// + public bool IsNamespace { get; } + + /// + public ImmutableArray Fields { get; } + /// + public ImmutableArray Methods { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the class or namespace. + /// Whether or not it is a class or namespace. + /// All of the fields that are contained. + /// All of the methods that are contained. + private Class( string name, bool isNamespace, in ImmutableArray fields, in ImmutableArray methods ) + { + Name = name; + IsNamespace = isNamespace; + + Fields = fields; + Methods = methods; + } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + internal Class WithFields( in ImmutableArray fields ) + { + return new Class( Name, IsNamespace, fields, Methods ); + } + + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + internal Class WithMethods( in ImmutableArray methods ) + { + return new Class( Name, IsNamespace, Fields, methods ); + } + + /// + public override string ToString() + { + return Name; + } + + /// + IUnit IUnit.WithFields( in ImmutableArray fields ) => WithFields( fields ); + /// + IUnit IUnit.WithMethods( in ImmutableArray methods ) => WithMethods( methods ); + + /// + /// Returns a new instance of . + /// + /// The name of the class. + /// The fields contained in the class. + /// The methods contained in the class. + /// A new instance of . + internal static Class NewClass( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new Class( name, false, fields, methods ); + } + + /// + /// Returns a new instance of as a namespace. + /// + /// The name of the namespace. + /// The fields contained in the namespace. + /// The methods contained in the namespace. + /// A new instance of as a namespace. + internal static Class NewNamespace( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new Class( name, true, fields, methods ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/IUnit.cs b/Source/MochaTool.InteropGen/Parsing/IUnit.cs new file mode 100644 index 00000000..ad8b68fb --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/IUnit.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Defines a container for fields and methods defined in C++. +/// +internal interface IUnit +{ + /// + /// The name of the . + /// + string Name { get; } + + /// + /// All of the fields contained in the . + /// + ImmutableArray Fields { get; } + /// + /// All of the methods contained in the . + /// + ImmutableArray Methods { get; } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + IUnit WithFields( in ImmutableArray fields ); + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + IUnit WithMethods( in ImmutableArray methods ); +} diff --git a/Source/MochaTool.InteropGen/Parsing/Method.cs b/Source/MochaTool.InteropGen/Parsing/Method.cs new file mode 100644 index 00000000..8b9540cc --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Method.cs @@ -0,0 +1,111 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a method in C++. +/// +internal sealed class Method +{ + /// + /// The name of the method. + /// + internal string Name { get; } + /// + /// The literal string containing the return type of the method. + /// + internal string ReturnType { get; } + + /// + /// Whether or not the method is a constructor. + /// + internal bool IsConstructor { get; } = false; + /// + /// Whether or not the method is a destructor. + /// + internal bool IsDestructor { get; } = false; + /// + /// Whether or not the method is static. + /// + internal bool IsStatic { get; } = false; + + /// + /// An array of all the parameters in the method. + /// + internal ImmutableArray Parameters { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// Whether or not the method is a constructor. + /// Whether or not the method is a destructor. + /// Whether or not the method is static. + /// An array of all the parameters in the method. + private Method( string name, string returnType, bool isConstructor, bool isDestructor, bool isStatic, in ImmutableArray parameters ) + { + Name = name; + ReturnType = returnType; + + IsConstructor = isConstructor; + IsDestructor = isDestructor; + IsStatic = isStatic; + + Parameters = parameters; + } + + /// + /// Returns a new instance of the with the parameters given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the parameters given. + internal Method WithParameters( in ImmutableArray parameters ) + { + return new( Name, ReturnType, IsConstructor, IsDestructor, IsStatic, parameters ); + } + + /// + public override string ToString() + { + var p = string.Join( ", ", Parameters ); + return $"{ReturnType} {Name}( {p} )"; + } + + /// + /// Returns a new instance of as a constructor. + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// An array of all the parameters in the method. + /// A new instance of as a constructor. + internal static Method NewConstructor( string name, string returnType, in ImmutableArray parameters ) + { + return new( name, returnType, true, false, false, parameters ); + } + + /// + /// Returns a new instance of as a destructor. + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// An array of all the parameters in the method. + /// A new instance of as a destructor. + internal static Method NewDestructor( string name, string returnType, in ImmutableArray parameters ) + { + return new( name, returnType, false, true, false, parameters ); + } + + /// + /// Returns a new instance of . + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// Whether or not the method is static. + /// An array of all the parameters in the method. + /// A new instance of . + internal static Method NewMethod( string name, string returnType, bool isStatic, in ImmutableArray parameters ) + { + return new( name, returnType, false, false, isStatic, parameters ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Parser.cs b/Source/MochaTool.InteropGen/Parsing/Parser.cs new file mode 100644 index 00000000..14810da4 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Parser.cs @@ -0,0 +1,249 @@ +using ClangSharp.Interop; +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.Extensions; +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Contains all parsing functionality for C++ header files. +/// +internal static class Parser +{ + /// + /// Cached launch arguments so that we don't have to regenerate them every time + /// + private static readonly string[] s_launchArgs = GetLaunchArgs(); + + /// + /// Parses a header file and returns all of the s contained inside. + /// + /// The absolute path to the header file to parse. + /// All of the s contained inside the header file. + internal unsafe static IEnumerable GetUnits( string path ) + { + var units = new List(); + + using var index = CXIndex.Create(); + using var unit = CXTranslationUnit.Parse( index, path, s_launchArgs, ReadOnlySpan.Empty, CXTranslationUnit_Flags.CXTranslationUnit_None ); + + // Only start walking diagnostics if logging is enabled to the minimum level. + if ( Log.IsEnabled( LogLevel.Warning ) ) + { + for ( var i = 0; i < unit.NumDiagnostics; i++ ) + { + var diagnostics = unit.GetDiagnostic( (uint)i ); + switch ( diagnostics.Severity ) + { + case CXDiagnosticSeverity.CXDiagnostic_Fatal: + Log.FatalDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + case CXDiagnosticSeverity.CXDiagnostic_Error: + Log.ErrorDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + case CXDiagnosticSeverity.CXDiagnostic_Warning: + Log.WarnDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + } + } + } + + CXChildVisitResult cursorVisitor( CXCursor cursor, CXCursor parent, void* data ) + { + if ( !cursor.Location.IsFromMainFile ) + return CXChildVisitResult.CXChildVisit_Continue; + + switch ( cursor.Kind ) + { + // + // Struct / class / namespace + // + case CXCursorKind.CXCursor_ClassDecl: + units.Add( Class.NewClass( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + case CXCursorKind.CXCursor_StructDecl: + units.Add( Struct.NewStructure( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + case CXCursorKind.CXCursor_Namespace: + units.Add( Class.NewNamespace( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + + // + // Methods + // + case CXCursorKind.CXCursor_Constructor: + case CXCursorKind.CXCursor_CXXMethod: + case CXCursorKind.CXCursor_FunctionDecl: + return VisitMethod( cursor, units ); + + // + // Field + // + case CXCursorKind.CXCursor_FieldDecl: + return VisitField( cursor, units ); + } + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + unit.Cursor.VisitChildren( cursorVisitor, default ); + + // + // Remove all items with duplicate names + // + for ( int i = 0; i < units.Count; i++ ) + { + var item = units[i]; + item = item.WithFields( item.Fields.GroupBy( x => x.Name ).Select( x => x.First() ).ToImmutableArray() ) + .WithMethods( item.Methods.GroupBy( x => x.Name ).Select( x => x.First() ).ToImmutableArray() ); + + units[i] = item; + } + + // + // Remove any units that have no methods or fields + // + units = units.Where( x => x.Methods.Length > 0 || x.Fields.Length > 0 ).ToList(); + + return units; + } + + /// + /// The visitor method for walking a method declaration. + /// + /// The cursor that is traversing the method. + /// The collection to fetch method owners from. + /// The next action the cursor should take in traversal. + private static unsafe CXChildVisitResult VisitMethod( in CXCursor cursor, ICollection units ) + { + // Early bails. + if ( !cursor.HasGenerateBindingsAttribute() ) + return CXChildVisitResult.CXChildVisit_Continue; + if ( cursor.CXXAccessSpecifier != CX_CXXAccessSpecifier.CX_CXXPublic && cursor.Kind != CXCursorKind.CXCursor_FunctionDecl ) + return CXChildVisitResult.CXChildVisit_Continue; + + // Verify that the method has an owner. + var ownerName = cursor.LexicalParent.Spelling.ToString(); + var owner = units.FirstOrDefault( x => x.Name == ownerName ); + if ( owner is null ) + return CXChildVisitResult.CXChildVisit_Continue; + + string name; + string returnType; + bool isStatic; + bool isConstructor; + bool isDestructor; + + var parametersBuilder = ImmutableArray.CreateBuilder(); + // We're traversing a constructor. + if ( cursor.Kind == CXCursorKind.CXCursor_Constructor ) + { + name = "Ctor"; + returnType = owner.Name + '*'; + isStatic = false; + isConstructor = true; + isDestructor = false; + } + // We're traversing a destructor. + else if ( cursor.Kind == CXCursorKind.CXCursor_Destructor ) + { + name = "DeCtor"; + returnType = '~' + owner.Name; + isStatic = false; + isConstructor = false; + isDestructor = true; + } + // We're traversing a standard method. + else + { + name = cursor.Spelling.ToString(); + returnType = cursor.ReturnType.Spelling.ToString(); + isStatic = cursor.IsStatic; + isConstructor = false; + isDestructor = false; + } + + // Visitor for parameter delcarations. + CXChildVisitResult methodChildVisitor( CXCursor cursor, CXCursor parent, void* data ) + { + if ( cursor.Kind != CXCursorKind.CXCursor_ParmDecl ) + return CXChildVisitResult.CXChildVisit_Continue; + + var name = cursor.Spelling.ToString(); + var type = cursor.Type.ToString(); + + parametersBuilder.Add( new Variable( name, type ) ); + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + cursor.VisitChildren( methodChildVisitor, default ); + + // Construct the method. + Method method; + if ( isConstructor ) + method = Method.NewConstructor( name, returnType, parametersBuilder.ToImmutable() ); + else if ( isDestructor ) + method = Method.NewDestructor( name, returnType, parametersBuilder.ToImmutable() ); + else + method = Method.NewMethod( name, returnType, isStatic, parametersBuilder.ToImmutable() ); + + // Update owner with new method. + var newOwner = owner.WithMethods( owner.Methods.Add( method ) ); + units.Remove( owner ); + units.Add( newOwner ); + + return CXChildVisitResult.CXChildVisit_Continue; + } + + /// + /// The visitor method for walking a field declaration. + /// + /// The cursor that is traversing the method. + /// The collection to fetch method owners from. + /// The next action the cursor should take in traversal. + private static CXChildVisitResult VisitField( in CXCursor cursor, ICollection units ) + { + // Early bail. + if ( !cursor.HasGenerateBindingsAttribute() ) + return CXChildVisitResult.CXChildVisit_Continue; + + // Verify that the field has an owner. + var ownerName = cursor.LexicalParent.Spelling.ToString(); + var owner = units.FirstOrDefault( x => x.Name == ownerName ); + if ( owner is null ) + return CXChildVisitResult.CXChildVisit_Recurse; + + // Update owner with new field. + var newOwner = owner.WithFields( owner.Fields.Add( new Variable( cursor.Spelling.ToString(), cursor.Type.ToString() ) ) ); + units.Remove( owner ); + units.Add( newOwner ); + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + /// + /// Returns a compiled array of launch arguments to pass to the C++ parser. + /// + /// A compiled array of launch arguments to pass to the C++ parser. + private static string[] GetLaunchArgs() + { + // Generate includes from vcxproj + var includeDirs = VcxprojParser.ParseIncludes( "../Mocha.Host/Mocha.Host.vcxproj" ); + + var args = new List + { + "-x", + "c++", + "-fparse-all-comments", + "-std=c++20", + "-DVK_NO_PROTOTYPES", + "-DNOMINMAX", + "-DVK_USE_PLATFORM_WIN32_KHR" + }; + + args.AddRange( includeDirs.Select( x => "-I" + x ) ); + + return args.ToArray(); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Struct.cs b/Source/MochaTool.InteropGen/Parsing/Struct.cs new file mode 100644 index 00000000..d48fd6ba --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Struct.cs @@ -0,0 +1,74 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a struct in C++. +/// +internal sealed class Struct : IUnit +{ + /// + public string Name { get; } + + /// + public ImmutableArray Fields { get; } + /// + public ImmutableArray Methods { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the struct. + /// The fields contained in the struct. + /// The methods contained in the struct. + private Struct( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + Name = name; + + Fields = fields; + Methods = methods; + } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + internal Struct WithFields( in ImmutableArray fields ) + { + return new( Name, fields, Methods ); + } + + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + internal Struct WithMethods( in ImmutableArray methods ) + { + return new( Name, Fields, methods ); + } + + /// + public override string ToString() + { + return Name; + } + + /// + IUnit IUnit.WithFields( in ImmutableArray fields ) => WithFields( fields ); + /// + IUnit IUnit.WithMethods( in ImmutableArray methods ) => WithMethods( methods ); + + /// + /// Returns a new instance of . + /// + /// The name of the struct. + /// The fields contained in the struct. + /// The methods contained in the struct. + /// A new instance of . + internal static Struct NewStructure( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new( name, fields, methods ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Variable.cs b/Source/MochaTool.InteropGen/Parsing/Variable.cs new file mode 100644 index 00000000..60c5e1e8 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Variable.cs @@ -0,0 +1,33 @@ +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a variable in C++. This can be a field, parameter, etc. +/// +internal sealed class Variable +{ + /// + /// The name of the variable. + /// + internal string Name { get; } + /// + /// The literal string containing the type of the variable. + /// + internal string Type { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the variable. + /// The literal string containing the type of the variable. + internal Variable( string name, string type ) + { + Name = name; + Type = type; + } + + /// + public override string ToString() + { + return $"{Type} {Name}"; + } +} diff --git a/Source/MochaTool.InteropGen/VcxprojParser.cs b/Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs similarity index 81% rename from Source/MochaTool.InteropGen/VcxprojParser.cs rename to Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs index 4ddaef8f..d7317fca 100644 --- a/Source/MochaTool.InteropGen/VcxprojParser.cs +++ b/Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs @@ -1,7 +1,10 @@ using System.Xml; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.Parsing; +/// +/// Contains functionality for parsing vcxproj files. +/// internal static class VcxprojParser { // Note that these paths only work for the windows x64 platforms right now. @@ -11,19 +14,6 @@ internal static class VcxprojParser private const string ExternalIncludePath = "/rs:Project/rs:PropertyGroup[@Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\"]/rs:ExternalIncludePath"; private const string IncludePath = "/rs:Project/rs:PropertyGroup[@Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\"]/rs:IncludePath"; - private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceManager namespaceManager ) - { - var nodeList = root.SelectNodes( xpath, namespaceManager ); - if ( nodeList?.Count == 0 || nodeList?[0] == null ) - throw new Exception( "Couldn't find IncludePath!" ); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. - var includeStr = nodeList[0].InnerText; -#pragma warning restore CS8602 // Dereference of a possibly null reference. - - return includeStr; - } - /// /// Parse the include list from a vcxproj file. /// @@ -31,20 +21,20 @@ private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceM /// This currently only supports x64-windows, so any different includes for other platforms /// will not be reflected here. /// - public static List ParseIncludes( string path ) + internal static List ParseIncludes( string path ) { - XmlDocument doc = new XmlDocument(); + var doc = new XmlDocument(); doc.Load( path ); - XmlNamespaceManager namespaceManager = new XmlNamespaceManager( doc.NameTable ); + var namespaceManager = new XmlNamespaceManager( doc.NameTable ); namespaceManager.AddNamespace( "rs", "http://schemas.microsoft.com/developer/msbuild/2003" ); - if ( doc.DocumentElement == null ) + if ( doc.DocumentElement is null ) throw new Exception( "Failed to parse root node!" ); - XmlNode root = doc.DocumentElement; + var root = doc.DocumentElement; - List includes = new(); + var includes = new List(); // Select Project -> PropertyGroup -> ExternalIncludePath { @@ -70,7 +60,7 @@ public static List ParseIncludes( string path ) { "ExternalIncludePath", "" } }; - List parsedIncludes = new(); + var parsedIncludes = new List(); // Simple find-and-replace for macros and environment variables foreach ( var include in includes ) @@ -78,13 +68,22 @@ public static List ParseIncludes( string path ) var processedInclude = include; foreach ( var environmentVariable in environmentVariables ) - { processedInclude = processedInclude.Replace( $"$({environmentVariable.Key})", environmentVariable.Value ); - } parsedIncludes.Add( processedInclude ); } return parsedIncludes; } + + private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceManager namespaceManager ) + { + var nodeList = root.SelectNodes( xpath, namespaceManager ); + if ( nodeList?.Count == 0 || nodeList?[0] is null ) + throw new Exception( "Couldn't find IncludePath!" ); + + var includeStr = nodeList[0]!.InnerText; + + return includeStr; + } } diff --git a/Source/MochaTool.InteropGen/Program.cs b/Source/MochaTool.InteropGen/Program.cs index 6349302f..6e815d58 100644 --- a/Source/MochaTool.InteropGen/Program.cs +++ b/Source/MochaTool.InteropGen/Program.cs @@ -1,102 +1,133 @@ -namespace MochaTool.InteropGen; +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.CodeGen; +using MochaTool.InteropGen.Extensions; +using MochaTool.InteropGen.Parsing; +namespace MochaTool.InteropGen; + +/// +/// The main entry point to the IntropGen program. +/// public static class Program { - internal static List s_generatedPaths { get; set; } = new(); - internal static List s_units { get; set; } = new(); - internal static List s_files { get; set; } = new(); - - private static void ProcessHeader( string baseDir, string path ) + /// + /// Contains all of the parsed units to generate bindings for. + /// + private static readonly List s_units = new(); + /// + /// Contains all of the files that need to be generated. + /// + private static readonly List s_files = new(); + + /// + /// The entry point to the program. + /// + /// The command-line arguments given to the program. + public static void Main( string[] args ) { - Console.WriteLine( $"Processing header {path}..." ); + using var _totalTime = new StopwatchLog( "InteropGen", LogLevel.Information ); - var units = Parser.GetUnits( path ); - var fileName = Path.GetFileNameWithoutExtension( path ); + var baseDir = args[0]; + Log.LogIntro(); - var managedGenerator = new ManagedCodeGenerator( units ); - var managedCode = managedGenerator.GenerateManagedCode(); - File.WriteAllText( $"{baseDir}Mocha.Common\\Glue\\{fileName}.generated.cs", managedCode ); + // + // Prep + // + DeleteExistingFiles( baseDir ); - var nativeGenerator = new NativeCodeGenerator( units ); - var relativePath = Path.GetRelativePath( $"{baseDir}/Mocha.Host/", path ); - var nativeCode = nativeGenerator.GenerateNativeCode( relativePath ); + using ( var _parseTime = new StopwatchLog( "Parsing" ) ) + Parse( baseDir ); - Console.WriteLine( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h" ); - File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h", nativeCode ); + // + // Expand methods out into list of (method name, method) + // + var methods = s_units.OfType().SelectMany( unit => unit.Methods, ( unit, method ) => (unit.Name, method) ).ToList(); - s_files.Add( fileName ); - s_units.AddRange( units ); + // + // Write files + // + WriteManagedStruct( baseDir, methods ); + WriteNativeStruct( baseDir, methods ); + WriteNativeIncludes( baseDir ); } - private static void QueueDirectory( ref List queue, string directory ) + /// + /// Deletes and re-creates the generated file directories. + /// + /// The base directory that contains the source projects. + private static void DeleteExistingFiles( string baseDir ) { - foreach ( var file in Directory.GetFiles( directory ) ) - { - if ( file.EndsWith( ".h" ) && !file.EndsWith( ".generated.h" ) ) - { - var fileContents = File.ReadAllText( file ); - - if ( !fileContents.Contains( "GENERATE_BINDINGS", StringComparison.CurrentCultureIgnoreCase ) ) - continue; // Fast early bail + var destCsDir = $"{baseDir}\\Mocha.Common\\Glue"; + var destHeaderDir = $"{baseDir}\\Mocha.Host\\generated"; - QueueFile( ref queue, file ); - } - } - - foreach ( var subDirectory in Directory.GetDirectories( directory ) ) - { - QueueDirectory( ref queue, subDirectory ); - } - } + if ( Directory.Exists( destHeaderDir ) ) + Directory.Delete( destHeaderDir, true ); + if ( Directory.Exists( destCsDir ) ) + Directory.Delete( destCsDir, true ); - private static void QueueFile( ref List queue, string path ) - { - queue.Add( path ); + Directory.CreateDirectory( destHeaderDir ); + Directory.CreateDirectory( destCsDir ); } + /// + /// Parses all header files in the Mocha.Host project for interop generation. + /// + /// The base directory that contains the source projects. private static void Parse( string baseDir ) { - List queue = new(); - QueueDirectory( ref queue, baseDir ); + // Find and queue all of the header files to parse. + var queue = new List(); + QueueDirectory( queue, baseDir + "\\Mocha.Host" ); - var dispatcher = new ThreadDispatcher( ( files ) => + // Dispatch jobs to parse all files. + var dispatcher = new ThreadDispatcher( async ( files ) => { foreach ( var path in files ) - { - ProcessHeader( baseDir, path ); - } + await ProcessHeaderAsync( baseDir, path ); }, queue ); // Wait for all threads to finish... while ( !dispatcher.IsComplete ) - Thread.Sleep( 500 ); + Thread.Sleep( 1 ); } - private static void WriteManagedStruct( string baseDir, ref List<(string Name, Method method)> methods ) + /// + /// Writes the C# unmanaged arguments. + /// + /// The base directory that contains the source projects. + /// An enumerable list of all of the methods to write in the struct. + private static void WriteManagedStruct( string baseDir, IEnumerable<(string Name, Method method)> methods ) { var (baseManagedStructWriter, managedStructWriter) = Utils.CreateWriter(); - managedStructWriter.WriteLine( $"using System.Runtime.InteropServices;" ); + managedStructWriter.WriteLine( "using System.Runtime.InteropServices;" ); managedStructWriter.WriteLine(); - managedStructWriter.WriteLine( $"[StructLayout( LayoutKind.Sequential )]" ); - managedStructWriter.WriteLine( $"public struct UnmanagedArgs" ); - managedStructWriter.WriteLine( $"{{" ); + managedStructWriter.WriteLine( "[StructLayout( LayoutKind.Sequential )]" ); + managedStructWriter.WriteLine( "public struct UnmanagedArgs" ); + managedStructWriter.WriteLine( '{' ); managedStructWriter.Indent++; + managedStructWriter.WriteLine( "public IntPtr __Root;" ); + var managedStructBody = string.Join( "\r\n\t", methods.Select( x => $"public IntPtr __{x.Name}_{x.method.Name}MethodPtr;" ) ); managedStructWriter.Write( managedStructBody ); managedStructWriter.WriteLine(); managedStructWriter.Indent--; - managedStructWriter.WriteLine( $"}}" ); + managedStructWriter.WriteLine( '}' ); managedStructWriter.Dispose(); File.WriteAllText( $"{baseDir}/Mocha.Common/Glue/UnmanagedArgs.cs", baseManagedStructWriter.ToString() ); } - private static void WriteNativeStruct( string baseDir, ref List<(string Name, Method method)> methods ) + /// + /// Writes the C++ unmanaged arguments. + /// + /// The base directory that contains the source projects. + /// An enumerable list of all of the methods to write in the struct. + private static void WriteNativeStruct( string baseDir, IEnumerable<(string Name, Method method)> methods ) { var (baseNativeStructWriter, nativeStructWriter) = Utils.CreateWriter(); @@ -105,35 +136,43 @@ private static void WriteNativeStruct( string baseDir, ref List<(string Name, Me nativeStructWriter.WriteLine( "#include \"InteropList.generated.h\"" ); nativeStructWriter.WriteLine(); nativeStructWriter.WriteLine( "struct UnmanagedArgs" ); - nativeStructWriter.WriteLine( $"{{" ); + nativeStructWriter.WriteLine( '{' ); nativeStructWriter.Indent++; + nativeStructWriter.WriteLine( "void* __Root;" ); + var nativeStructBody = string.Join( "\r\n\t", methods.Select( x => $"void* __{x.Name}_{x.method.Name}MethodPtr;" ) ); nativeStructWriter.Write( nativeStructBody ); nativeStructWriter.WriteLine(); nativeStructWriter.Indent--; - nativeStructWriter.WriteLine( $"}};" ); + nativeStructWriter.WriteLine( "};" ); nativeStructWriter.WriteLine(); nativeStructWriter.WriteLine( "inline UnmanagedArgs args" ); - nativeStructWriter.WriteLine( $"{{" ); + nativeStructWriter.WriteLine( '{' ); nativeStructWriter.Indent++; + nativeStructWriter.WriteLine( "Root::GetInstance()," ); + nativeStructBody = string.Join( ",\r\n\t", methods.Select( x => $"(void*)__{x.Name}_{x.method.Name}" ) ); nativeStructWriter.Write( nativeStructBody ); nativeStructWriter.WriteLine(); nativeStructWriter.Indent--; - nativeStructWriter.WriteLine( $"}};" ); + nativeStructWriter.WriteLine( "};" ); nativeStructWriter.WriteLine(); - nativeStructWriter.WriteLine( $"#endif // __GENERATED_UNMANAGED_ARGS_H" ); + nativeStructWriter.WriteLine( "#endif // __GENERATED_UNMANAGED_ARGS_H" ); nativeStructWriter.Dispose(); File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\UnmanagedArgs.generated.h", baseNativeStructWriter.ToString() ); } + /// + /// Writes the C++ includes for the host project. + /// + /// The base directory that contains the source projects. private static void WriteNativeIncludes( string baseDir ) { var (baseNativeListWriter, nativeListWriter) = Utils.CreateWriter(); @@ -154,48 +193,56 @@ private static void WriteNativeIncludes( string baseDir ) File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\InteropList.generated.h", baseNativeListWriter.ToString() ); } - private static void DeleteExistingFiles( string baseDir ) + /// + /// Parses a header file and generates its C# and C++ interop code. + /// + /// The base directory that contains the source projects. + /// + /// A task that represents the asynchronous operation. + private static async Task ProcessHeaderAsync( string baseDir, string path ) { - var destCsDir = $"{baseDir}\\Mocha.Common\\Glue\\"; - var destHeaderDir = $"{baseDir}\\Mocha.Host\\generated\\"; + Log.ProcessingHeader( path ); - if ( Directory.Exists( destHeaderDir ) ) - Directory.Delete( destHeaderDir, true ); - if ( Directory.Exists( destCsDir ) ) - Directory.Delete( destCsDir, true ); + // Parse header. + var units = Parser.GetUnits( path ); - Directory.CreateDirectory( destHeaderDir ); - Directory.CreateDirectory( destCsDir ); - } + // Generate interop code. + var managedCode = ManagedCodeGenerator.GenerateCode( units ); + var relativePath = Path.GetRelativePath( $"{baseDir}/Mocha.Host/", path ); + var nativeCode = NativeCodeGenerator.GenerateCode( relativePath, units ); - public static void Main( string[] args ) - { - var baseDir = args[0]; - var start = DateTime.Now; + // Write interop code. + var fileName = Path.GetFileNameWithoutExtension( path ); + var csTask = File.WriteAllTextAsync( $"{baseDir}Mocha.Common\\Glue\\{fileName}.generated.cs", managedCode ); + var nativeTask = File.WriteAllTextAsync( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h", nativeCode ); - Console.WriteLine( "Generating C# <--> C++ interop code..." ); + // Wait for writing to finish. + await Task.WhenAll( csTask, nativeTask ); - // - // Prep - // - DeleteExistingFiles( baseDir ); - Parse( baseDir ); + s_files.Add( fileName ); + s_units.AddRange( units ); + } - // - // Expand methods out into list of (method name, method) - // - var methods = s_units.OfType().SelectMany( unit => unit.Methods, ( unit, method ) => (unit.Name, method) ).ToList(); + /// + /// Searches the directory for any header files that should be parsed. + /// + /// The queue collection to append to. + /// The absolute path to the directory to search for files. + private static void QueueDirectory( ICollection queue, string directory ) + { + foreach ( var file in Directory.GetFiles( directory, "*.h" ) ) + { + if ( file.EndsWith( ".generated.h" ) ) + continue; - // - // Write files - // - WriteManagedStruct( baseDir, ref methods ); - WriteNativeStruct( baseDir, ref methods ); - WriteNativeIncludes( baseDir ); + var fileContents = File.ReadAllText( file ); + if ( !fileContents.Contains( "GENERATE_BINDINGS", StringComparison.CurrentCultureIgnoreCase ) ) + continue; // Fast early bail + + queue.Add( file ); + } - // Track time & output total duration - var end = DateTime.Now; - var totalTime = end - start; - Console.WriteLine( $"-- Took {totalTime.TotalSeconds} seconds." ); + foreach ( var subDirectory in Directory.GetDirectories( directory ) ) + QueueDirectory( queue, subDirectory ); } } diff --git a/Source/MochaTool.InteropGen/Properties/launchSettings.json b/Source/MochaTool.InteropGen/Properties/launchSettings.json index 4443d61e..32179963 100644 --- a/Source/MochaTool.InteropGen/Properties/launchSettings.json +++ b/Source/MochaTool.InteropGen/Properties/launchSettings.json @@ -3,7 +3,7 @@ "Run InteropGen": { "commandName": "Project", "commandLineArgs": "$(SolutionDir)", - "workingDirectory": "F:\\Projects\\mocha-native\\Source\\MochaTool.InteropGen" + "workingDirectory": "$(ProjectDir)" } } } \ No newline at end of file diff --git a/Source/MochaTool.InteropGen/StopwatchLog.cs b/Source/MochaTool.InteropGen/StopwatchLog.cs new file mode 100644 index 00000000..e9ae7f09 --- /dev/null +++ b/Source/MochaTool.InteropGen/StopwatchLog.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.Extensions; +using System.Diagnostics; + +namespace MochaTool.InteropGen; + +/// +/// Represents a scoped logger for recording time taken to complete an operation. +/// +internal readonly struct StopwatchLog : IDisposable +{ + /// + /// The name of the operation being completed. + /// + private readonly string name = "Unknown"; + /// + /// The level at which to log the operation. + /// + private readonly LogLevel logLevel = LogLevel.Debug; + /// + /// The timestamp at which the operation started. + /// + private readonly long startTimestamp = Stopwatch.GetTimestamp(); + + /// + /// Initializes a new instance of . + /// + /// The name of the operation being completed. + /// The level at which to log the operation. + /// The timestamp at which the operation started. + internal StopwatchLog( string name, LogLevel logLevel = LogLevel.Debug, long? startTimestamp = null ) + { + this.name = name; + this.logLevel = logLevel; + this.startTimestamp = startTimestamp ?? Stopwatch.GetTimestamp(); + } + + /// + public void Dispose() + { + // Log the time taken to complete the operation. + Log.ReportTime( logLevel, name, Stopwatch.GetElapsedTime( startTimestamp ).TotalSeconds ); + } +} diff --git a/Source/MochaTool.InteropGen/ThreadDispatcher.cs b/Source/MochaTool.InteropGen/ThreadDispatcher.cs index 60fff598..2c46e44f 100644 --- a/Source/MochaTool.InteropGen/ThreadDispatcher.cs +++ b/Source/MochaTool.InteropGen/ThreadDispatcher.cs @@ -1,14 +1,26 @@ namespace MochaTool.InteropGen; -public class ThreadDispatcher +internal class ThreadDispatcher { - public delegate void ThreadCallback( List threadQueue ); + internal delegate void ThreadCallback( List threadQueue ); + internal delegate Task AsyncThreadCallback( List threadQueue ); - private int _threadCount = 16; + internal bool IsComplete => _threadsCompleted >= _threadCount; + + private int _threadCount = (int)Math.Ceiling( Environment.ProcessorCount * 0.75 ); private int _threadsCompleted = 0; - public bool IsComplete => _threadsCompleted == _threadCount; - public ThreadDispatcher( ThreadCallback threadStart, List queue ) + internal ThreadDispatcher( ThreadCallback threadStart, List queue ) + { + Setup( queue, threadQueue => threadStart( threadQueue ) ); + } + + internal ThreadDispatcher( AsyncThreadCallback threadStart, List queue ) + { + Setup( queue, threadQueue => threadStart( threadQueue ).Wait() ); + } + + private void Setup( List queue, Action> threadStart ) { var batchSize = queue.Count / _threadCount - 1; @@ -21,8 +33,7 @@ public ThreadDispatcher( ThreadCallback threadStart, List queue ) .Select( g => g.Select( p => p.Value ).ToList() ) .ToList(); - if ( batched.Count < _threadCount ) - _threadCount = batched.Count; // Min. 1 per thread + _threadCount = batched.Count; for ( int i = 0; i < batched.Count; i++ ) { @@ -30,7 +41,6 @@ public ThreadDispatcher( ThreadCallback threadStart, List queue ) var thread = new Thread( () => { threadStart( threadQueue ); - _threadsCompleted++; } ); diff --git a/Source/MochaTool.InteropGen/Units/Class.cs b/Source/MochaTool.InteropGen/Units/Class.cs deleted file mode 100644 index 7e436e65..00000000 --- a/Source/MochaTool.InteropGen/Units/Class.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Class : IUnit -{ - public Class( string name ) : this() - { - Name = name; - - Fields = new(); - Methods = new(); - } - - public string Name { get; set; } - public List Methods { get; set; } - public List Fields { get; set; } - public bool IsNamespace { get; set; } - - public override string ToString() - { - return Name; - } -} diff --git a/Source/MochaTool.InteropGen/Units/Field.cs b/Source/MochaTool.InteropGen/Units/Field.cs deleted file mode 100644 index e17a2781..00000000 --- a/Source/MochaTool.InteropGen/Units/Field.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Variable -{ - public Variable( string name, string type ) : this() - { - Name = name; - Type = type; - } - - public string Name { get; set; } - public string Type { get; set; } - - public override string ToString() - { - return $"{Type} {Name}"; - } -} diff --git a/Source/MochaTool.InteropGen/Units/IUnit.cs b/Source/MochaTool.InteropGen/Units/IUnit.cs deleted file mode 100644 index 71f88a6c..00000000 --- a/Source/MochaTool.InteropGen/Units/IUnit.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MochaTool.InteropGen; - -public interface IUnit -{ - public string Name { get; set; } - public List Fields { get; set; } - public List Methods { get; set; } -} diff --git a/Source/MochaTool.InteropGen/Units/Method.cs b/Source/MochaTool.InteropGen/Units/Method.cs deleted file mode 100644 index 92ffe8b9..00000000 --- a/Source/MochaTool.InteropGen/Units/Method.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Method -{ - public Method( string name, string returnType ) - { - Name = name; - ReturnType = returnType; - Parameters = new(); - } - - public bool IsConstructor { get; set; } = false; - public bool IsDestructor { get; set; } = false; - public bool IsStatic { get; set; } = false; - - public string Name { get; set; } - public string ReturnType { get; set; } - public List Parameters { get; set; } - - public override string ToString() - { - var p = string.Join( ", ", Parameters ); - return $"{ReturnType} {Name}( {p} )"; - } -} diff --git a/Source/MochaTool.InteropGen/Units/Structure.cs b/Source/MochaTool.InteropGen/Units/Structure.cs deleted file mode 100644 index 4e2f150f..00000000 --- a/Source/MochaTool.InteropGen/Units/Structure.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Structure : IUnit -{ - public Structure( string name ) : this() - { - Name = name; - - Fields = new(); - Methods = new(); - } - - public string Name { get; set; } - public List Methods { get; set; } - public List Fields { get; set; } - - public override string ToString() - { - return Name; - } -} diff --git a/Source/MochaTool.InteropGen/Utils.cs b/Source/MochaTool.InteropGen/Utils.cs index 5ad0120c..3bdb93b8 100644 --- a/Source/MochaTool.InteropGen/Utils.cs +++ b/Source/MochaTool.InteropGen/Utils.cs @@ -1,10 +1,64 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Extensions; +using System.CodeDom.Compiler; namespace MochaTool.InteropGen; -static class Utils +/// +/// Contains a number of utility methods. +/// +internal static class Utils { - public static string GetManagedType( string nativeType ) + /// + /// Used as a lookup table for mapping native types to managed ones. + /// + private static readonly Dictionary s_lookupTable = new() + { + // Native type Managed type + //------------------------------- + { "void", "void" }, + { "uint32_t", "uint" }, + { "int32_t", "int" }, + { "size_t", "uint" }, + + { "char**", "ref string" }, + { "char **", "ref string" }, + { "char*", "string" }, + { "char *", "string" }, + { "void*", "IntPtr" }, + { "void *", "IntPtr" }, + + // STL + { "std::string", "/* UNSUPPORTED */ string" }, + + // GLM + { "glm::vec2", "Vector2" }, + { "glm::vec3", "Vector3" }, + { "glm::mat4", "Matrix4x4" }, + { "glm::quat", "Rotation" }, + + // Custom + { "Quaternion", "Rotation" }, + { "InteropStruct", "IInteropArray" }, + { "Handle", "uint" } + }; + + /// + /// Returns whether or not the string represents a pointer. + /// + /// The native type to check. + /// Whether or not the string represents a pointer. + internal static bool IsPointer( string nativeType ) + { + var managedType = GetManagedType( nativeType ); + return nativeType.Trim().EndsWith( "*" ) && managedType != "string" && managedType != "IntPtr"; + } + + /// + /// Returns the C# version of a native type. + /// + /// The native type to check. + /// The C# verison of a native type. + internal static string GetManagedType( string nativeType ) { // Trim whitespace from beginning / end (if it exists) nativeType = nativeType.Trim(); @@ -13,52 +67,22 @@ public static string GetManagedType( string nativeType ) if ( nativeType.StartsWith( "const" ) ) nativeType = nativeType[5..].Trim(); - // Create a dictionary to hold the mapping between native and managed types - var lookupTable = new Dictionary() - { - // Native type Managed type - //------------------------------- - { "void", "void" }, - { "uint32_t", "uint" }, - { "size_t", "uint" }, - - { "char**", "ref string" }, - { "char **", "ref string" }, - { "char*", "string" }, - { "char *", "string" }, - { "void*", "IntPtr" }, - { "void *", "IntPtr" }, - - // STL - { "std::string", "/* UNSUPPORTED */ string" }, - - // GLM - { "glm::vec2", "Vector2" }, - { "glm::vec3", "Vector3" }, - { "glm::mat4", "Matrix4x4" }, - { "glm::quat", "Rotation" }, - - // Custom - { "Quaternion", "Rotation" }, - { "InteropStruct", "IInteropArray" }, - }; - // Check if the native type is a reference if ( nativeType.EndsWith( "&" ) ) return GetManagedType( nativeType[0..^1] ); // Check if the native type is in the lookup table - if ( lookupTable.ContainsKey( nativeType ) ) + if ( s_lookupTable.TryGetValue( nativeType, out var value ) ) { // Bonus: Emit a compiler warning if the native type is std::string if ( nativeType == "std::string" ) { // There's a better API that does this but I can't remember what it is // TODO: Show position of the warning (line number, file name) - Console.WriteLine( "warning IG0001: std::string is not supported in managed code. Use a C string instead." ); + Log.WarnDiagnostic( "warning IG0001: std::string is not supported in managed code. Use a C string instead." ); } - return lookupTable[nativeType]; + return value; } // Check if the native type is a pointer @@ -69,7 +93,11 @@ public static string GetManagedType( string nativeType ) return nativeType; } - public static (StringWriter StringWriter, IndentedTextWriter TextWriter) CreateWriter() + /// + /// Creates and returns the text writer for writing formatted files. + /// + /// The created text writer. + internal static (StringWriter StringWriter, IndentedTextWriter TextWriter) CreateWriter() { var baseTextWriter = new StringWriter(); diff --git a/Source/vcpkg.json b/Source/vcpkg.json index 06912efd..2fdf0a72 100644 --- a/Source/vcpkg.json +++ b/Source/vcpkg.json @@ -17,7 +17,8 @@ }, "sdl2-image", "spdlog", - "vulkan-memory-allocator" + "vulkan-memory-allocator", + "gamenetworkingsockets" ], "overrides": [ {