{"id":783,"date":"2023-01-27T21:50:45","date_gmt":"2023-01-27T10:50:45","guid":{"rendered":"https:\/\/x37v.com\/x37v\/?p=783"},"modified":"2023-01-28T14:03:54","modified_gmt":"2023-01-28T03:03:54","slug":"teaching-max-to-play-ddr","status":"publish","type":"post","link":"https:\/\/x37v.com\/x37v\/max\/teaching-max-to-play-ddr\/","title":{"rendered":"Teaching Max to play Dance Dance Revolution"},"content":{"rendered":"\n<p>I&#8217;ve been playing some old games with <a rel=\"noreferrer noopener\" href=\"https:\/\/openemu.org\" data-type=\"URL\" data-id=\"https:\/\/openemu.org\" target=\"_blank\">OpenEmu<\/a> recently, and got hooked on the idea of automating DDR game input by reading content from the screen with Max and sending virtual keystrokes back to OpenEmu. Here\u2019s a quick example of what I ended up with:<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Max Plays Dance Dance Revolution.\" width=\"620\" height=\"349\" src=\"https:\/\/www.youtube.com\/embed\/qRzqXAa8Hw8?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><figcaption class=\"wp-element-caption\">Max plays some of the more difficult levels of the game.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The backstory\u2026<\/h2>\n\n\n\n<p>It all started when I found some PlayStation &#8216;dance mat&#8217; controllers (<a rel=\"noreferrer noopener\" href=\"https:\/\/www.retrosales.com.au\/products\/controller-sony-playstation-ps1-dance-dance-revolution-mat-new-boxed-ru017\" target=\"_blank\">like these<\/a>) that were made for the <a rel=\"noreferrer noopener\" href=\"https:\/\/www.ddrgame.com\" data-type=\"URL\" data-id=\"https:\/\/www.ddrgame.com\" target=\"_blank\">Dance Dance Revolution<\/a> PSX game. I have an original PSX and a copy of the game that are boxed up somewhere, so I tracked down an ISO of the game (and connected the mats to the computer with a PSX to USB converter) to try them out with an emulator instead. The mats had been folded up for years and no longer worked very well, but the futile exercise got me thinking about how you might create a virtual DDR-bot with <a rel=\"noreferrer noopener\" href=\"https:\/\/cycling74.com\/products\/max\" target=\"_blank\">Max<\/a> that could &#8216;read&#8217; the arrow information from the screen to trigger button presses automatically.<\/p>\n\n\n\n<p>The idea took several approaches before the system was confident enough to know how to play. But, as it turns out, we can get surprisingly far with some primitive computer vision strategies.<\/p>\n\n\n\n<p>Here&#8217;s how I built it\u2026<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">OpenEmu<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Setup<\/h3>\n\n\n\n<p>The first thing that is worth doing is making OpenEmu&#8217;s emulation larger. The size of the game window (at 1.0x) in OpenEmu is 300&#215;240 (which is a little small on my 2560&#215;1440 display), so I elected to upscale the window in OpenEmu a little bit (2.0x) to make it a little more \u2018readable\u2019 on my screen.* As we&#8217;re going to use&nbsp;<em>Max<\/em>&nbsp;to observe the game though, this means that we\u2019re actually asking it to watch 4.0x as many pixels (given that it is doubled in width&nbsp;and&nbsp;height&#8230; but my 2013 machine seems to cope OK).<\/p>\n\n\n\n<p>* As well as adjusting the scale of the window, OpenEmu lets you apply filters the emulation, so I\u2019ve kept this as <em>Pixellate<\/em> to preserve hard edges by duplicating pixels without smoothing. (<em>Nearest Neighbour<\/em> would also be fine). We&#8217;ll re-downscale this in Max with interpolation off (<code class=\"max object\" data-inlets=\"1\" data-outlets=\"2\">jit.matrix 4 char 300 240 @interp 0<\/code>) to reduce our pixel crunching.<\/p>\n\n\n\n<p>While the game window is now upscaled to 600&#215;480, the actual location of the game window on my screen starts at (2, 45) given the border of the windows and menu bar in macOS Catalina. We\u2019ll therefore ask Max to watch the desktop with: <code class=\"max object\" data-inlets=\"1\" data-outlets=\"2\">jit.desktop 4 char 600 480 @rect 2 45 602 525<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Getting the Game Screen Into Max<\/h2>\n\n\n\n<p>Getting the game screen into Max was fairly easy, but the first time you use <code class=\"max object\" data-inlets=\"1\" data-outlets=\"2\">jit.desktop<\/code> you need to explicitly give it permission to capture the screen.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"573\" height=\"283\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Max-Accessibility.png\" alt=\"\" class=\"wp-image-812\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Max-Accessibility.png 573w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Max-Accessibility-300x148.png 300w\" sizes=\"auto, (max-width: 573px) 100vw, 573px\" \/><figcaption class=\"wp-element-caption\">Once the permissions are granted in System Preferences, we are able to capture the game window. Progress.<\/figcaption><\/figure>\n\n\n\n<p>One of the first things I noticed after doing this is that there are a number of visual cues around the screen which might be helpful to time the simulated keystrokes. One of these was the way that the target arrows pulsed in time with the music.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"180\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Pulsing-Arrows.gif\" alt=\"\" class=\"wp-image-792\"\/><figcaption class=\"wp-element-caption\">The monochromatic arrow areas pulsing in time with the music.<\/figcaption><\/figure>\n\n\n\n<p>At this point, I started working in parallel on being able to trigger OpenEmu from Max.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Triggering Key Presses in OpenEmu<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">A Major Catch<\/h3>\n\n\n\n<p>This part of the process ended up being a little more involved, due to the way that OpenEmu captures keyboard events. The initial plan was to ask Max to trigger keyboard input using something like <a rel=\"noreferrer noopener\" href=\"http:\/\/www.11olsen.de\/code\/category\/6-max-msp-externals\" target=\"_blank\">11olsen&#8217;s<\/a> <code class=\"max object\" data-inlets=\"1\" data-outlets=\"0\">11strokes<\/code> object. Unfortunately, OpenEmu captures keyboard input events a lot lower than Max can send them, so it won\u2019t respond to AppleEvents or simulated keyboard input.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">OSCulator-in-the-Middle<\/h3>\n\n\n\n<p>The solution was to creating a <a rel=\"noreferrer noopener\" href=\"https:\/\/www.osculator.net\/doc\/manual:hid_output\" target=\"_blank\">virtual joystick with OSCulator<\/a>, and have Max pipe OSC encoded instructions to it that could be converted to HID events.(See <a href=\"https:\/\/github.com\/OpenEmu\/OpenEmu\/issues\/1169\">https:\/\/github.com\/OpenEmu\/OpenEmu\/issues\/1169<\/a>). To create the virtual joystick, you need to install a system extension.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"532\" height=\"266\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-Virtual-Joystick.png\" alt=\"\" class=\"wp-image-813\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-Virtual-Joystick.png 532w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-Virtual-Joystick-300x150.png 300w\" sizes=\"auto, (max-width: 532px) 100vw, 532px\" \/><\/figure>\n\n\n\n<p>After installing OSCulator&#8217;s Virtual Joystick system extension and setting up the OSC routes, I was able to map OSC messages to HID button events.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"746\" height=\"400\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSculator-OSC-Routes.png\" alt=\"\" class=\"wp-image-816\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSculator-OSC-Routes.png 746w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSculator-OSC-Routes-300x161.png 300w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSculator-OSC-Routes-620x332.png 620w\" sizes=\"auto, (max-width: 746px) 100vw, 746px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"385\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu-1024x385.png\" alt=\"\" class=\"wp-image-814\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu-1024x385.png 1024w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu-300x113.png 300w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu-768x288.png 768w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu-620x233.png 620w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/OSCulator-to-OpenEmu.png 1406w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">OSC encoded inputs in OSCulator are translated to HID output events, which are mapped to Up\/Down\/Left\/Right inputs in OpenEmu.<\/figcaption><\/figure>\n\n\n\n<p>Crisis averted. Back to the fun stuff.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Identifying Arrows<\/h2>\n\n\n\n<p>A key part of having Max play DDR autonomously is that it needs to be able to understand when an arrow passes the target area. Like the pulsing monochrome arrows in the target zone, the rising arrows also have a few characteristics: the centre pulses white, and the arrow shape&#8217;s hue rotates through a variety of colours.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"180\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Pulsing-Arrows-1.gif\" alt=\"\" class=\"wp-image-809\"\/><figcaption class=\"wp-element-caption\">As the arrows ascend up the screen, they pulse in time with the track.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"180\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Pulsing-Grey.gif\" alt=\"\" class=\"wp-image-808\"\/><figcaption class=\"wp-element-caption\">When an arrow passes over the target zone, the internal colour inverts to white.<\/figcaption><\/figure>\n\n\n\n<p>It took a bit of thinking (and a bit of experimenting) about how best to identify arrows as they pass by the target zone. I came up with a series of masks which I thought might help me draw out useful information (and ignore the background area around them).<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"115\" height=\"30\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Hue-Mask2.png\" alt=\"\" class=\"wp-image-801\"\/><figcaption class=\"wp-element-caption\">Centre Zones<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"115\" height=\"30\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Hue-Mask3.png\" alt=\"\" class=\"wp-image-802\"\/><figcaption class=\"wp-element-caption\">Arrowhead Outlines<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"115\" height=\"30\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Hue-Mask4.png\" alt=\"\" class=\"wp-image-803\"\/><figcaption class=\"wp-element-caption\">Arrowhead Shapes (Filled)<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"115\" height=\"30\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Hue-Mask.png\" alt=\"\" class=\"wp-image-798\"\/><figcaption class=\"wp-element-caption\">Arrow Outlines<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"115\" height=\"30\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Hue-Mask1.png\" alt=\"\" class=\"wp-image-799\"\/><figcaption class=\"wp-element-caption\">Arrow Shapes (Filled) \u2014 this is the one I ended up using.<\/figcaption><\/figure>\n\n\n\n<p>One initial thought was to watch the internal section of the rising arrow and wait until it goes white (using the &#8216;Centre Zones&#8217; mask below to concentrate on this part of the arrow). This produced some positive results until I noticed in some of the more fast-paced songs that it only pulsed white on quarter-notes\u2026&nbsp;which meant that fast songs with eighth-notes were overlooked. I decided that it might be best to use some of the other masks to try to identify a shift in from monochromatic to colour in the target zone.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"576\" height=\"300\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Problematic-Notes.png\" alt=\"\" class=\"wp-image-810\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Problematic-Notes.png 576w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Problematic-Notes-300x156.png 300w\" sizes=\"auto, (max-width: 576px) 100vw, 576px\" \/><figcaption class=\"wp-element-caption\">Watching the centre section of the arrows turn white is OK for quarter-notes, but eighth-notes pass by unnoticed.<\/figcaption><\/figure>\n\n\n\n<p>The way I ended up identifying arrows with moderate success was by masking the arrow target areas, and watching for increases in chrominance. Tracking the white parts of the arrows meant that I couldn&#8217;t identify notes on off-beats,&nbsp;so switching the approach to identify increases in chrominance as the arrows passed the&nbsp;target should help overcome this obstacle.<\/p>\n\n\n\n<p>The arrows in the target frame are pulsing, but they remain grey (which means that the R, G, B channels are roughly equal). When an arrow event passes through the target area though, it brings colour in to the frame. The amount of colour can be identified by converting the RGB matrix into an HSL matrix (<code class=\"max object\" data-inlets=\"1\" data-outlets=\"2\">jit.rgb2hsl<\/code>), then piping the third outlet (saturation) into a <code class=\"max object\" data-inlets=\"1\" data-outlets=\"4\">jit.3m<\/code> and watching the &#8216;mean&#8217; levels of the single channel matrix.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"817\" height=\"231\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Masked-Arrows-to-HSL.png\" alt=\"\" class=\"wp-image-796\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Masked-Arrows-to-HSL.png 817w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Masked-Arrows-to-HSL-300x85.png 300w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Masked-Arrows-to-HSL-768x217.png 768w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Masked-Arrows-to-HSL-620x175.png 620w\" sizes=\"auto, (max-width: 817px) 100vw, 817px\" \/><figcaption class=\"wp-element-caption\">Arrow area is masked, and the result is sent to jit.rgb2hsl to identify deviation from monochrome.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Watching Changes in Chroma<\/h2>\n\n\n\n<p>In the bottom right corner of the video, I&#8217;ve created a collection of <code class=\"max object\" data-inlets=\"1\" data-outlets=\"2\">multislider<\/code> objects to illustrate a running history of how Max understands the arrows as they pass the target area. Note that we have spikes that indicate the highest point of saturation in colour that indicates when the arrows are most aligned with the arrow target areas. While we can use this information to identify when an arrow has aligned with the arrow frame with quite good accuracy, we (unfortunately) determine the peak value when the arrow moves away from the target area, which would mean that we would trigger the events too late. Perhaps a different approach would be to ask Max to trigger an event when it crosses a threshold, and use this downturn event to reset the state with a onebang (allowing arrows to be triggered again).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Limiting Input to Songs Only<\/h2>\n\n\n\n<p>So that I didn&#8217;t have to juggle with starting and stopping Max from acting when it shouldn&#8217;t, one of the final touches I added was to disable arrow triggers if part of the game screen wasn&#8217;t in view. (This is why you might noticed Max go to sleep in between tracks.) Max will watch the score part of the screen to understand when to trigger arrow events. This ensures that arrows are not triggered on Demonstration screens, or other spurious instances of colour in the masked areas.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"522\" src=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play-1024x522.png\" alt=\"\" class=\"wp-image-806\" srcset=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play-1024x522.png 1024w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play-300x153.png 300w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play-768x391.png 768w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play-620x316.png 620w, https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Do-Play-Dont-Play.png 1170w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">We know that a song is playing when two features are on the screen. The frame around the successbar and the border on the score.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Future Improvements<\/h2>\n\n\n\n<p>The video at the start of this post shows an example of Max playing some of the more difficult tracks in the game:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;If You Were Here&#8221; \u2014&nbsp;Jennifer. [Paramount \ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6]<\/li>\n\n\n\n<li>&#8220;Afronova&#8221; \u2014&nbsp;Re-Venge. [Catastrophic \ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6]<\/li>\n\n\n\n<li>&#8220;Dynamie Rave&#8221; \u2014&nbsp;Naoki. [Catastrophic \ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6\ud83e\uddb6]<\/li>\n<\/ul>\n\n\n\n<p>As can be seen, there are occasions where the timing of the triggered arrow events is not quite right. The system completes &#8220;If You Were Here&#8221; and &#8220;Dynamite Rave&#8221; fairly well, but struggles a bit with &#8220;Afronova&#8221;. This is mostly due to limitations in my implementation: as I&#8217;m purely using the screen to identify the events, the system gets easily fooled by rapid repeats when it can&#8217;t discern a drop in colour between frames.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Alternative Approaches<\/h3>\n\n\n\n<p>There might be some creative ways to get Max to follow the BPM of the track a little more acutely (and therefore quantize arrow trigger events) by performing some kind of beat detection on the music track.  Alternatively, we might be able to determine the BPM of the track by watching the rate at which the target arrows pulse. Instead of just watching the arrows when they enter the frame, maybe it might be more robust to measure the optical flow of the rising arrows and time their triggers with a sub-frame temporal accuracy.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Patcher<\/h2>\n\n\n\n<p>There are a couple of other things going on in the patcher if you want to download and have a snoop around. (Of course, you&#8217;ll need to do some setup with OpenEmu and OSCulator.)<\/p>\n\n\n\n<div class=\"wp-block-file\"><a id=\"wp-block-file--media-311b78e1-247a-4249-91b0-fe66c8a8b6f8\" href=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Max-Plays-DDR.txt\">Max-Plays-DDR<\/a><a href=\"https:\/\/x37v.com\/x37v\/wp-content\/uploads\/2023\/01\/Max-Plays-DDR.txt\" class=\"wp-block-file__button wp-element-button\" download aria-describedby=\"wp-block-file--media-311b78e1-247a-4249-91b0-fe66c8a8b6f8\">Download<\/a><\/div>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve been playing some old games with OpenEmu recently, and got hooked on the idea of automating DDR game input by reading content from the screen with Max and sending virtual keystrokes back to OpenEmu. Here\u2019s a quick example of what I ended up with: The backstory\u2026 It all started when I found some PlayStation [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2}},"categories":[13],"tags":[31,30,11,32,23,4,34],"class_list":["post-783","post","type-post","status-publish","format-standard","hentry","category-max","tag-cv","tag-emulation","tag-games","tag-jitter","tag-max","tag-osc","tag-osculator"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p4SgL0-cD","_links":{"self":[{"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/posts\/783","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/comments?post=783"}],"version-history":[{"count":21,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/posts\/783\/revisions"}],"predecessor-version":[{"id":825,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/posts\/783\/revisions\/825"}],"wp:attachment":[{"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/media?parent=783"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/categories?post=783"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/x37v.com\/x37v\/wp-json\/wp\/v2\/tags?post=783"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}