{"id":2072,"date":"2023-07-03T23:58:12","date_gmt":"2023-07-03T23:58:12","guid":{"rendered":"https:\/\/writingagame.com\/?p=2072"},"modified":"2026-04-08T17:04:00","modified_gmt":"2026-04-08T17:04:00","slug":"chapter-7-external-files-android-assets","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2023\/07\/03\/chapter-7-external-files-android-assets\/","title":{"rendered":"Chapter 7. External files, Android assets"},"content":{"rendered":"\n<p><strong>Tags:<\/strong> <em>Android NDK, Asset Manager, JNI, File I\/O, Gradle, APK Timestamp, Cross-Platform Consistency<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>The idea is the same as in previous chapter &#8211; to place the data files within the reach of the executable. However, in Android&#8217;s case we are dealing NOT with the &#8220;executable&#8221;, but with APK. Besides, we have to deliver data files not just to different folder, but to different device. We can do that through a special &#8220;<strong>assets<\/strong>&#8221; folder that will be embedded into APK, that will be eventually installed on destination device.<\/p>\n\n\n\n<p>In Android Studio it&#8217;s <strong>app\/assets<\/strong>.<\/p>\n\n\n\n<p>Physical location: <em>C:\\CPP\\a999hello\\pa\\app\\src\\main\\assets<\/em><\/p>\n\n\n\n<p>1. Start <em>Android Studio<\/em>, open <em>C:\\CPP\\a999hello\\pa<\/em> solution.<\/p>\n\n\n\n<p>If <strong>app\/assets<\/strong> is not there, right-click on  <em><strong>app<\/strong> -&gt; New -&gt; Folder-&gt; Assets Folder<\/em>, Target Source Set <em>&#8211; main<\/em>, <strong>Finish<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Next step is to copy our data files\/folders to <em>assets <\/em>directory (prior to building APK). After wasting some time on <em>CMakeLists.txt<\/em>, found out that smart people do it in <em>build.gradle<\/em>.<\/p>\n\n\n\n<p>2. Open  <em>Gradle Scripts \/ build.gradle (<strong>Mobile :app<\/strong>)<\/em> and add to the very end following code:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; first-line: 54; title: ; notranslate\" title=\"\">\ntask copyFiles(type: Copy) {\n    duplicatesStrategy = DuplicatesStrategy.INCLUDE\n    from &quot;..\/..\/dt&quot;\n    into &quot;.\/src\/main\/assets\/dt&quot;\n}\npreBuild.dependsOn(copyFiles)\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>from <\/em>and <em>into <\/em>pathes are defined by physical script location in <em>C:\\CPP\\a999hello\\pa\\<strong>app<\/strong><\/em>.<\/li>\n\n\n\n<li>After modifying <em>build.gradle<\/em>, the <em>run <\/em>button (green arrow) is disabled (grayed out). To enable it back, close Android Studio and open it again.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Our data files will now be embedded as <strong>assets <\/strong>into the APK and installed on the target device.<\/p>\n\n\n\n<p>Next step is to <strong>unpack <\/strong>them to Android&#8217;s local hard drive.<\/p>\n\n\n\n<p>While we can access and use them directly through the special Assets Interface, we still need to remember that assets are NOT files in the usual sense. The difference:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Assets can&#8217;t be accessed via common files tools. Via dedicated Assets Interface only.<\/li>\n\n\n\n<li>They are read-only. You ca<strong>n&#8217;t<\/strong> modify them or create new ones programmatically.<\/li>\n\n\n\n<li>They are stored as a part of the APK in compressed form. Assets interface just extracts them on the fly when requested.<\/li>\n<\/ul>\n\n\n\n<p>No doubts that eventually we&#8217;ll have to deal with normal files too. It can be program-generated files, user-defined data or files downloaded from the Web. Anyway, we won&#8217;t be able to treat them the same way as assets.<\/p>\n\n\n\n<p>Sure, we can treat assets as assets and files as files. However, such dualism doesn&#8217;t look very consistent. Besides, it will require separate implementation or even <em>imitation <\/em>on the Windows side, which seems totally confusing and unreasonable.<\/p>\n\n\n\n<p>In short words, I think, the best solution is to unpack assets and to save them as usual files, so we can use normal files toolkit consistently across all our platforms.<\/p>\n\n\n\n<p>First challenge &#8211; is how to get a list of assets to unpack.<\/p>\n\n\n\n<p>We have an Assets NDK interface, that can list files in given folder (but not sub-folders), which is not enough for us. Or we can use <strong>Java Layer Interface<\/strong>, which can show folder&#8217;s content <strong>including <\/strong>sub-folders. I will use&nbsp;<strong>list_assets()<\/strong>&nbsp;function by&nbsp;<em><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/marcel303\" target=\"_blank\">Marcel Smit<\/a><\/em>. This function returns given folder&#8217;s content in a form of strings vector.<\/p>\n\n\n\n<p>Using this function we CAN recursively scan assets through all sub-folders starting from &#8220;dt&#8221; and then copy files to a file system.<\/p>\n\n\n\n<p>For saving (writing) files we&#8217;ll need to figure out files root directory, like in Windows. Just in case, mine<em>&nbsp;<\/em>turned out to be&nbsp;<em>\/data\/user\/0\/com.writingagame.pa_hello\/files<\/em>.<\/p>\n\n\n\n<p>One more thing: we don&#8217;t want to run assets update every time. We want it only once after APK installed or updated. We will save APK&#8217;s timestamp in a separate file and then compare APK&#8217;s timestamp with saved timestamp from the file. If they match, assets are fresh enough, if not &#8211; run the update. After assets update is finished, we will create or overwrite saved timestamp file. Another challenge here &#8211; is how to extract <em>apkLastUpdateTime<\/em> from the APK. Since there is no direct access, we&#8217;ll again use Java Layer Interface.<\/p>\n\n\n\n<p>3. So, open&nbsp;<em>main.cpp<\/em>&nbsp;and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [11,12,13,15,124,155,286,287,288,290]; title: ; notranslate\" title=\"\">\n#include &quot;platform.h&quot;\n#include &lt;jni.h&gt;\n#include &lt;EGL\/egl.h&gt;\n#include &lt;game-activity\/GameActivity.cpp&gt;\n#include &lt;game-text-input\/gametextinput.cpp&gt;\n#include &lt;game-activity\/native_app_glue\/android_native_app_glue.c&gt;\n#include &quot;TheApp.cpp&quot;\n#include &lt;string&gt;\n#include &lt;vector&gt;\n#include &lt;sys\/stat.h&gt; \/\/mkdir for Android\nstd::string filesRoot;\nTheApp theApp;\nstruct android_app* pAndroidApp = NULL;\nEGLDisplay androidDisplay = EGL_NO_DISPLAY;\nEGLSurface androidSurface = EGL_NO_SURFACE;\nEGLContext androidContext = EGL_NO_CONTEXT;\nbool bExitApp = false;\nint screenSize&#91;2] = {0,0};\nvoid android_init_display() {\n    \/\/ Choose your render attributes\n    constexpr EGLint attribs&#91;] = {\n            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,\n            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,\n            EGL_BLUE_SIZE, 8,\n            EGL_GREEN_SIZE, 8,\n            EGL_RED_SIZE, 8,\n            EGL_DEPTH_SIZE, 24,\n            EGL_NONE\n    };\n    \/\/ The default display is probably what you want on Android\n    auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);\n    eglInitialize(display, nullptr, nullptr);\n    \/\/ figure out how many configs there are\n    EGLint numConfigs;\n    eglChooseConfig(display, attribs, nullptr, 0, &amp;numConfigs);\n    \/\/ get the list of configurations\n    std::unique_ptr&lt;EGLConfig&#91;]&gt; supportedConfigs(new EGLConfig&#91;numConfigs]);\n    eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &amp;numConfigs);\n    \/\/ Find a config we like.\n    \/\/ Could likely just grab the first if we don&#039;t care about anything else in the config.\n    \/\/ Otherwise hook in your own heuristic\n    auto config = *std::find_if(\n            supportedConfigs.get(),\n            supportedConfigs.get() + numConfigs,\n            &#91;&amp;display](const EGLConfig &amp;config) {\n                EGLint red, green, blue, depth;\n                if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &amp;red)\n                    &amp;&amp; eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &amp;green)\n                    &amp;&amp; eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &amp;blue)\n                    &amp;&amp; eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &amp;depth)) {\n                    \/\/aout &lt;&lt; &quot;Found config with &quot; &lt;&lt; red &lt;&lt; &quot;, &quot; &lt;&lt; green &lt;&lt; &quot;, &quot; &lt;&lt; blue &lt;&lt; &quot;, &quot;\n                    \/\/     &lt;&lt; depth &lt;&lt; std::endl;\n                    return red == 8 &amp;&amp; green == 8 &amp;&amp; blue == 8 &amp;&amp; depth == 24;\n                }\n                return false;\n            });\n    \/\/ create the proper window surface\n    EGLint format;\n    eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &amp;format);\n    EGLSurface surface = eglCreateWindowSurface(display, config, pAndroidApp-&gt;window, nullptr);\n    \/\/ Create a GLES 3 context\n    EGLint contextAttribs&#91;] = {\n            EGL_CONTEXT_MAJOR_VERSION, 3,\n            EGL_CONTEXT_MINOR_VERSION, 2,\n            EGL_NONE};\n    EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);\n    \/\/ get some window metrics\n    auto madeCurrent = eglMakeCurrent(display, surface, surface, context);\n    if(!madeCurrent) {\n        ;\n    }\n    androidDisplay = display;\n    androidSurface = surface;\n    androidContext = context;\n}\nvoid android_term_display() {\n    if (androidDisplay != EGL_NO_DISPLAY) {\n        eglMakeCurrent(androidDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);\n        if (androidContext != EGL_NO_CONTEXT) {\n            eglDestroyContext(androidDisplay, androidContext);\n            androidContext = EGL_NO_CONTEXT;\n        }\n        if (androidSurface != EGL_NO_SURFACE) {\n            eglDestroySurface(androidDisplay, androidSurface);\n            androidSurface = EGL_NO_SURFACE;\n        }\n        eglTerminate(androidDisplay);\n        androidDisplay = EGL_NO_DISPLAY;\n    }\n}\nvoid handle_cmd(android_app *pApp, int32_t cmd) {\n    switch (cmd) {\n        case APP_CMD_INIT_WINDOW:\n            android_init_display();\n            \/\/updateRenderArea\n            EGLint width,height;\n            eglQuerySurface(androidDisplay, androidSurface, EGL_WIDTH, &amp;width);\n            eglQuerySurface(androidDisplay, androidSurface, EGL_HEIGHT, &amp;height);\n            screenSize&#91;0] = 0;\n            screenSize&#91;1] = 0;\n            theApp.onScreenResize(width,height);\n            break;\n        case APP_CMD_TERM_WINDOW:\n            android_term_display();\n            break;\n        default:\n            break;\n    }\n}\nstatic std::vector&lt;std::string&gt; list_assets(android_app* app, const char* asset_path)\n{ \/\/by Marcel Smit, found on https:\/\/github.com\/android\/ndk-samples\/issues\/603\n    std::vector&lt;std::string&gt; result;\n    JNIEnv* env = nullptr;\n    app-&gt;activity-&gt;vm-&gt;AttachCurrentThread(&amp;env, nullptr);\n    auto context_object = app-&gt;activity-&gt;javaGameActivity;\/\/clazz;\n    auto getAssets_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getAssets&quot;, &quot;()Landroid\/content\/res\/AssetManager;&quot;);\n    auto assetManager_object = env-&gt;CallObjectMethod(context_object, getAssets_method);\n    auto list_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(assetManager_object), &quot;list&quot;, &quot;(Ljava\/lang\/String;)&#91;Ljava\/lang\/String;&quot;);\n    jstring path_object = env-&gt;NewStringUTF(asset_path);\n    auto files_object = (jobjectArray)env-&gt;CallObjectMethod(assetManager_object, list_method, path_object);\n    env-&gt;DeleteLocalRef(path_object);\n    auto length = env-&gt;GetArrayLength(files_object);\n    for (int i = 0; i &lt; length; i++)\n    {\n        jstring jstr = (jstring)env-&gt;GetObjectArrayElement(files_object, i);\n        const char* filename = env-&gt;GetStringUTFChars(jstr, nullptr);\n        if (filename != nullptr)\n        {\n            result.push_back(filename);\n            env-&gt;ReleaseStringUTFChars(jstr, filename);\n        }\n        env-&gt;DeleteLocalRef(jstr);\n    }\n    app-&gt;activity-&gt;vm-&gt;DetachCurrentThread();\n    return result;\n}\nint updateAssets() {\n    \/\/get APK apkLastUpdateTime timestamp\n    JNIEnv* env = nullptr;\n    pAndroidApp-&gt;activity-&gt;vm-&gt;AttachCurrentThread(&amp;env, nullptr);\n    jobject context_object = pAndroidApp-&gt;activity-&gt;javaGameActivity;\/\/clazz;\n    jmethodID getPackageNameMid_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getPackageName&quot;, &quot;()Ljava\/lang\/String;&quot;);\n    jstring packageName = (jstring)env-&gt;CallObjectMethod(context_object, getPackageNameMid_method);\n    jmethodID getPackageManager_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getPackageManager&quot;, &quot;()Landroid\/content\/pm\/PackageManager;&quot;);\n    jobject packageManager_object = env-&gt;CallObjectMethod(context_object, getPackageManager_method);\n    jmethodID getPackageInfo_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(packageManager_object), &quot;getPackageInfo&quot;, &quot;(Ljava\/lang\/String;I)Landroid\/content\/pm\/PackageInfo;&quot;);\n    jobject packageInfo_object = env-&gt;CallObjectMethod(packageManager_object, getPackageInfo_method, packageName, 0x0);\n    jfieldID updateTimeFid = env-&gt;GetFieldID(env-&gt;GetObjectClass(packageInfo_object), &quot;lastUpdateTime&quot;, &quot;J&quot;);\n    long int apkLastUpdateTime = env-&gt;GetLongField(packageInfo_object, updateTimeFid);\n    \/\/ APK updateTime timestamp retrieved\n    \/\/ compare with saved timestamp\n    std::string updateTimeFilePath = filesRoot + &quot;\/dt\/apk_update_time.bin&quot;;\n    FILE* inFile = fopen(updateTimeFilePath.c_str(), &quot;r&quot;);\n    if (inFile != NULL)\n    {\n        long int savedUpdateTime;\n        fread(&amp;savedUpdateTime, 1, sizeof(savedUpdateTime), inFile);\n        fclose(inFile);\n        if (savedUpdateTime == apkLastUpdateTime) {\n            mylog(&quot;Assets are up to date.\\n&quot;);\n            return 0;\n        }\n    }\n    \/\/ if here - need to update assets\n    AAssetManager* am = pAndroidApp-&gt;activity-&gt;assetManager;\n    std::vector&lt;std::string&gt; dirsToCheck; \/\/list of assets folders to check\n    dirsToCheck.push_back(&quot;dt&quot;); \/\/root folder\n    std::vector&lt;std::string&gt; filesToUpdate;\n    while (dirsToCheck.size() &gt; 0) {\n        \/\/open last element from directories vector\n        std::string dirPath = dirsToCheck.back();\n        dirsToCheck.pop_back(); \/\/delete last element\n        \/\/mylog(&quot;Scanning directory &lt;%s&gt;\\n&quot;, dirPath.c_str());\n        \/\/make sure folder exists on local drive\n        std::string outPath = filesRoot + &quot;\/&quot; + dirPath; \/\/ .c_str();\n        struct stat info;\n        int statRC = stat(outPath.c_str(), &amp;info);\n        if (statRC == 0)\n            mylog(&quot;%s folder exists.\\n&quot;, outPath.c_str());\n        else {\n            \/\/ mylog(&quot;Try to create %s\\n&quot;, outPath.c_str());\n            int status = mkdir(outPath.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);\n            if (status == 0)\n                mylog(&quot;%s folder added.\\n&quot;, outPath.c_str());\n            else {\n                mylog(&quot;ERROR creating, status=%d, errno: %s.\\n&quot;, status, std::strerror(errno));\n            }\n        }\n        \/\/get folder&#039;s content\n        std::vector&lt;std::string&gt; dirItems = list_assets(pAndroidApp, dirPath.c_str());\n        int itemsN = dirItems.size();\n        \/\/mylog(&quot;%d items.\\n&quot;,itemsN);\n        \/\/scan directory items\n        for (int i = 0; i &lt; itemsN; i++) {\n            std::string itemPath = dirPath + &quot;\/&quot; + dirItems.at(i);\n            \/\/mylog(&quot;item %d: &lt;%s&gt; - &quot;, i,itemPath.c_str());\n            \/\/try to open it to see if it&#039;s a file\n            AAsset* asset = AAssetManager_open(am, itemPath.c_str(), AASSET_MODE_UNKNOWN);\n            if (asset != NULL) {\n                \/\/it&#039;s a file\n                \/\/mylog(&quot;add file to list: %s\\n&quot;,dirItems.at(i).c_str());\n                filesToUpdate.push_back(itemPath);\n                AAsset_close(asset);\n            }\n            else {\n                dirsToCheck.push_back(itemPath);\n                \/\/mylog(&quot;It&#039;s a folder, add to folders list to check.\\n&quot;);\n            }\n        }\n        dirItems.clear();\n    }\n    \/\/save files\n    int buffSize = 1000000; \/\/1Mb, guess, should be enough?\n    char* buff = new char&#91;buffSize];\n    int filesN = filesToUpdate.size();\n    mylog(&quot;%d files to update.\\n&quot;,filesN);\n    for(int i=0;i&lt;filesN;i++){\n        std::string assetPath = filesToUpdate.at(i);\n        AAsset* asset = AAssetManager_open(am, assetPath.c_str(), AASSET_MODE_UNKNOWN);\n        uint32_t assetSize = AAsset_getLength(asset);\n        \/\/mylog(&quot;saving file %d: %s %d\\n&quot;,i,assetPath.c_str(),assetSize);\n        if (assetSize &gt; buffSize) {\n            mylog(&quot;ERROR in main.cpp-&gt;updateAssets(): File %s is too big (%d).\\n&quot;, assetPath.c_str(),assetSize);\n            return -1;\n        }\n        AAsset_read(asset, buff, assetSize);\n        std::string outPath = filesRoot + &quot;\/&quot; + assetPath;\n        FILE* outFile = fopen(outPath.c_str(), &quot;w&quot;);\n        if (outFile == NULL) {\n            mylog(&quot;ERROR in main.cpp-&gt;updateAssets(): Can&#039;t create file %s\\n&quot;, assetPath.c_str());\n            return -1;\n        }\n        fwrite(buff, 1, assetSize, outFile);\n        fflush(outFile);\n        fclose(outFile);\n        mylog(&quot;%d: %s, size %d\\n&quot;, i+1, assetPath.c_str(),assetSize);\n        AAsset_close(asset);\n    }\n    delete&#91;] buff;\n    \/\/ save updateTime\n    FILE* outFile = fopen(updateTimeFilePath.c_str(), &quot;w&quot;);\n    if (outFile != NULL)\n    {\n        fwrite(&amp;apkLastUpdateTime, 1, sizeof(apkLastUpdateTime), outFile);\n        fflush(outFile);\n        fclose(outFile);\n    }\n    else\n        mylog(&quot;ERROR creating %s\\n&quot;, updateTimeFilePath.c_str());\n    return 1;\n}\n\/*!\n * This the main entry point for a native activity\n *\/\nvoid android_main(struct android_app *pApp) {\n    pAndroidApp = pApp;\n    \/\/ register an event handler for Android events\n    pApp-&gt;onAppCmd = handle_cmd;\n    myPollEvents(); \/\/this will wait for display initialization\n    \/\/retrieving files root\n    filesRoot.assign(pAndroidApp-&gt;activity-&gt;internalDataPath);\n    mylog(&quot;filesRoot = %s\\n&quot;, filesRoot.c_str());\n    updateAssets();\n    theApp.run();\n    android_term_display();\n    std::terminate();\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Turn on Android, unlock, plug in, allow.<\/p>\n\n\n\n<p>Run the app (green arrow). On the screen &#8211; same spinning triangle. But&#8230;<\/p>\n\n\n\n<p>Open <em>Logcat<\/em> (in Android Studio&#8217;s bottom menu), in the search string type &#8220;mylog&#8221;.<\/p>\n\n\n\n<p>Here we can see the assets updating process.<\/p>\n\n\n\n<p>Now stop debugging (red square), clear Logcat (optional), and start the app again.<\/p>\n\n\n\n<p>What do we have in Logcat this time?:<\/p>\n\n\n\n<p><strong>Assets are up to date<\/strong>!<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b04\/c07\/01.jpg\"><\/p>\n\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>By established tradition, here is a&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/writingagame.com\/index.php\/2023\/05\/11\/github\/\" target=\"_blank\">GitHub<\/a> link:<\/p>\n\n\n\n<p><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/bkantemir\/_wg_407\" target=\"_blank\">https:\/\/github.com\/bkantemir\/_wg_407<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Tags: Android NDK, Asset Manager, JNI, File I\/O, Gradle, APK Timestamp, Cross-Platform Consistency The idea is the same as in previous chapter &#8211; to place the data files within the reach of the executable. However, in Android&#8217;s case we are dealing NOT with the &#8220;executable&#8221;, but with APK. Besides, we have to deliver data files [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-2072","post","type-post","status-publish","format-standard","hentry","category-cross-platform-3d"],"_links":{"self":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/2072","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/comments?post=2072"}],"version-history":[{"count":48,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/2072\/revisions"}],"predecessor-version":[{"id":4283,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/2072\/revisions\/4283"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=2072"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=2072"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=2072"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}