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 }