1 /**
2  * Retrograde Engine
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2021 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.debugging;
13 
14 import retrograde.entity;
15 import retrograde.game;
16 import retrograde.engine;
17 import retrograde.messaging;
18 import retrograde.input;
19 import retrograde.stringid;
20 import retrograde.player;
21 
22 import poodinis;
23 
24 import collie.net;
25 import collie.codec.http;
26 import collie.codec.http.server;
27 
28 import std.string;
29 import std.conv;
30 import std.experimental.logger;
31 //import std.typecons;
32 import std.json;
33 import std.exception;
34 
35 import core.thread;
36 
37 class RemoteDebugger : EntityProcessor {
38 
39     @Autowire
40     private std.experimental.logger.Logger logger;
41 
42     @Autowire
43     private Game game;
44 
45     @Autowire
46     @OptionalDependency
47     private DebugWidget[] debugWidgets;
48 
49     private ServerThread thread;
50 
51     public override bool acceptsEntity(Entity entity) {
52         return false;
53     }
54 
55     private class ServerThread : Thread {
56         this() {
57             super(&run);
58         }
59 
60         private HttpServer server;
61 
62         private void run() {
63             HTTPServerOptions options = new HTTPServerOptions();
64             options.handlerFactories ~= &createRequestHandler;
65             options.threads = 1;
66             HTTPServerOptions.IPConfig ipConfig;
67             ipConfig.address = new InternetAddress("0.0.0.0", 8080);
68 
69             server = new HttpServer(options);
70             server.addBind(ipConfig);
71             logger.info("The remote debugger is listening on ", ipConfig.address.toString());
72             server.start();
73         }
74 
75         private RequestHandler createRequestHandler(RequestHandler, HTTPMessage) {
76             DebuggerRequestHandler.RouteMap routes;
77 
78             routes["/ping"] = &pong;
79             routes["/widgets/"] = &createWidgetList;
80 
81             foreach(widget; debugWidgets) {
82                 routes["/data/" ~ widget.resourceName] = &widget.createContentJson;
83             }
84 
85             return new DebuggerRequestHandler(routes);
86         }
87 
88         private void terminate() {
89             server.stop();
90         }
91     }
92 
93     class DebuggerRequestHandler : RequestHandler {
94         alias RouteFunc = void delegate(HttpMessage message, ref ResponseBuilder response);
95         alias RouteMap = RouteFunc[string];
96 
97         private HttpMessage message;
98         private RouteMap routes;
99         private RouteFunc* route;
100 
101         this(RouteMap routes) {
102             this.routes = routes;
103         }
104 
105         protected override void onRequest(HttpMessage message) nothrow
106         {
107             collectException({
108                 this.message = message;
109                 this.route = message.url in routes;
110             }());
111         }
112 
113         protected override void onBody(const ubyte[] data) nothrow {}
114 
115         protected override void onEOM() nothrow {
116             auto exception = collectException({
117                 auto response = new ResponseBuilder(_downstream);
118                 response.header("Server", format("%s remote debugger - %s", getEngineName(), getEngineVersionText()));
119 
120                 if (route !is null) {
121                     response.status(cast(ushort) 200, HTTPMessage.statusText(200));
122                     (*route)(this.message, response);
123                 } else {
124                     response.status(cast(ushort) 404, HTTPMessage.statusText(404));
125                 }
126 
127                 response.sendWithEOM();
128             }());
129 
130             if (exception !is null) {
131                 collectException({
132                     logger.error(exception.message ~ "\n" ~ exception.info.toString);
133 
134                     auto response = new ResponseBuilder(_downstream);
135                     response.status(cast(ushort) 500, HTTPMessage.statusText(500));
136                     response.sendWithEOM();
137                 }());
138             }
139         }
140 
141         protected override void onError(HTTPErrorCode code) nothrow {}
142 
143         protected override void requestComplete() nothrow {}
144     }
145 
146     private void pong(HttpMessage message, ref ResponseBuilder response) {
147         response
148             .setCorsHeaders()
149             .header("Content-Type", "text/plain")
150             .setBody(cast(ubyte[]) "pong");
151     }
152 
153     private void createWidgetList(HttpMessage message, ref ResponseBuilder response) {
154         JSONValue[] widgetList;
155 
156         foreach(widget; debugWidgets) {
157             JSONValue widgetJson = [
158                 "element": widget.elementName
159             ];
160 
161             widgetJson.object["elementParameters"] = widget.createElementParameters();
162             widgetList ~= widgetJson;
163         }
164 
165         JSONValue widgetListJson;
166         widgetListJson.array = widgetList;
167         response.setJsonBody(widgetListJson);
168     }
169 
170     public override void initialize() {
171         logger.info("Remote debugger enabled");
172         foreach(widget; debugWidgets) {
173             widget.initialize();
174         }
175 
176         thread = new ServerThread;
177         thread.start();
178     }
179 
180     public override void cleanup() {
181         logger.info("Remote debugger disabled");
182         thread.terminate();
183         thread.join();
184     }
185 }
186 
187 private ResponseBuilder setCorsHeaders(ref ResponseBuilder response) {
188     return response
189         .header("Access-Control-Allow-Origin", "*")
190         .header("Access-Control-Allow-Methods", "GET");
191 }
192 
193 private ResponseBuilder setJsonBody(ref ResponseBuilder response, ref JSONValue responseJson) {
194     return response
195         .setCorsHeaders()
196         .header("Content-Type", "application/json")
197         .setBody(cast(ubyte[]) responseJson.toString);
198 }
199 
200 class RemoteDebuggerContext : ApplicationContext {
201     public override void registerDependencies(shared(DependencyContainer) container) {
202         container.register!(DebugWidget, GameInfoDebugWidget);
203         container.register!(DebugWidget, EntityManagerDebugWidget);
204         container.register!(DebugWidget, MessagingDebugWidget);
205     }
206 }
207 
208 interface DebugWidget {
209     @property string resourceName();
210     @property string elementName();
211     void initialize();
212     JSONValue createElementParameters();
213     void createContentJson(HttpMessage message, ref ResponseBuilder response);
214 }
215 
216 abstract class SimpleDebugWidget : DebugWidget {
217     public @property string elementName() {
218         return "rg-simplewidget";
219     }
220 }
221 
222 class GameInfoDebugWidget : SimpleDebugWidget {
223     @Autowire
224     private Game game;
225 
226     public @property string resourceName() {
227         return "game-info";
228     }
229 
230     public void initialize() {}
231 
232     public JSONValue createElementParameters() {
233         return JSONValue([
234             "title": "Game Info",
235             "resource": resourceName
236         ]);
237     }
238 
239     public void createContentJson(HttpMessage message, ref ResponseBuilder response) {
240         JSONValue json = [
241             "content": format("%s - %s %s - target frametime: %sms - lag limit: %s frames", game.name, getEngineName(), getEngineVersionText(), game.targetFrameTime, game.lagFrameLimit)
242         ];
243 
244         response.setJsonBody(json);
245     }
246 }
247 
248 class EntityManagerDebugWidget : DebugWidget {
249     @Autowire
250     private EntityManager entityManager;
251 
252     public @property string resourceName() {
253         return "entity-info";
254     }
255 
256     public @property string elementName() {
257         return "rg-entitymanagerwidget";
258     }
259 
260     public void initialize() {}
261 
262     public JSONValue createElementParameters() {
263         return JSONValue([
264             "title": "Entity Manager",
265             "resource": resourceName
266         ]);
267     }
268 
269     public void createContentJson(HttpMessage message, ref ResponseBuilder response) {
270         JSONValue info = [
271             "entities": createEntitiesJson(),
272             "processors": createProcessorsJson()
273         ];
274 
275         response.setJsonBody(info);
276     }
277 
278     private JSONValue createEntitiesJson() {
279         JSONValue[] entityJsons;
280         foreach(entity; entityManager.entities) {
281             JSONValue entityJson = [
282                 "name": entity.name
283             ];
284 
285             entityJson.object["id"] = JSONValue(entity.id);
286             entityJson.object["components"] = createComponentsJson(entity);
287             entityJsons ~= entityJson;
288         }
289 
290         JSONValue entityJsonsList;
291         entityJsonsList.array = entityJsons;
292         return entityJsonsList;
293     }
294 
295     private JSONValue createComponentsJson(Entity entity) {
296         JSONValue[] componentJsons;
297         foreach(component; entity.components) {
298             JSONValue componentJson = [
299                 "componentType": component.getComponentTypeString()
300             ];
301 
302             componentJson.object["componentTypeSid"] = JSONValue(component.getComponentType());
303             componentJson.object["data"] = createSnapshotJson(component);
304             componentJsons ~= componentJson;
305         }
306 
307         JSONValue componentJsonsList;
308         componentJsonsList.array = componentJsons;
309         return componentJsonsList;
310     }
311 
312     private JSONValue createProcessorsJson() {
313         JSONValue[] processorJsons;
314         foreach(processor; entityManager.processors) {
315             JSONValue processorJson = [
316                 "type": typeid(processor).name
317             ];
318 
319             processorJson.object["entities"] = createProcessorEntitiesJson(processor);
320             processorJsons ~= processorJson;
321         }
322 
323         JSONValue processorJsonsList;
324         processorJsonsList.array = processorJsons;
325         return processorJsonsList;
326     }
327 
328     private JSONValue createProcessorEntitiesJson(EntityProcessor processor) {
329         JSONValue[] entityJsons;
330         foreach(entity; processor.entities) {
331             JSONValue entityJson = [
332                 "name": entity.name
333             ];
334 
335             entityJson.object["id"] = JSONValue(entity.id);
336 
337             entityJsons ~= entityJson;
338         }
339 
340         JSONValue entityJsonsList;
341         entityJsonsList.array = entityJsons;
342         return entityJsonsList;
343     }
344 
345     private JSONValue createSnapshotJson(EntityComponent component) {
346         JSONValue data = parseJSON("{}");
347         auto snapshot = cast(Snapshotable) component;
348         if (snapshot !is null) {
349             auto snapshotData = snapshot.getSnapshotData();
350             foreach(snapshotTuple; snapshotData.byKeyValue()) {
351                 data.object[snapshotTuple.key] = JSONValue(snapshotTuple.value);
352             }
353         }
354 
355         return data;
356     }
357 }
358 
359 class MessageLogger {
360     private MessageChannel _channel;
361     private const(Message)[] _log;
362     private static const int maxLogs = 10;
363 
364     public @property channel() {
365         return _channel;
366     }
367 
368     public @property log() {
369         return _log;
370     }
371 
372     this(MessageChannel channel) {
373         this._channel = channel;
374     }
375 
376     public void connect() {
377         _channel.connect(&logMessage);
378     }
379 
380     private void logMessage(const(Message) message) {
381         if (_log.length >= maxLogs) {
382             _log = _log[1 .. $];
383         }
384 
385         _log ~= message;
386     }
387 }
388 
389 class MessagingDebugWidget : DebugWidget {
390 
391     @Autowire
392     @OptionalDependency
393     private retrograde.messaging.EventChannel[] eventChannels;
394 
395     @Autowire
396     @OptionalDependency
397     private CommandChannel[] commandChannels;
398 
399     @Autowire
400     @OptionalDependency
401     private MessageProcessor[] messageProcessors;
402 
403     @Autowire
404     private SidMap sidMap;
405 
406     private MessageLogger[] eventLoggers;
407     private MessageLogger[] commandLoggers;
408 
409     public @property string resourceName() {
410         return "messaging-info";
411     }
412 
413     public @property string elementName() {
414         return "rg-messagingwidget";
415     }
416 
417     public void initialize() {
418         foreach(channel; eventChannels) {
419             addChannelAsLogger(channel, eventLoggers);
420         }
421 
422         foreach(channel; commandChannels) {
423             addChannelAsLogger(channel, commandLoggers);
424         }
425     }
426 
427     private void addChannelAsLogger(MessageChannel channel, ref MessageLogger[] loggerDestination) {
428         auto logger = new MessageLogger(channel);
429         logger.connect();
430         loggerDestination ~= logger;
431     }
432 
433     public JSONValue createElementParameters() {
434         return JSONValue([
435             "title": "Messaging",
436             "resource": resourceName
437         ]);
438     }
439 
440     public void createContentJson(HttpMessage message, ref ResponseBuilder response) {
441         JSONValue[] eventChannelsJsons;
442         JSONValue[] commandChannelsJsons;
443         JSONValue[] messageProcessorsJsons;
444 
445         foreach(logger; eventLoggers) {
446             JSONValue eventChannelJson = [
447                 "name": typeid(logger.channel).name
448             ];
449 
450             eventChannelJson.object["messageHistory"] = createMessageHistoryJson(logger);
451             eventChannelsJsons ~= eventChannelJson;
452         }
453 
454         foreach(logger; commandLoggers) {
455             JSONValue commandChannelJson = [
456                 "name": typeid(logger.channel).name
457             ];
458 
459             commandChannelJson.object["messageHistory"] = createMessageHistoryJson(logger);
460             commandChannelsJsons ~= commandChannelJson;
461         }
462 
463         foreach(processor; messageProcessors) {
464             messageProcessorsJsons ~= JSONValue([
465                 "name": typeid(processor).name
466             ]);
467         }
468 
469         JSONValue info = [
470             "eventChannels": eventChannelsJsons,
471             "commandChannels": commandChannelsJsons,
472             "messageProcessors": messageProcessorsJsons
473         ];
474 
475         response.setJsonBody(info);
476     }
477 
478     private JSONValue createMessageHistoryJson(MessageLogger logger) {
479         JSONValue[] messageJsons;
480 
481         foreach(message; logger.log) {
482             debug(readableStringId) {
483                 string type = message.type;
484             } else {
485                 string type;
486                 if (sidMap.contains(message.type)) {
487                     type = sidMap[message.type];
488                 } else {
489                     type = format("<sid:%s>", message.type);
490                 }
491             }
492 
493             JSONValue messageJson = [
494                 "type": type,
495                 "data": createDataString(message.data)
496             ];
497 
498             messageJson.object["magnitude"] = JSONValue(message.magnitude);
499             messageJsons ~= messageJson;
500         }
501 
502         JSONValue messsageJsonsList;
503         messsageJsonsList.array = messageJsons;
504         return messsageJsonsList;
505     }
506 
507     private string createDataString(const(MessageData) data) {
508         if (data is null) {
509             return "";
510         }
511 
512         return to!string(data);
513     }
514 }
515 
516 class DebugEventPrinter {
517 
518     @Autowire
519     private CoreEngineCommandChannel coreEventChannel;
520 
521     @Autowire
522     private RawInputEventChannel rawInputEventChannel;
523 
524     @Autowire
525     private MappedInputCommandChannel mappedInputCommandChannel;
526 
527     @Autowire
528     private std.experimental.logger.core.Logger logger;
529 
530     public void initialize() {
531         coreEventChannel.connect(&printEvent);
532         rawInputEventChannel.connect(&printEvent);
533         mappedInputCommandChannel.connect(&printEvent);
534     }
535 
536     private void printEvent(const(Event) event) {
537         string extraData = "";
538 
539         switch (event.type) {
540             case InputEvent.JOYSTICK_AXIS_MOVEMENT:
541                 auto data = cast(JoystickAxisEventData) event.data;
542                 extraData = format("Axis: %s", data.axis);
543                 break;
544 
545             case InputEvent.JOYSTICK_BALL_MOVEMENT:
546                 auto data = cast(JoystickBallEventData) event.data;
547                 extraData = format("Ball: %s", data.ball);
548                 break;
549 
550             case InputEvent.JOYSTICK_HAT:
551                 auto data = cast(JoystickHatEventData) event.data;
552                 extraData = format("Hat: %s", data.hat);
553                 break;
554 
555             case InputEvent.JOYSTICK_BUTTON:
556                 auto data = cast(JoystickButtonEventData) event.data;
557                 extraData = format("Button: %s", data.button);
558                 break;
559 
560             case InputEvent.JOYSTICK_ADDED:
561             case InputEvent.JOYSTICK_REMOVED:
562                 auto data = cast(InputMessageData) event.data;
563                 extraData = format("Device: %s", data.device);
564                 break;
565 
566             case InputEvent.KEYBOARD_KEY:
567                 auto data = cast(KeyboardKeyEventData) event.data;
568                 extraData = format("Key: %s - Modifiers: %s", to!string(data.scanCode), data.modifiers);
569                 break;
570 
571             case InputEvent.MOUSE_MOTION:
572                 auto data = cast(MouseMotionEventData) event.data;
573                 extraData = format("Axis: %s", data.axis);
574                 break;
575 
576             case InputEvent.MOUSE_BUTTON:
577                 auto data = cast(MouseButtonEventData) event.data;
578                 extraData = format("Button: %s", data.button);
579                 break;
580 
581             default:
582                 break;
583         }
584 
585         debug(readableStringId) {
586             string type = event.type;
587         } else {
588             string type = format("<sid:%s>", event.type);
589         }
590 
591         logger.infof("%s: Received %s event with magnitude %s %s", typeid(this), type, event.magnitude, encloseIfNotEmpty(extraData));
592     }
593 
594     private string encloseIfNotEmpty(string text) {
595         string result = text;
596         if (text.length > 0) {
597             result = "(" ~ text ~ ")";
598         }
599         return result;
600     }
601 }
602 
603 public void registerRetrogradeDebugSids(SidMap sidMap) {
604     registerLifecycleDebugSids(sidMap);
605     registerEngineDebugSids(sidMap);
606     registerPlayerLifecycleDebugSids(sidMap);
607     registerInputEventDebugSids(sidMap);
608 }