1 /**
2  * Retrograde Engine
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2023 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE.txt file.
10  */
11 
12 module retrograde.bootstrap;
13 
14 import retrograde.core.runtime : EngineRuntime, StandardEngineRuntime;
15 import retrograde.core.game : Game;
16 import retrograde.core.entity : EntityManager;
17 import retrograde.core.messaging : MessageHandler;
18 import retrograde.core.platform : Platform, PlatformSettings, NullPlatform;
19 import retrograde.core.logging : StdoutLogger;
20 import retrograde.core.input : InputMapper;
21 import retrograde.core.rendering : RenderSystem, ShaderProgram, GraphicsApi, NullGraphicsApi, DepthTestingMode;
22 import retrograde.core.storage : StorageSystem, GenericStorageSystem;
23 
24 import retrograde.rendering.generic : GenericRenderSystem;
25 
26 import retrograde.ai.generative.stabilityai : StabilityAiApi, StabilityAiApiImpl;
27 
28 import poodinis : Inject, DependencyContainer, ResolveOption, initializedBy, existingInstance;
29 import poodinis.valueinjector.mirage : loadConfig, parseIniConfig;
30 
31 import std.logger : Logger, MultiLogger, sharedLog;
32 import std.file : exists;
33 
34 struct GraphicsApiSettings {
35     /*
36      * Set the initial depth testing mode.
37      * Setting it up in the bootstrap prevents unnecesary recompilation of shaders.
38      */
39     DepthTestingMode initialDepthTestingMode = DepthTestingMode.apiDefault;
40 
41     //TODO: Add more settings?
42 }
43 
44 version (Have_glfw_d) {
45     import retrograde.platform.glfw : GlfwPlatform, GlfwPlatformSettings;
46 
47     alias DefaultPlatform = GlfwPlatform;
48     alias DefaultPlatformSettings = GlfwPlatformSettings;
49 } else {
50     alias DefaultPlatform = NullPlatform;
51     alias DefaultPlatformSettings = PlatformSettings;
52 }
53 
54 version (Have_bindbc_opengl) {
55     import retrograde.rendering.api.opengl : OpenGlGraphicsApi;
56 
57     alias DefaultGraphicsApi = OpenGlGraphicsApi;
58 } else {
59     alias DefaultGraphicsApi = NullGraphicsApi;
60 }
61 
62 /**
63  * Bootstrap function for quickly setting up the DI framework with all required and typically
64  * used engine functionality. After set-up, the game is started.
65  */
66 public void startGame(GameType : Game, EngineRuntimeType:
67     EngineRuntime = StandardEngineRuntime, PlatformType:
68     Platform = DefaultPlatform,
69 RenderSystemType:
70     RenderSystem = GenericRenderSystem,
71 GraphicsApiType:
72     GraphicsApi = DefaultGraphicsApi)(
73     const PlatformSettings platformSettings = new DefaultPlatformSettings(),
74     const GraphicsApiSettings graphicsApiSettings = GraphicsApiSettings(),
75     shared DependencyContainer dependencies = new shared DependencyContainer(),
76     const bool suppressBootstrapWarnings = false
77 ) {
78 
79     dependencies.setPersistentResolveOptions(ResolveOption.registerBeforeResolving);
80 
81     dependencies.register!(EngineRuntime, EngineRuntimeType);
82     dependencies.register!(Game, GameType);
83     dependencies.register!(RenderSystem, RenderSystemType);
84     dependencies.register!(GraphicsApi, GraphicsApiType);
85     dependencies.register!(Platform, PlatformType);
86     dependencies.register!(StorageSystem, GenericStorageSystem);
87     dependencies.register!(StabilityAiApi, StabilityAiApiImpl);
88 
89     const configPath = "./engine.ini";
90     if (exists(configPath)) {
91         dependencies.loadConfig(configPath);
92     } else {
93         string defaultConfig = import(configPath);
94         dependencies.parseIniConfig(defaultConfig);
95     }
96 
97     dependencies.register!Logger.initializedBy({
98         auto logger = new MultiLogger(); // Cannot be shared, but must be for sharedLog
99 
100         auto stdoutLogger = new StdoutLogger();
101         if (stdoutLogger.stdoutIsAvailable()) {
102             logger.insertLogger("stdoutLogger", stdoutLogger);
103         }
104 
105         // sharedLog = stdoutLogger; //Creating shared loggers is broken. Fix when phobos fixes it.
106         return logger;
107     });
108 
109     static if (is(DefaultPlatform == NullPlatform)) {
110         if (!suppressBootstrapWarnings) {
111             dependencies.resolve!Logger.warning(
112                 "No platform implementation available. Using NullPlatform. Add a suitable platform library to your project to get rid of this warning."
113             );
114         }
115     }
116 
117     static if (is(DefaultGraphicsApi == NullGraphicsApi)) {
118         if (!suppressBootstrapWarnings) {
119             dependencies.resolve!Logger.warning(
120                 "No graphics api implementation available. Using NullGraphicsApi. Add a suitable graphics api library to your project to get rid of this warning."
121             );
122         }
123     }
124 
125     auto graphicsApi = dependencies.resolve!GraphicsApi;
126     graphicsApi.setDepthTestingMode(graphicsApiSettings.initialDepthTestingMode);
127 
128     auto renderSystem = dependencies.resolve!RenderSystem;
129     auto entityManager = dependencies.resolve!EntityManager;
130     entityManager.addEntityProcessor(renderSystem);
131 
132     auto runtime = dependencies.resolve!EngineRuntime;
133     runtime.startGame(platformSettings);
134 }
135 
136 /** 
137  * An implementation of the Game interface that contains the usual engine functionality.
138  *
139  * A bootstrap game takes care of initializing, updating and rendering an EntityManager,
140  * MessageHandler and InputMapper. Make sure to call super.X() when overriding any of the Game 
141  * interface methods.
142  */
143 class BootstrapGame : Game {
144     protected @Inject EntityManager entityManager;
145     protected @Inject MessageHandler messageHandler;
146     protected @Inject InputMapper inputMapper;
147 
148     public void initialize() {
149     }
150 
151     public void update() {
152         messageHandler.shiftStandbyToActiveQueue();
153         inputMapper.update();
154         entityManager.update();
155     }
156 
157     public void render(double extraPolation) {
158         entityManager.draw();
159     }
160 
161     public void terminate() {
162     }
163 }
164 
165 version (unittest) {
166     class TestGame : Game {
167         public bool isInitialized;
168 
169         @Inject private EngineRuntime runtime;
170         @Inject EntityManager entityManager;
171         @Inject MessageHandler messageHandler;
172 
173         public override void initialize() {
174             isInitialized = true;
175         }
176 
177         public override void update() {
178             runtime.terminate();
179         }
180 
181         public override void render(double extraPolation) {
182         }
183 
184         public override void terminate() {
185         }
186     }
187 
188     @("Bootstrap of testgame")
189     unittest {
190         auto dependencies = new shared DependencyContainer();
191         startGame!TestGame(new PlatformSettings(), GraphicsApiSettings(), dependencies, true);
192         const game = dependencies.resolve!TestGame;
193         assert(game.isInitialized);
194     }
195 }