diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 45242d98e5..27782be9e4 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -33,6 +33,10 @@ on: default: Debug type: string +concurrency: + group: build-android-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Stride Runtime for Android diff --git a/.github/workflows/build-assembly-processor.yml b/.github/workflows/build-assembly-processor.yml index 9a00e4194e..6eda02accf 100644 --- a/.github/workflows/build-assembly-processor.yml +++ b/.github/workflows/build-assembly-processor.yml @@ -29,6 +29,10 @@ on: default: Debug type: string +concurrency: + group: build-assembly-processor-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Assembly Processor diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 9ada33e0a1..7a713efef6 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -33,6 +33,10 @@ on: default: Debug type: string +concurrency: + group: build-ios-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Stride Runtime for iOS diff --git a/.github/workflows/build-launcher.yml b/.github/workflows/build-launcher.yml index bcced9d613..11258e4aa2 100644 --- a/.github/workflows/build-launcher.yml +++ b/.github/workflows/build-launcher.yml @@ -31,6 +31,10 @@ on: default: Debug type: string +concurrency: + group: build-launcher-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Stride Runtime for Windows diff --git a/.github/workflows/build-linux-runtime.yml b/.github/workflows/build-linux-runtime.yml index 531768b08f..baea1644d8 100644 --- a/.github/workflows/build-linux-runtime.yml +++ b/.github/workflows/build-linux-runtime.yml @@ -43,6 +43,10 @@ on: default: OpenGL type: string +concurrency: + group: build-linux-runtime-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}-${{ github.event.inputs.graphics-api || inputs.graphics-api || 'OpenGL' }} + cancel-in-progress: true + jobs: # # Build Stride Runtime for Linux diff --git a/.github/workflows/build-vs-package.yml b/.github/workflows/build-vs-package.yml index f89b051ad0..03427c189f 100644 --- a/.github/workflows/build-vs-package.yml +++ b/.github/workflows/build-vs-package.yml @@ -28,6 +28,10 @@ on: default: Debug type: string +concurrency: + group: build-vs-package-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Visual Studio Package diff --git a/.github/workflows/build-windows-full.yml b/.github/workflows/build-windows-full.yml index c1f6ce681e..d18fc15037 100644 --- a/.github/workflows/build-windows-full.yml +++ b/.github/workflows/build-windows-full.yml @@ -28,6 +28,10 @@ on: default: Debug type: string +concurrency: + group: build-windows-full-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} + cancel-in-progress: true + jobs: # # Build Stride for Windows diff --git a/.github/workflows/build-windows-runtime.yml b/.github/workflows/build-windows-runtime.yml index 6d0ded05a7..60008aef7f 100644 --- a/.github/workflows/build-windows-runtime.yml +++ b/.github/workflows/build-windows-runtime.yml @@ -46,6 +46,10 @@ on: default: Direct3D11 type: string +concurrency: + group: build-windows-runtime-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}-${{ github.event.inputs.graphics-api || inputs.graphics-api || 'Direct3D11' }} + cancel-in-progress: true + jobs: # # Build Stride Runtime for Windows diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35813c2aba..f940f82411 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,10 @@ on: - '!crowdin.yml' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: ### Misc. ### Assembly-Processor: diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 551819bcce..2124c3af7a 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -26,13 +26,17 @@ on: default: Simple type: string +concurrency: + group: test-linux-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}-${{ github.event.inputs.test-category || inputs.test-category || 'Simple' }} + cancel-in-progress: true + jobs: # # Test Stride on Linux # Linux-Tests: if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event.pull_request.draft == false }} - name: Test (${{ github.event.inputs.build-type || inputs.build-type }}, ${{ github.event.inputs.test-category || inputs.test-category }}) + name: Test (${{ github.event.inputs.build-type || inputs.build-type }}, ${{ github.event.inputs.test-category || inputs.test-category || 'Simple' }}) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -43,7 +47,7 @@ jobs: dotnet-version: '8.0.x' - name: Build run: | - dotnet build build\Stride.Tests.${{ github.event.inputs.test-category || inputs.test-category }}.slnf ` + dotnet build build\Stride.Tests.${{ github.event.inputs.test-category || inputs.test-category || 'Simple' }}.slnf ` -restore -m:1 -nr:false ` -v:m -p:WarningLevel=0 ` -p:Configuration=${{ github.event.inputs.build-type || inputs.build-type }} ` @@ -51,6 +55,6 @@ jobs: -p:StrideGraphicsApis=OpenGL - name: Test run: | - dotnet test build\Stride.Tests.${{ github.event.inputs.test-category || inputs.test-category }}.slnf ` + dotnet test build\Stride.Tests.${{ github.event.inputs.test-category || inputs.test-category || 'Simple' }}.slnf ` --no-build ` -p:Configuration=${{ github.event.inputs.build-type || inputs.build-type }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 449a83eb6e..8d6136c078 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -39,6 +39,10 @@ on: default: Simple type: string +concurrency: + group: test-windows-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}-${{ github.event.inputs.test-category || inputs.test-category || 'Simple' }} + cancel-in-progress: true + jobs: # # Test Stride on Windows diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props index 28607e25a4..6368b59dc8 100644 --- a/sources/Directory.Packages.props +++ b/sources/Directory.Packages.props @@ -11,6 +11,7 @@ + diff --git a/sources/buildengine/Stride.Core.BuildEngine.Common/MicrothreadLocalDatabases.cs b/sources/buildengine/Stride.Core.BuildEngine.Common/MicrothreadLocalDatabases.cs index 8c892bea0e..1a99c7f97f 100644 --- a/sources/buildengine/Stride.Core.BuildEngine.Common/MicrothreadLocalDatabases.cs +++ b/sources/buildengine/Stride.Core.BuildEngine.Common/MicrothreadLocalDatabases.cs @@ -113,6 +113,11 @@ private static DatabaseFileProvider CreateDatabase(BuildTransaction transaction) private class MicroThreadLocalProviderService : IDatabaseFileProviderService { - public DatabaseFileProvider FileProvider => MicroThreadLocalDatabaseFileProvider.Value; + public DatabaseFileProvider FileProvider + { + get => MicroThreadLocalDatabaseFileProvider.Value; + set => MicroThreadLocalDatabaseFileProvider.Value = value; + //throw new InvalidOperationException($"Can not change the value of a {nameof(MicroThreadLocalProviderService.FileProvider)}"); + } } } diff --git a/sources/core/Stride.Core.Serialization/IO/IDatabaseFileProviderService.cs b/sources/core/Stride.Core.Serialization/IO/IDatabaseFileProviderService.cs index 93c36db2f8..7d251cd2c2 100644 --- a/sources/core/Stride.Core.Serialization/IO/IDatabaseFileProviderService.cs +++ b/sources/core/Stride.Core.Serialization/IO/IDatabaseFileProviderService.cs @@ -5,5 +5,5 @@ namespace Stride.Core.IO; public interface IDatabaseFileProviderService { - DatabaseFileProvider FileProvider { get; } -} \ No newline at end of file + DatabaseFileProvider FileProvider { get; set; } +} diff --git a/sources/core/Stride.Core.Serialization/Serialization/Contents/IContentManager.cs b/sources/core/Stride.Core.Serialization/Serialization/Contents/IContentManager.cs index baf4983438..d495e40f7c 100644 --- a/sources/core/Stride.Core.Serialization/Serialization/Contents/IContentManager.cs +++ b/sources/core/Stride.Core.Serialization/Serialization/Contents/IContentManager.cs @@ -25,6 +25,19 @@ public interface IContentManager /// A stream to the raw asset. Stream OpenAsStream(string url, StreamFlags streamFlags = StreamFlags.None); + /// + /// Saves an asset at a specific URL. + /// + /// The URL. + /// The asset. + /// The custom storage type to use. Use null as default. + /// + /// url + /// or + /// asset + /// + void Save(string url, object asset, Type? storageType); + /// /// Loads content from the specified URL. /// diff --git a/sources/core/Stride.Core/IServiceRegistry.cs b/sources/core/Stride.Core/IServiceRegistry.cs index c2b564561a..9f25c94df1 100644 --- a/sources/core/Stride.Core/IServiceRegistry.cs +++ b/sources/core/Stride.Core/IServiceRegistry.cs @@ -47,6 +47,15 @@ public interface IServiceRegistry /// Thrown when a service of the same type is already registered. void AddService(T service) where T : class; + /// + /// Adds a service to this . + /// + /// The service to add. + /// The type to register as. + /// Thrown when the provided service is null. + /// Thrown when a service of the same type is already registered. + void AddService(object service, Type type); + /// /// Gets the service object of the specified type. /// diff --git a/sources/core/Stride.Core/ServiceRegistry.cs b/sources/core/Stride.Core/ServiceRegistry.cs index 09b7543516..ca5dcede37 100644 --- a/sources/core/Stride.Core/ServiceRegistry.cs +++ b/sources/core/Stride.Core/ServiceRegistry.cs @@ -76,6 +76,22 @@ public void AddService(T service) OnServiceAdded(new ServiceEventArgs(type, service)); } + /// + /// + /// This implementation triggers the event after a service is successfully added. + /// + public void AddService(object service, Type type) + { + ArgumentNullException.ThrowIfNull(service); + + lock (registeredService) + { + if (!registeredService.TryAdd(type, service)) + throw new ArgumentException("Service is already registered with this type", nameof(type)); + } + OnServiceAdded(new ServiceEventArgs(type, service)); + } + /// /// /// This implementation triggers the event after a service is successfully removed. diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs index 589ceaeccb..b0059ad35d 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs @@ -48,7 +48,8 @@ public abstract class EntityHierarchyEditorGame : EditorServiceGame private Material fallbackColorMaterial; private Material fallbackTextureMaterial; - protected EntityHierarchyEditorGame(TaskCompletionSource gameContentLoadedTaskSource, IEffectCompiler effectCompiler, string effectLogPath) + protected EntityHierarchyEditorGame(TaskCompletionSource gameContentLoadedTaskSource, IEffectCompiler effectCompiler, string effectLogPath, GameContext context = null) + : base(context) { this.gameContentLoadedTaskSource = gameContentLoadedTaskSource; this.effectCompiler = effectCompiler; diff --git a/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs b/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs index bd04fdc2b0..25a80860c5 100644 --- a/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs +++ b/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs @@ -99,6 +99,11 @@ public bool IsEditorHidden public event EventHandler ExceptionThrown; + public EditorServiceGame(GameContext context) : base(context) + { + + } + /// /// Calculates and returns the position of the mouse in the scene. /// diff --git a/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs b/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs index 5eab6d24fc..66d262c9c3 100644 --- a/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs +++ b/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs @@ -3,6 +3,7 @@ using Stride.Core.Diagnostics; using Stride.Engine; +using Stride.Games; using Stride.Graphics; namespace Stride.Editor.Engine @@ -17,7 +18,7 @@ public class EmbeddedGame : Game /// public static bool DebugMode { get; set; } - public EmbeddedGame() + public EmbeddedGame(GameContext context) : base(context) { GraphicsDeviceManager.PreferredGraphicsProfile = new [] { GraphicsProfile.Level_11_0, GraphicsProfile.Level_10_1, GraphicsProfile.Level_10_0 }; GraphicsDeviceManager.PreferredBackBufferWidth = 64; diff --git a/sources/editor/Stride.Editor/Preview/GameStudioPreviewService.cs b/sources/editor/Stride.Editor/Preview/GameStudioPreviewService.cs index 522b71f911..99047e09fa 100644 --- a/sources/editor/Stride.Editor/Preview/GameStudioPreviewService.cs +++ b/sources/editor/Stride.Editor/Preview/GameStudioPreviewService.cs @@ -137,8 +137,8 @@ private void StrideUIThread() initializationSignal.Set(); - PreviewGame = new PreviewGame(AssetBuilderService.EffectCompiler); var context = new GameContextWinforms(gameForm) { InitializeDatabase = false }; + PreviewGame = new PreviewGame(AssetBuilderService.EffectCompiler, context); // Wait for shaders to be loaded AssetBuilderService.WaitForShaders(); diff --git a/sources/editor/Stride.Editor/Preview/PreviewGame.cs b/sources/editor/Stride.Editor/Preview/PreviewGame.cs index c88ac25032..b7a5a8c9f1 100644 --- a/sources/editor/Stride.Editor/Preview/PreviewGame.cs +++ b/sources/editor/Stride.Editor/Preview/PreviewGame.cs @@ -2,15 +2,16 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Threading.Tasks; -using Stride.Core.BuildEngine; -using Stride.Core; -using Stride.Core.Diagnostics; -using Stride.Core.Mathematics; using Stride.Assets; using Stride.Assets.SpriteFont; using Stride.Assets.SpriteFont.Compiler; +using Stride.Core; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.Mathematics; using Stride.Engine; using Stride.Engine.Design; +using Stride.Games; using Stride.Graphics; using Stride.Rendering.Compositing; using Stride.Shaders.Compiler; @@ -43,7 +44,7 @@ public class PreviewGame : EditorGame.Game.EditorServiceGame private Scene previewScene; - public PreviewGame(IEffectCompiler effectCompiler) + public PreviewGame(IEffectCompiler effectCompiler, GameContext context) : base(context) { this.effectCompiler = effectCompiler; } diff --git a/sources/engine/Stride.Debugger/Debugger/LiveAssemblyReloader.cs b/sources/engine/Stride.Debugger/Debugger/LiveAssemblyReloader.cs index 67c314e85f..bb4d910626 100644 --- a/sources/engine/Stride.Debugger/Debugger/LiveAssemblyReloader.cs +++ b/sources/engine/Stride.Debugger/Debugger/LiveAssemblyReloader.cs @@ -1,29 +1,30 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Stride.Core; using Stride.Core.Reflection; -using Stride.Core.Serialization; using Stride.Core.Yaml; using Stride.Core.Yaml.Events; using Stride.Core.Yaml.Serialization; using Stride.Debugger.Target; using Stride.Engine; +using Stride.Games; namespace Stride.Debugger { public static class LiveAssemblyReloader { - public static void Reload(Game game, AssemblyContainer assemblyContainer, List assembliesToUnregister, List assembliesToRegister) + public static void Reload(GameBase game, AssemblyContainer assemblyContainer, List assembliesToUnregister, List assembliesToRegister) { List entities = new List(); + var sceneSystem = game.Services.GetSafeServiceAs(); + if (game != null) - entities.AddRange(game.SceneSystem.SceneInstance); + entities.AddRange(sceneSystem.SceneInstance); CloneReferenceSerializer.References = new List(); diff --git a/sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs b/sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs new file mode 100644 index 0000000000..5c9f60db0c --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Stride.Core; +using Stride.Core.Diagnostics; +using Stride.Games; +using Stride.Input; + +namespace Stride.Engine.Builder; + +/// +/// Helps build the game and preps it to be able to run after . +/// +/// +public class GameBuilder : IGameBuilder +{ + /// + /// This is used to allow the same instance to be registered multiple times as differenet interfaces or types. This was done due to how works."/> + /// + public Dictionary InternalServices { get; internal set; } = []; + + /// + /// This allows for Service to be registered through DI. + /// + public IServiceCollection Services { get; internal set; } = new ServiceCollection(); + + /// + /// This is a direct reference to the game systems collection of the . + /// + public GameSystemCollection GameSystems { get; internal set; } + + /// + /// Adds log listeners to the game on . This is registered first so it will log build errors if they occur."/> + /// + public List LogListeners { get; internal set; } = []; + + /// + /// Adds input sources to the game on . + /// + public List InputSources { get; internal set; } = []; + + public GameBase Game { get; set; } + + public GameContext Context { get; set; } + + private static Logger _log => GlobalLogger.GetLogger(nameof(GameBuilder)); + + internal GameBuilder(GameBase game) + { + Game = game ?? new MinimalGame(null); + GameSystems = Game.GameSystems; + Services.AddSingleton(Game.Services); + InternalServices.Add(typeof(IServiceRegistry), Game.Services); + } + + /// + /// Creates a new instance of the class. + /// + /// + public static GameBuilder Create(GameBase game = null) + { + return new GameBuilder(game); + } + + public virtual GameBase Build() + { + foreach (var logListener in LogListeners) + { + GlobalLogger.GlobalMessageLogged += logListener; + } + + var provider = Services.BuildServiceProvider(); + foreach (var service in InternalServices) + { + if (service.Key == typeof(IServiceRegistry) || service.Key == typeof(IServiceProvider)) + continue; + + try + { + if (service.Value == null) + { + var instance = provider.GetService(service.Key); + + if(instance == null) + { + //check if the type is inherited from another instance in the services. + foreach (var kvp in InternalServices) + { + if (kvp.Key.IsAssignableFrom(service.Key) && kvp.Value != null) + { + instance = provider.GetService(kvp.Key); + if(instance is not null) + break; + } + } + } + + _log.Info($"Registering service {service.Key.Name}."); + Game.Services.AddService(instance, service.Key); + InternalServices[service.Key] = instance; + } + else + { + _log.Info($"Registering service {service.Key.Name}."); + Game.Services.AddService(service.Value, service.Key); + } + } + catch (Exception ex) + { + // TODO: check if service is already registered first. + _log.Error($"Failed to register service {service.Key.Name}.\n\n", ex); + } + } + + // Add all game systems to the game. + foreach (var service in InternalServices) + { + var system = provider.GetService(service.Key); + if (system is IGameSystemBase gameSystem && !Game.GameSystems.Contains(gameSystem)) + { + _log.Info($"Adding game system {gameSystem.GetType().Name} to the game systems collection."); + Game.GameSystems.Add(gameSystem); + } + } + + if (Context != null) + { + _log.Info($"Setting game context."); + Game.SetGameContext(Context); + } + + if(InputSources.Count > 0) + { + var inputManager = Game.Services.GetService(); + + if (inputManager is null) + { + _log.Info("No InputManager found in the game services, creating default."); + inputManager = new InputManager(); + Game.Services.AddService(inputManager); + } + + foreach (var inputSource in InputSources) + { + _log.Info($"Adding input source {inputSource.GetType().Name} to the input manager."); + inputManager.Sources.Add(inputSource); + } + } + + return Game; + } +} diff --git a/sources/engine/Stride.Engine/Engine/Builder/GameBuilderExtensions.cs b/sources/engine/Stride.Engine/Engine/Builder/GameBuilderExtensions.cs new file mode 100644 index 0000000000..200289b29c --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/Builder/GameBuilderExtensions.cs @@ -0,0 +1,191 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Stride.Core; +using Stride.Core.Diagnostics; +using Stride.Core.IO; +using Stride.Core.Serialization.Contents; +using Stride.Core.Storage; +using Stride.Games; +using Stride.Input; +using Stride.Rendering; +using Stride.Shaders.Compiler; + +namespace Stride.Engine.Builder; +public static class GameBuilderExtensions +{ + + /// + /// Adds core systems to the game. Does not register the systems into the + /// + /// + /// + /// + /// + public static IGameBuilder AddGameSystem(this IGameBuilder gameBuilder, T gameSystem) where T : IGameSystemBase + { + gameBuilder.GameSystems.Add(gameSystem); + return gameBuilder; + } + + /// + /// Registers a service into the . + /// + /// + /// + /// + /// + public static IGameBuilder AddService(this IGameBuilder gameBuilder, T service) where T : class + { + gameBuilder.InternalServices.Add(typeof(T), service); + gameBuilder.Services.AddSingleton(service); + return gameBuilder; + } + + /// + /// Registers a service into the . + /// + /// + /// + /// + public static IGameBuilder AddService(this IGameBuilder gameBuilder) where T : class + { + gameBuilder.InternalServices.Add(typeof(T), null); + gameBuilder.Services.AddSingleton(); + return gameBuilder; + } + + /// + /// Registers a service and its interface into the . + /// + /// + /// + /// + /// + public static IGameBuilder AddService(this IGameBuilder gameBuilder) where TClass : class, TInterface where TInterface : class + { + // This is a work around to allow DI to work the same way as the ServiceRegistry expects. + // Without registering both the interface and the class, the DI will not be able to resolve the interface on build. + gameBuilder.InternalServices.Add(typeof(TInterface), null); + gameBuilder.InternalServices.Add(typeof(TClass), null); + gameBuilder.Services.AddSingleton(); + return gameBuilder; + } + + /// + /// Adds a log listener to the game. This is used thoughout Stride systems for logging events. + /// + /// + /// + /// + public static IGameBuilder AddLogListener(this IGameBuilder gameBuilder, LogListener logListener) + { + gameBuilder.LogListeners.Add(logListener); + return gameBuilder; + } + + /// + /// Adds the Stride input system to the game with no sources. + /// + /// + /// + public static IGameBuilder UseStrideInput(this IGameBuilder gameBuilder) + { + var services = gameBuilder.InternalServices[typeof(IServiceRegistry)] as IServiceRegistry; + + var inputSystem = new InputSystem(services); + + gameBuilder + .AddGameSystem(inputSystem) + .AddService(inputSystem) + .AddService(inputSystem.Manager); + + return gameBuilder; + } + + public static IGameBuilder SetGameContext(this IGameBuilder gameBuilder, GameContext context) + { + gameBuilder.Context = context; + return gameBuilder; + } + + /// + /// Add a custom database file provider to the game. + /// + /// + /// + /// + public static IGameBuilder SetDbFileProvider(this IGameBuilder gameBuilder, DatabaseFileProvider provider) + { + // Gets initialized by the GameBase constructor. + var fileProviderService = gameBuilder.Game.Services.GetService(); + + fileProviderService.FileProvider = provider; + return gameBuilder; + } + + /// + /// Creates a default database to be used in the game. + /// + /// + /// + public static IGameBuilder UseDefaultDb(this IGameBuilder gameBuilder) + { + using (Profiler.Begin(GameProfilingKeys.ObjectDatabaseInitialize)) + { + // Create and mount database file system + var objDatabase = ObjectDatabase.CreateDefaultDatabase(); + + // Only set a mount path if not mounted already + var mountPath = VirtualFileSystem.ResolveProviderUnsafe("/asset", true).Provider == null ? "/asset" : null; + var result = new DatabaseFileProvider(objDatabase, mountPath); + + gameBuilder.SetDbFileProvider(result); + } + + return gameBuilder; + } + + public static IGameBuilder UseDefaultContentManager(this IGameBuilder gameBuilder) + { + var services = gameBuilder.Game.Services; + var content = new ContentManager(services); + services.AddService(content); + services.AddService(content); + return gameBuilder; + } + + /// + /// Adds a default effect compiler to the game. This is used to compile shaders and effects. + /// + /// + /// + /// + /// + public static EffectSystem CreateDefaultEffectCompiler(this EffectSystem effectSystem, IVirtualFileProvider fileProvider) + { + EffectCompilerBase compiler = new EffectCompiler(fileProvider) + { + SourceDirectories = { EffectCompilerBase.DefaultSourceShaderFolder }, + }; + + if(fileProvider is DatabaseFileProvider databaseFileProvider) + { + effectSystem.Compiler = new EffectCompilerCache(compiler, databaseFileProvider); + return effectSystem; + } + + throw new ArgumentException("The file provider must be a DatabaseFileProvider", nameof(fileProvider)); + } + + /// + /// Adds an input source to the game. This requires the Stride input system to be used. + /// + /// + /// + /// + public static IGameBuilder AddStrideInputSource(this IGameBuilder gameBuilder, IInputSource inputSource) + { + gameBuilder.InputSources.Add(inputSource); + return gameBuilder; + } +} diff --git a/sources/engine/Stride.Engine/Engine/Builder/IGameBuilder.cs b/sources/engine/Stride.Engine/Engine/Builder/IGameBuilder.cs new file mode 100644 index 0000000000..eba6f13b5b --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/Builder/IGameBuilder.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Stride.Core.Diagnostics; +using Stride.Games; +using System; +using Microsoft.Extensions.DependencyInjection; +using Stride.Input; +using Stride.Core.IO; + +namespace Stride.Engine.Builder; +public interface IGameBuilder +{ + Dictionary InternalServices { get; } + + IServiceCollection Services { get; } + + GameSystemCollection GameSystems { get; } + + List LogListeners { get; } + + List InputSources { get; } + + GameBase Game { get; set; } + + GameContext Context { get; set; } +} diff --git a/sources/engine/Stride.Engine/Engine/Builder/MinimalGame.cs b/sources/engine/Stride.Engine/Engine/Builder/MinimalGame.cs new file mode 100644 index 0000000000..ec51270191 --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/Builder/MinimalGame.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Stride.Core.Diagnostics; +using Stride.Core.Serialization; +using Stride.Core.Serialization.Contents; +using Stride.Games; +using Stride.Graphics; +using Stride.Input; + +namespace Stride.Engine.Builder; + +/// +/// A game class with no registered systems by default. +/// +public class MinimalGame : GameBase, IHostedService +{ + + /// + /// Gets the graphics device manager. + /// + /// The graphics device manager. + public GraphicsDeviceManager GraphicsDeviceManager { get; internal set; } + + public MinimalGame(GameContext gameContext) : base() + { + Context = gameContext ?? GetDefaultContext(); + Context.CurrentGame = this; + + // Create Platform + Context.GamePlatform = GamePlatform.Create(Context); + Context.GamePlatform.Activated += GamePlatform_Activated; + Context.GamePlatform.Deactivated += GamePlatform_Deactivated; + Context.GamePlatform.Exiting += GamePlatform_Exiting; + Context.GamePlatform.WindowCreated += GamePlatformOnWindowCreated; + + // Setup registry + Services.AddService(this); + Services.AddService(Context.GamePlatform); + Services.AddService(Context.GamePlatform); + + // Creates the graphics device manager + GraphicsDeviceManager = new GraphicsDeviceManager(this); + Services.AddService(GraphicsDeviceManager); + Services.AddService(GraphicsDeviceManager); + } + + public override void ConfirmRenderingSettings(bool gameCreation) + { + var deviceManager = (GraphicsDeviceManager)graphicsDeviceManager; + + if (gameCreation) + { + //if our device width or height is actually smaller then requested we use the device one + deviceManager.PreferredBackBufferWidth = Context.RequestedWidth = Math.Min(deviceManager.PreferredBackBufferWidth, Window.ClientBounds.Width); + deviceManager.PreferredBackBufferHeight = Context.RequestedHeight = Math.Min(deviceManager.PreferredBackBufferHeight, Window.ClientBounds.Height); + } + } + + protected override void Initialize() + { + base.Initialize(); + + Content.Serializer.LowLevelSerializerSelector = new SerializerSelector("Default", "Content"); + + // Add window specific input source + var inputManager = Services.GetService(); + if (inputManager is not null) + { + var windowInputSource = InputSourceFactory.NewWindowInputSource(Context); + inputManager.Sources.Add(windowInputSource); + } + } + + protected override void PrepareContext() + { + //Allow the user to add their own ContentManager + var contentManager = Services.GetService(); + + if (contentManager is null) + { + Log.Info("No ContentManager found, creating default ContentManager"); + contentManager = new ContentManager(Services); + Services.AddService(contentManager); + Services.AddService(contentManager); + } + + Content = contentManager; + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + Run(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + Exit(); + return Task.CompletedTask; + } + +} diff --git a/sources/engine/Stride.Engine/Engine/Design/GameSettings.cs b/sources/engine/Stride.Engine/Engine/Design/GameSettings.cs index a012e43d9d..7bfd6fab92 100644 --- a/sources/engine/Stride.Engine/Engine/Design/GameSettings.cs +++ b/sources/engine/Stride.Engine/Engine/Design/GameSettings.cs @@ -1,12 +1,10 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System; using Stride.Core; using Stride.Core.Mathematics; using Stride.Core.Serialization.Contents; using Stride.Data; -using Stride.Graphics; namespace Stride.Engine.Design { diff --git a/sources/engine/Stride.Engine/Engine/Game.cs b/sources/engine/Stride.Engine/Engine/Game.cs index 6c4c162ed5..7064d566e6 100644 --- a/sources/engine/Stride.Engine/Engine/Game.cs +++ b/sources/engine/Stride.Engine/Engine/Game.cs @@ -188,8 +188,23 @@ public LogMessageType ConsoleLogLevel /// /// Initializes a new instance of the class. /// - public Game() + public Game(GameContext context = null) : base() { + Context = context ?? GetDefaultContext(); + Context.CurrentGame = this; + + // Create Platform + Context.GamePlatform = GamePlatform.Create(Context); + Context.GamePlatform.Activated += GamePlatform_Activated; + Context.GamePlatform.Deactivated += GamePlatform_Deactivated; + Context.GamePlatform.Exiting += GamePlatform_Exiting; + Context.GamePlatform.WindowCreated += GamePlatformOnWindowCreated; + + // Setup registry + Services.AddService(this); + Services.AddService(Context.GamePlatform); + Services.AddService(Context.GamePlatform); + // Register the logger backend before anything else logListener = GetLogListener(); @@ -205,6 +220,7 @@ public Game() Services.AddService(SceneSystem); Streaming = new StreamingManager(Services); + Services.AddService(Streaming); Audio = new AudioSystem(Services); Services.AddService(Audio); @@ -256,7 +272,7 @@ protected override void PrepareContext() if (Context.InitializeDatabase) { databaseFileProvider = InitializeAssetDatabase(); - ((DatabaseFileProviderService)Services.GetService()).FileProvider = databaseFileProvider; + Services.GetService().FileProvider = databaseFileProvider; var renderingSettings = new RenderingSettings(); if (Content.Exists(GameSettings.AssetUrl)) @@ -342,6 +358,7 @@ protected override void Initialize() Input = inputSystem.Manager; Services.AddService(Input); GameSystems.Add(inputSystem); + SetInitialInputSources(Input); // Initialize the systems base.Initialize(); @@ -393,6 +410,49 @@ protected override void Initialize() OnGameStarted(this); } + private void SetInitialInputSources(InputManager inputManager) + { + // Add window specific input source + var windowInputSource = InputSourceFactory.NewWindowInputSource(Context); + inputManager.Sources.Add(windowInputSource); + + // Add platform specific input sources + switch (Context.ContextType) + { +#if STRIDE_UI_SDL + case AppContextType.DesktopSDL: + break; +#endif +#if STRIDE_PLATFORM_ANDROID + case AppContextType.Android: + break; +#endif +#if STRIDE_PLATFORM_IOS + case AppContextType.iOS: + break; +#endif +#if STRIDE_PLATFORM_UWP + case AppContextType.UWPXaml: + case AppContextType.UWPCoreWindow: + break; +#endif + case AppContextType.Desktop: +#if (STRIDE_UI_WINFORMS || STRIDE_UI_WPF) + inputManager.Sources.Add(new InputSourceWindowsDirectInput()); + if (InputSourceWindowsXInput.IsSupported()) + inputManager.Sources.Add(new InputSourceWindowsXInput()); +#if STRIDE_INPUT_RAWINPUT + if (rawInputEnabled && context is GameContextWinforms gameContextWinforms) + inputManager.Sources.Add(new InputSourceWindowsRawInput(gameContextWinforms.Control)); +#endif +#endif + break; + default: + Log.Warning("GameContext type is not supported by the InputManager. Register your own for input to be handled properly."); + break; + } + } + internal static DatabaseFileProvider InitializeAssetDatabase() { using (Profiler.Begin(GameProfilingKeys.ObjectDatabaseInitialize)) diff --git a/sources/engine/Stride.Engine/Engine/GameSystem.cs b/sources/engine/Stride.Engine/Engine/GameSystem.cs index 3ea3a8f9ec..545609b8c9 100644 --- a/sources/engine/Stride.Engine/Engine/GameSystem.cs +++ b/sources/engine/Stride.Engine/Engine/GameSystem.cs @@ -19,6 +19,6 @@ protected GameSystem(IServiceRegistry registry) : base(registry) /// /// The game. /// This value can be null - public new Game Game => (Game)base.Game; + public new GameBase Game => base.Game; } } diff --git a/sources/engine/Stride.Engine/Stride.Engine.csproj b/sources/engine/Stride.Engine/Stride.Engine.csproj index 7bded5b689..7836b61cf4 100644 --- a/sources/engine/Stride.Engine/Stride.Engine.csproj +++ b/sources/engine/Stride.Engine/Stride.Engine.csproj @@ -26,6 +26,7 @@ + diff --git a/sources/engine/Stride.Games/Android/GamePlatformAndroid.cs b/sources/engine/Stride.Games/Android/GamePlatformAndroid.cs index 167afa863d..5db252837c 100644 --- a/sources/engine/Stride.Games/Android/GamePlatformAndroid.cs +++ b/sources/engine/Stride.Games/Android/GamePlatformAndroid.cs @@ -28,7 +28,7 @@ private void PopulateFullName() FullName = $"{manufacturer} - {model}"; } - public GamePlatformAndroid(GameBase game) : base(game) + public GamePlatformAndroid(GameContext context) : base(context) { PopulateFullName(); } diff --git a/sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs b/sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs index 7d48fae49e..c3911ba734 100644 --- a/sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs +++ b/sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs @@ -29,7 +29,7 @@ namespace Stride.Games { internal class GamePlatformDesktop : GamePlatform { - public GamePlatformDesktop(GameBase game) : base(game) + public GamePlatformDesktop(GameContext context) : base(context) { IsBlockingRun = true; #if (STRIDE_UI_WINFORMS || STRIDE_UI_WPF) diff --git a/sources/engine/Stride.Games/GameBase.cs b/sources/engine/Stride.Games/GameBase.cs index 9fd977372d..9cb2262684 100644 --- a/sources/engine/Stride.Games/GameBase.cs +++ b/sources/engine/Stride.Games/GameBase.cs @@ -41,7 +41,7 @@ public abstract class GameBase : ComponentBase, IGame { #region Fields - private readonly GamePlatform gamePlatform; + private GamePlatform gamePlatform => Context.GamePlatform; private IGraphicsDeviceService graphicsDeviceService; protected IGraphicsDeviceManager graphicsDeviceManager; private ResumeManager resumeManager; @@ -59,7 +59,7 @@ public abstract class GameBase : ComponentBase, IGame private bool isMouseVisible; - internal object TickLock = new object(); + internal object TickLock = new(); #endregion @@ -95,21 +95,23 @@ protected GameBase() GameSystems = new GameSystemCollection(Services); Services.AddService(GameSystems); - // Create Platform - gamePlatform = GamePlatform.Create(this); - gamePlatform.Activated += GamePlatform_Activated; - gamePlatform.Deactivated += GamePlatform_Deactivated; - gamePlatform.Exiting += GamePlatform_Exiting; - gamePlatform.WindowCreated += GamePlatformOnWindowCreated; - - // Setup registry - Services.AddService(this); - Services.AddService(gamePlatform); - Services.AddService(gamePlatform); - IsActive = true; } + protected static GameContext GetDefaultContext() + { +#if STRIDE_PLATFORM_UWP + return GameContextFactory.NewGameContextUWPXaml(); +#elif STRIDE_PLATFORM_ANDROID + return GameContextFactory.NewGameContextAndroid(); +#elif STRIDE_PLATFORM_IOS + return GameContextFactory.NewGameContextiOS(); +#else + // Here we cover all Desktop variants: OpenTK, SDL, Winforms,... + return GameContextFactory.NewGameContextDesktop(); +#endif + } + #endregion #region Public Events @@ -165,7 +167,7 @@ protected GameBase() /// /// Gets the . /// - public ContentManager Content { get; private set; } + public ContentManager Content { get; protected set; } /// /// Gets the game components registered by this game. @@ -177,15 +179,15 @@ protected GameBase() /// Gets the game context. /// /// The game context. - public GameContext Context { get; private set; } + public GameContext Context { get; protected set; } /// /// Gets the graphics device. /// /// The graphics device. - public GraphicsDevice GraphicsDevice { get; private set; } + public GraphicsDevice GraphicsDevice { get; protected set; } - public GraphicsContext GraphicsContext { get; private set; } + public GraphicsContext GraphicsContext { get; protected set; } /// /// Gets or sets the time between each when is false. @@ -197,13 +199,13 @@ protected GameBase() /// Gets a value indicating whether this instance is active. /// /// true if this instance is active; otherwise, false. - public bool IsActive { get; private set; } + public bool IsActive { get; protected set; } /// /// Gets a value indicating whether this instance is exiting. /// /// true if this instance is exiting; otherwise, false. - public bool IsExiting{ get; private set; } + public bool IsExiting{ get; protected set; } /// /// Gets or sets a value indicating whether the elapsed time between each update should be constant, @@ -295,13 +297,14 @@ public bool IsMouseVisible /// Gets the abstract window. /// /// The window. + [Obsolete("Use GameContext.GameWindow instead.")] public GameWindow Window { get { - if (gamePlatform != null) + if (Context != null) { - return gamePlatform.MainWindow; + return Context.GameWindow; } return null; } @@ -402,7 +405,7 @@ internal void InitializeBeforeRun() /// /// The window Context for this game. /// Cannot run this instance while it is already running - public void Run(GameContext gameContext = null) + public virtual void Run(GameContext gameContext = null) { if (IsRunning) { @@ -416,22 +419,12 @@ public void Run(GameContext gameContext = null) throw new InvalidOperationException("No GraphicsDeviceManager found"); } - // Gets the GameWindow Context - if (gameContext == null) + SetGameContext(gameContext); + EnsureGameContextIsSet(); + if(Context is null) { - AppContextType c; - if (OperatingSystem.IsWindows()) - c = AppContextType.Desktop; - else if (OperatingSystem.IsAndroid()) - c = AppContextType.Android; - else if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()) - c = AppContextType.iOS; - else - c = AppContextType.DesktopSDL; - gameContext = GameContextFactory.NewGameContext(c); + throw new InvalidOperationException("No GameContext found"); } - - Context = gameContext; PrepareContext(); @@ -446,7 +439,7 @@ public void Run(GameContext gameContext = null) Context.RequestedGraphicsProfile = graphicsDeviceManagerImpl.PreferredGraphicsProfile; Context.DeviceCreationFlags = graphicsDeviceManagerImpl.DeviceCreationFlags; - gamePlatform.Run(Context); + gamePlatform.Run(); if (gamePlatform.IsBlockingRun) { @@ -468,6 +461,28 @@ public void Run(GameContext gameContext = null) } } + /// + /// Attempts to get GameContext based on the current platform. + /// + private void EnsureGameContextIsSet() + { + // Gets the Game Context + if (Context == null) + { + AppContextType c; + if (OperatingSystem.IsWindows()) + c = AppContextType.Desktop; + else if (OperatingSystem.IsAndroid()) + c = AppContextType.Android; + else if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()) + c = AppContextType.iOS; + else + c = AppContextType.DesktopSDL; + + Context = GameContextFactory.NewGameContext(c); + } + } + /// /// Creates or updates before window and device are created. /// @@ -512,6 +527,46 @@ public void Tick() } } + public virtual void SetGameContext(GameContext context) + { + if(IsRunning) + { + throw new InvalidOperationException("Cannot set the game context while the game is running"); + } + + if (Context is not null && context is not null) + { + context.GamePlatform = Context.GamePlatform; + Context = context; + Context.CurrentGame = this; + + // Overwrite Platform + if (gamePlatform != null) + { + Context.GamePlatform.Activated -= GamePlatform_Activated; + Context.GamePlatform.Deactivated -= GamePlatform_Deactivated; + Context.GamePlatform.Exiting -= GamePlatform_Exiting; + Context.GamePlatform.WindowCreated -= GamePlatformOnWindowCreated; + + Services.RemoveService(); + Services.RemoveService(); + } + + Context.GamePlatform = GamePlatform.Create(context); + Context.GamePlatform.Activated += GamePlatform_Activated; + Context.GamePlatform.Deactivated += GamePlatform_Deactivated; + Context.GamePlatform.Exiting += GamePlatform_Exiting; + Context.GamePlatform.WindowCreated += GamePlatformOnWindowCreated; + + Services.AddService(Context.GamePlatform); + Services.AddService(Context.GamePlatform); + } + else if (context is not null) + { + Context = context; + } + } + /// /// Calls automatically based on this game's setup, override it to implement your own system. /// @@ -586,7 +641,7 @@ protected virtual void RawTickProducer() RawTick(singleFrameElapsedTime, updateCount, drawLag / (float)TargetElapsedTime.Ticks, drawFrame); - var window = gamePlatform.MainWindow; + var window = Window; if (gamePlatform.IsBlockingRun) // throttle fps if Game.Tick() called from internal main loop { if (window.IsMinimized || window.Visible == false || (window.Focused == false && TreatNotFocusedLikeMinimized)) @@ -717,17 +772,13 @@ protected override void Destroy() for (int i = 0; i < array.Length; i++) { var disposable = array[i] as IDisposable; - if (disposable != null) - { - disposable.Dispose(); - } + disposable?.Dispose(); } // Reset graphics context GraphicsContext = null; - var disposableGraphicsManager = graphicsDeviceManager as IDisposable; - if (disposableGraphicsManager != null) + if (graphicsDeviceManager is IDisposable disposableGraphicsManager) { disposableGraphicsManager.Dispose(); } @@ -888,7 +939,7 @@ protected virtual void OnWindowCreated() WindowCreated?.Invoke(this, EventArgs.Empty); } - private void GamePlatformOnWindowCreated(object sender, EventArgs eventArgs) + protected void GamePlatformOnWindowCreated(object sender, EventArgs eventArgs) { Window.IsMouseVisible = isMouseVisible; OnWindowCreated(); @@ -912,7 +963,7 @@ protected virtual void UnloadContent() GameSystems.UnloadContent(); } - private void GamePlatform_Activated(object sender, EventArgs e) + protected void GamePlatform_Activated(object sender, EventArgs e) { if (!IsActive) { @@ -921,7 +972,7 @@ private void GamePlatform_Activated(object sender, EventArgs e) } } - private void GamePlatform_Deactivated(object sender, EventArgs e) + protected void GamePlatform_Deactivated(object sender, EventArgs e) { if (IsActive) { @@ -930,7 +981,7 @@ private void GamePlatform_Deactivated(object sender, EventArgs e) } } - private void GamePlatform_Exiting(object sender, EventArgs e) + protected void GamePlatform_Exiting(object sender, EventArgs e) { OnExiting(this, EventArgs.Empty); } diff --git a/sources/engine/Stride.Games/GameContext.cs b/sources/engine/Stride.Games/GameContext.cs index 8e2338269e..4fd1a2bc8a 100644 --- a/sources/engine/Stride.Games/GameContext.cs +++ b/sources/engine/Stride.Games/GameContext.cs @@ -30,7 +30,7 @@ namespace Stride.Games { /// - /// Contains context used to render the game (Control for WinForm, a DrawingSurface for WP8...etc.). + /// Contains context for the game and its core modules. /// public abstract class GameContext { @@ -56,6 +56,14 @@ public abstract class GameContext /// The run loop. public Action ExitCallback { get; internal set; } + public GameBase CurrentGame { get; set; } + + public GamePlatform GamePlatform { get; set; } + + public GameWindow GameWindow { get; set; } + + public IServiceRegistry Services { get; set; } + // TODO: remove these requested values. /// diff --git a/sources/engine/Stride.Games/GamePlatform.cs b/sources/engine/Stride.Games/GamePlatform.cs index 36848ae436..d6fa19874d 100644 --- a/sources/engine/Stride.Games/GamePlatform.cs +++ b/sources/engine/Stride.Games/GamePlatform.cs @@ -30,35 +30,38 @@ namespace Stride.Games { - internal abstract class GamePlatform : ReferenceBase, IGraphicsDeviceFactory, IGamePlatform + public abstract class GamePlatform : ReferenceBase, IGraphicsDeviceFactory, IGamePlatform { private bool hasExitRan = false; - protected readonly GameBase game; - protected readonly IServiceRegistry Services; - protected GameWindow gameWindow; + protected GameBase game => context.CurrentGame; + + protected GameWindow gameWindow => context.GameWindow; + + protected GameContext context; public string FullName { get; protected set; } = string.Empty; - protected GamePlatform(GameBase game) + protected GamePlatform(GameContext context) { - this.game = game; - Services = game.Services; + Services = context.Services; + this.context = context; + this.context.GamePlatform = this; } - public static GamePlatform Create(GameBase game) + public static GamePlatform Create(GameContext context) { #if STRIDE_PLATFORM_UWP - return new GamePlatformUWP(game); + return new GamePlatformUWP(context); #elif STRIDE_PLATFORM_ANDROID - return new GamePlatformAndroid(game); + return new GamePlatformAndroid(context); #elif STRIDE_PLATFORM_IOS - return new GamePlatformiOS(game); + return new GamePlatformiOS(context); #else // Here we cover all Desktop variants: OpenTK, SDL, Winforms,... - return new GamePlatformDesktop(game); + return new GamePlatformDesktop(context); #endif } @@ -115,11 +118,12 @@ public virtual GameWindow CreateWindow(GameContext gameContext) /// public bool IsBlockingRun { get; protected set; } - public void Run(GameContext gameContext) + public void Run() { - IsBlockingRun = !gameContext.IsUserManagingRun; + IsBlockingRun = !context.IsUserManagingRun; - gameWindow = CreateWindow(gameContext); + // Create the game window if not already created manually + context.GameWindow ??= CreateWindow(context); // Register on Activated gameWindow.Activated += OnActivated; @@ -293,7 +297,7 @@ public virtual List FindBestDevices(GameGraphicsParam IsFullScreen = preferredParameters.IsFullScreen, PreferredFullScreenOutputIndex = preferredParameters.PreferredFullScreenOutputIndex, PresentationInterval = preferredParameters.SynchronizeWithVerticalRetrace ? PresentInterval.One : PresentInterval.Immediate, - DeviceWindowHandle = MainWindow.NativeWindow, + DeviceWindowHandle = context.GameWindow.NativeWindow, ColorSpace = preferredParameters.ColorSpace, }, }; @@ -380,7 +384,7 @@ protected override void Destroy() if (gameWindow != null) { gameWindow.Dispose(); - gameWindow = null; + context.GameWindow = null; } Activated = null; diff --git a/sources/engine/Stride.Games/GraphicsDeviceManager.cs b/sources/engine/Stride.Games/GraphicsDeviceManager.cs index 86cfab0220..08cd2404b4 100644 --- a/sources/engine/Stride.Games/GraphicsDeviceManager.cs +++ b/sources/engine/Stride.Games/GraphicsDeviceManager.cs @@ -49,7 +49,7 @@ public class GraphicsDeviceManager : ComponentBase, IGraphicsDeviceManager, IGra private readonly object lockDeviceCreation; - private GameBase game; + private readonly GameBase game; private bool deviceSettingsChanged; @@ -100,12 +100,12 @@ public class GraphicsDeviceManager : ComponentBase, IGraphicsDeviceManager, IGra /// /// The game. /// The game instance cannot be null. - internal GraphicsDeviceManager(GameBase game) + public GraphicsDeviceManager(GameBase game) { this.game = game; if (this.game == null) { - throw new ArgumentNullException("game"); + throw new ArgumentNullException(nameof(game)); } lockDeviceCreation = new object(); @@ -119,8 +119,8 @@ internal GraphicsDeviceManager(GameBase game) preferredBackBufferHeight = DefaultBackBufferHeight; preferredRefreshRate = new Rational(60, 1); PreferredMultisampleCount = MultisampleCount.None; - PreferredGraphicsProfile = new[] - { + PreferredGraphicsProfile = + [ GraphicsProfile.Level_11_1, GraphicsProfile.Level_11_0, GraphicsProfile.Level_10_1, @@ -128,7 +128,7 @@ internal GraphicsDeviceManager(GameBase game) GraphicsProfile.Level_9_3, GraphicsProfile.Level_9_2, GraphicsProfile.Level_9_1, - }; + ]; graphicsDeviceFactory = game.Services.GetService(); if (graphicsDeviceFactory == null) @@ -661,6 +661,7 @@ protected virtual GraphicsDeviceInformation FindBestDevice(bool anySuitableDevic preferredParameters.PreferredBackBufferHeight = resizedBackBufferHeight; } + graphicsDeviceFactory = game.Services.GetService(); var devices = graphicsDeviceFactory.FindBestDevices(preferredParameters); if (devices.Count == 0) { diff --git a/sources/engine/Stride.Games/IGame.cs b/sources/engine/Stride.Games/IGame.cs index c8da0cf304..5788899fcd 100644 --- a/sources/engine/Stride.Games/IGame.cs +++ b/sources/engine/Stride.Games/IGame.cs @@ -4,7 +4,6 @@ using Stride.Core; using Stride.Core.Serialization.Contents; -using Stride.Games.Time; using Stride.Graphics; namespace Stride.Games @@ -140,5 +139,11 @@ public interface IGame /// /// The window. GameWindow Window { get; } + + /// + /// Sets the game context. + /// + /// + public void SetGameContext(GameContext context); } } diff --git a/sources/engine/Stride.Games/IGamePlatform.cs b/sources/engine/Stride.Games/IGamePlatform.cs index 9db9b9618f..ea4671c2dc 100644 --- a/sources/engine/Stride.Games/IGamePlatform.cs +++ b/sources/engine/Stride.Games/IGamePlatform.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; + namespace Stride.Games { /// @@ -17,6 +19,7 @@ public interface IGamePlatform /// Gets the main window. /// /// The main window. + [Obsolete("Use GameContext.MainWindow instead. This property will be removed in a future version.")] GameWindow MainWindow { get; } /// diff --git a/sources/engine/Stride.Games/Windowing/IGameWindow.cs b/sources/engine/Stride.Games/Windowing/IGameWindow.cs new file mode 100644 index 0000000000..643b1d6ac9 --- /dev/null +++ b/sources/engine/Stride.Games/Windowing/IGameWindow.cs @@ -0,0 +1,23 @@ +using System; +using Stride.Core.Mathematics; + +namespace Stride.Games.Windowing; +public interface IGameWindow : IStrideSurface +{ + public IntPtr WindowHandle { get; } + public Int2 Position { get; set; } + public Int2 Size { get; set; } + + public string Title { get; set; } + + public WindowState State { get; set; } +} + +public enum WindowState +{ + Normal, + Minimized, + Maximized, + FullscreenWindowed, + FullscreenExclusive, +} diff --git a/sources/engine/Stride.Games/Windowing/IStrideSurface.cs b/sources/engine/Stride.Games/Windowing/IStrideSurface.cs new file mode 100644 index 0000000000..7e7481ff54 --- /dev/null +++ b/sources/engine/Stride.Games/Windowing/IStrideSurface.cs @@ -0,0 +1,8 @@ +using System; +using Stride.Core.Mathematics; + +namespace Stride.Games.Windowing; +public interface IStrideSurface +{ + public Int2 Size { get; set; } +} diff --git a/sources/engine/Stride.Games/WindowsStore/GamePlatformUWP.cs b/sources/engine/Stride.Games/WindowsStore/GamePlatformUWP.cs index 466ac7fc17..87cfa1bbc7 100644 --- a/sources/engine/Stride.Games/WindowsStore/GamePlatformUWP.cs +++ b/sources/engine/Stride.Games/WindowsStore/GamePlatformUWP.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. // // Copyright (c) 2010-2013 SharpDX - Alexandre Mutel @@ -32,7 +32,7 @@ namespace Stride.Games { internal class GamePlatformUWP : GamePlatform { - public GamePlatformUWP(GameBase game) : base(game) + public GamePlatformUWP(GameContext context) : base(context) { // Application lifecycle reference: // https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-lifecycle diff --git a/sources/engine/Stride.Games/iOS/GamePlatformiOS.cs b/sources/engine/Stride.Games/iOS/GamePlatformiOS.cs index 70f2c9ddbd..5ca80248c4 100644 --- a/sources/engine/Stride.Games/iOS/GamePlatformiOS.cs +++ b/sources/engine/Stride.Games/iOS/GamePlatformiOS.cs @@ -27,7 +27,7 @@ private unsafe void PopulateFullName() Marshal.FreeHGlobal(output); } - public GamePlatformiOS(GameBase game) : base(game) + public GamePlatformiOS(GameContext context) : base(context) { PopulateFullName(); } diff --git a/sources/engine/Stride.Input/InputManager.cs b/sources/engine/Stride.Input/InputManager.cs index 5b16714c14..eb9e1d788c 100644 --- a/sources/engine/Stride.Input/InputManager.cs +++ b/sources/engine/Stride.Input/InputManager.cs @@ -318,8 +318,6 @@ public void Initialize(GameContext gameContext) { this.gameContext = gameContext ?? throw new ArgumentNullException(nameof(gameContext)); - AddSources(); - // After adding initial devices, reassign gamepad id's // this creates a beter index assignment in the case where you have both an xbox controller and another controller at startup var sortedGamePads = GamePads.OrderBy(x => x.CanChangeIndex); @@ -626,10 +624,11 @@ public void PoolInputEvent(InputEvent inputEvent) { eventRouters[inputEvent.GetType()].PoolEvent(inputEvent); } - + /// /// Resets the collection back to it's default values /// + [Obsolete("This should be managed manually instead by using the Sources collection")] public void ResetSources() { Sources.Clear(); @@ -704,7 +703,8 @@ private void AddSources() #endif break; default: - throw new InvalidOperationException("GameContext type is not supported by the InputManager"); + Logger.Warning("GameContext type is not supported by the InputManager. Register your own for input to be handled properly."); + break; } } diff --git a/sources/engine/Stride.Input/SDL/InputSourceSDL.cs b/sources/engine/Stride.Input/SDL/InputSourceSDL.cs index 7b33c0afb0..328cb58cad 100644 --- a/sources/engine/Stride.Input/SDL/InputSourceSDL.cs +++ b/sources/engine/Stride.Input/SDL/InputSourceSDL.cs @@ -14,7 +14,7 @@ namespace Stride.Input /// /// Provides support for mouse/touch/keyboard/gamepads using SDL /// - internal unsafe class InputSourceSDL : InputSourceBase + public unsafe class InputSourceSDL : InputSourceBase { private static Sdl SDL = Window.SDL; diff --git a/sources/engine/Stride.Input/Windows/InputSourceWinforms.cs b/sources/engine/Stride.Input/Windows/InputSourceWinforms.cs index b46764d109..cbfca61bfa 100644 --- a/sources/engine/Stride.Input/Windows/InputSourceWinforms.cs +++ b/sources/engine/Stride.Input/Windows/InputSourceWinforms.cs @@ -18,7 +18,7 @@ namespace Stride.Input /// /// Provides support for mouse and keyboard input on windows forms /// - internal class InputSourceWinforms : InputSourceBase + public class InputSourceWinforms : InputSourceBase { private readonly HashSet heldKeys = new HashSet(); private readonly List keysToRelease = new List(); diff --git a/sources/engine/Stride.Rendering/Rendering/Fonts/GameFontSystem.cs b/sources/engine/Stride.Rendering/Rendering/Fonts/GameFontSystem.cs index 0db5f3866f..570f6895b3 100644 --- a/sources/engine/Stride.Rendering/Rendering/Fonts/GameFontSystem.cs +++ b/sources/engine/Stride.Rendering/Rendering/Fonts/GameFontSystem.cs @@ -15,11 +15,11 @@ public class GameFontSystem : GameSystemBase { public FontSystem FontSystem { get; private set; } - public GameFontSystem(IServiceRegistry registry) + public GameFontSystem(IServiceRegistry registry, FontSystem fontSystem = null) : base(registry) { Visible = true; - FontSystem = new FontSystem(); + FontSystem = fontSystem ?? new FontSystem(); } public override void Draw(GameTime gameTime) diff --git a/sources/engine/Stride.Rendering/Streaming/StreamingManager.cs b/sources/engine/Stride.Rendering/Streaming/StreamingManager.cs index 394c791ec6..21ca774303 100644 --- a/sources/engine/Stride.Rendering/Streaming/StreamingManager.cs +++ b/sources/engine/Stride.Rendering/Streaming/StreamingManager.cs @@ -92,9 +92,8 @@ public class StreamingManager : GameSystemBase, IStreamingManager, ITexturesStre public StreamingManager([NotNull] IServiceRegistry services) : base(services) { - services.AddService(this); - services.AddService(this); - services.AddService(this); + Services.AddService(this); + Services.AddService(this); ContentStreaming = new ContentStreamingService(); diff --git a/sources/shared/SharedAssemblyInfo.cs b/sources/shared/SharedAssemblyInfo.cs index 949fe0282e..f1a976f73c 100644 --- a/sources/shared/SharedAssemblyInfo.cs +++ b/sources/shared/SharedAssemblyInfo.cs @@ -25,7 +25,7 @@ internal class StrideVersion /// /// The version used by editor for display purpose. The 4th digit will automatically be replaced by the git height when building packages with Stride.Build. /// - public const string PublicVersion = "4.2.0.1"; + public const string PublicVersion = "4.2.1.1"; /// /// The current assembly version as text, currently same as . @@ -71,4 +71,4 @@ internal partial class PublicKeys #else public const string Default = ""; #endif -} \ No newline at end of file +}