1 module dash.editor.editor; 2 import dash.core.dgame; 3 import dash.editor.websockets, dash.editor.events; 4 import dash.utility.output; 5 6 import vibe.data.json; 7 import vibe.http.status: HTTPStatus; 8 import std.uuid, std.typecons; 9 10 /** 11 * The editor manager class. Handles all interactions with editors. 12 * 13 * May be overridden to override default event implementations. 14 */ 15 class Editor 16 { 17 public: 18 /** 19 * Initializes the editor with a DGame instance. 20 * 21 * Called by DGame. 22 */ 23 final void initialize( DGame instance ) 24 { 25 game = instance; 26 27 server.start( this ); 28 registerDefaultEvents(); 29 onInitialize(); 30 } 31 32 /** 33 * Processes pending events. 34 * 35 * Called by DGame. 36 */ 37 final void update() 38 { 39 server.update(); 40 processEvents(); 41 } 42 43 /** 44 * Shutsdown the editor interface. 45 * 46 * Called by DGame. 47 */ 48 final void shutdown() 49 { 50 server.stop(); 51 } 52 53 /** 54 * Sends a message to all attached editors. 55 * 56 * Params: 57 * key = The key of the event. 58 * value = The data along side it. 59 */ 60 final void send( DataType )( string key, DataType value ) 61 { 62 send( key, value, ( Json res ) { } ); 63 } 64 static assert(is(typeof( send( "key", "data" ) ))); 65 66 /** 67 * Sends a message to all attached editors. 68 * 69 * In most cases, you won't have to manually specify template parameters, 70 * they should be inferred. 71 * 72 * Params: 73 * key = The key of the event. 74 * value = The data along side it. 75 * cb = The callback to call when a response is received. 76 * 77 * Examples: 78 * --- 79 * // DataType inferred as string, ResponseType inferred as string. 80 * editor.send( "my_key", "my_value", ( string response ) { 81 * // Handle response 82 * } ); 83 * --- 84 */ 85 final void send( DataType, ResponseType )( string key, DataType value, void delegate( ResponseType ) cb ) 86 { 87 UUID cbId = randomUUID(); 88 89 void callbackHandler( EventMessage msg ) 90 { 91 auto response = msg.value.deserializeJson!EventResponse; 92 93 if( response.status == EventResponse.Status.ok ) 94 cb( response.data.deserializeJson!ResponseType ); 95 else 96 throw response.data.deserializeJson!TransferableException().toException(); 97 } 98 registerCallbackHandler( cbId, &callbackHandler ); 99 100 EventMessage msg; 101 msg.key = key; 102 msg.value = value.serializeToJson(); 103 msg.callbackId = cbId.toString(); 104 105 server.send( msg ); 106 } 107 static assert(is(typeof( send( "key", "data", ( string response ) { } ) ))); 108 109 /** 110 * Registers an event callback, for when an event with the given key is received. 111 * 112 * Params: 113 * key = The key of the event. 114 * event = The handler to call. 115 * 116 * * Examples: 117 * --- 118 * // DataType inferred as string, ResponseType inferred as string. 119 * editor.registerEventHandler( "loopback", ( string receivedData ) { 120 * // Handle response 121 * // Return your response, or nothing if signify success without response. 122 * return receivedData; 123 * } ); 124 * 125 * Returns: The ID of the event, so it can be unregistered later. 126 */ 127 final UUID registerEventHandler( DataType, ResponseType )( string key, ResponseType delegate( DataType ) event ) 128 { 129 void handler( EventMessage msg ) 130 { 131 // Automatically deserialize received data to requested type. 132 DataType receivedData; 133 try 134 { 135 receivedData = msg.value.deserializeJson!DataType; 136 } 137 catch( JSONException e ) 138 { 139 errorf( "Error deserializing received message with key \"%s\" to %s: %s", key, DataType.stringof, e.msg ); 140 return; 141 } 142 143 // Create a message with the callback id, and the response of the event. 144 EventMessage newMsg; 145 newMsg.key = CallbackMessageKey; 146 newMsg.callbackId = msg.callbackId; 147 148 // Build response to send back 149 EventResponse res; 150 151 try 152 { 153 static if(is( ResponseType == void )) 154 { 155 // Call the event handler. 156 event( receivedData ); 157 res.data = Json( "success" ); 158 } 159 else 160 { 161 // Call the event handler, and capture the result. 162 ResponseType result = event( receivedData ); 163 res.data = result.serializeToJson(); 164 } 165 166 // If we've made it this far, it's a success 167 res.status = EventResponse.Status.ok; 168 } 169 catch( Exception e ) 170 { 171 // If failure, send exception. 172 res.status = EventResponse.Status.error; 173 res.data = TransferableException.fromException( e ).serializeToJson(); 174 } 175 176 // Serialize response, and sent it across. 177 newMsg.value = res.serializeToJson(); 178 server.send( newMsg ); 179 } 180 181 return registerInternalMessageHandler( key, &handler ); 182 } 183 static assert(is(typeof( registerEventHandler( "key", ( string data ) { } ) ))); 184 static assert(is(typeof( registerEventHandler( "key", ( string data ) => data ) ))); 185 186 /** 187 * Unregisters an event callback. 188 * 189 * Params: 190 * id = The id of the handler to remove. 191 */ 192 final void unregisterEventHandler( UUID id ) 193 { 194 foreach( _, handlerTupArr; eventHandlers ) 195 { 196 foreach( i, handlerTup; handlerTupArr ) 197 { 198 if( handlerTup.id == id ) 199 { 200 auto end = handlerTupArr[ i+1..$ ]; 201 handlerTupArr = handlerTupArr[ 0..i ] ~ end; 202 } 203 } 204 } 205 } 206 207 protected: 208 DGame game; 209 WebSocketServer server; 210 211 /// To be overridden 212 void onInitialize() { } 213 /// ditto 214 void onStartPlay() { } 215 /// ditto 216 void onPausePlay() { } 217 /// ditto 218 void onStopPlay() { } 219 220 /** 221 * Processes all pending events. 222 * 223 * Called by update. 224 */ 225 final void processEvents() 226 { 227 // Clear the events 228 scope(exit) pendingEvents.length = 0; 229 230 foreach( event; pendingEvents ) 231 { 232 // Dispatch to handlers. 233 if( auto handlerTupArray = event.key in eventHandlers ) 234 { 235 foreach( handlerTup; *handlerTupArray ) 236 { 237 handlerTup.handler( event ); 238 } 239 } 240 else 241 { 242 warningf( "Invalid editor event received with key %s", event.key ); 243 } 244 } 245 } 246 247 package: 248 /// The message key for callbacks 249 enum CallbackMessageKey = "__callback__"; 250 251 alias InternalEventHandler = void delegate( EventMessage ); 252 alias EventHandlerTuple = Tuple!(UUID, "id", InternalEventHandler, "handler"); 253 254 /// Register an event from the front end. 255 final void queueEvent( EventMessage msg ) 256 { 257 pendingEvents ~= msg; 258 } 259 260 /// Register a message internally, after generating a handler for it. 261 final UUID registerInternalMessageHandler( string key, InternalEventHandler handler ) 262 { 263 auto id = randomUUID(); 264 eventHandlers[ key ] ~= EventHandlerTuple( id, handler ); 265 return id; 266 } 267 268 /// If a send call requests a callback, register it. 269 final void registerCallbackHandler( UUID id, InternalEventHandler handler ) 270 { 271 callbacks[ id ] = handler; 272 } 273 274 /// Register built-in event handlers. 275 final void registerDefaultEvents() 276 { 277 registerInternalMessageHandler( CallbackMessageKey, &handleCallback ); 278 279 // Test handler, responds with request 280 registerEventHandler!( Json, Json )( "loopback", json => json ); 281 282 registerGameEvents( this, game ); 283 registerObjectEvents( this, game ); 284 } 285 286 /// Handles callback messages 287 final void handleCallback( EventMessage msg ) 288 { 289 // If it's a callback, dispatch it as such. 290 UUID id = msg.callbackId.parseUUID(); 291 if( id.empty ) 292 { 293 error( "Callback received with empty id" ); 294 } 295 else if( auto cb = id in callbacks ) 296 { 297 (*cb)( msg ); 298 callbacks.remove( id ); 299 } 300 else 301 { 302 errorf( "Callback reference lost: %s", id ); 303 } 304 } 305 306 private: 307 EventMessage[] pendingEvents; 308 EventHandlerTuple[][string] eventHandlers; 309 InternalEventHandler[UUID] callbacks; 310 } 311 312 /// Easy to handle response struct. 313 private struct EventResponse 314 { 315 /// Status of a request 316 enum Status 317 { 318 ok = 0, 319 error = 2, 320 } 321 322 Status status; 323 Json data; 324 } 325 326 // Exception that can be serialized 327 struct TransferableException 328 { 329 string msg; 330 size_t line; 331 string file; 332 333 static TransferableException fromException( Exception e ) 334 { 335 TransferableException except; 336 except.msg = e.msg; 337 except.line = e.line; 338 except.file = e.file; 339 return except; 340 } 341 342 Exception toException() 343 { 344 return new Exception( msg, file, line ); 345 } 346 }