diff --git a/source/build/include/cache1d.h b/source/build/include/cache1d.h
index 9c77ea13d..940ee615c 100644
--- a/source/build/include/cache1d.h
+++ b/source/build/include/cache1d.h
@@ -91,6 +91,7 @@ CACHE1D_FIND_REC *klistpath(const char *path, const char *mask, int type);
 
 extern int32_t lz4CompressionLevel;
 int32_t kdfread_LZ4(void *buffer, int dasizeof, int count, buildvfs_kfd fil);
+int32_t kdfread_LZ4(void* buffer, int dasizeof, int count, FileReader& fil);
 void dfwrite_LZ4(const void *buffer, int dasizeof, int count, buildvfs_FILE fil);
 
 class KFileReaderInterface : public FileReaderInterface
@@ -144,6 +145,13 @@ inline FileReader kopenFileReader(const char* name, int where)
 	return FileReader(fri);
 }
 
+// This is only here to mark a file as not being part of the game assets (e.g. savegames)
+// These should be handled differently (e.g read from a userdata directory or similar things.)
+inline FileReader fopenFileReader(const char* name, int where)
+{
+	return kopenFileReader(name, 0);
+}
+
 inline bool testkopen(const char* name, int where)
 {
 	int handle = where == 0 ? kopen4loadfrommod(name, 0) : kopen4load(name, where);
diff --git a/source/build/src/cache1d.cpp b/source/build/src/cache1d.cpp
index b21abdc71..937ecb688 100644
--- a/source/build/src/cache1d.cpp
+++ b/source/build/src/cache1d.cpp
@@ -1707,6 +1707,32 @@ int32_t kdfread_LZ4(void *buffer, int dasizeof, int count, buildvfs_kfd fil)
     return decompressedLength/dasizeof;
 }
 
+int32_t kdfread_LZ4(void* buffer, int dasizeof, int count, FileReader &fil)
+{
+	int32_t leng;
+
+	// read compressed data length
+	if (fil.Read(&leng, sizeof(leng)) != sizeof(leng))
+		return -1;
+
+	leng = B_LITTLE32(leng);
+
+	char* pCompressedData = compressedDataStackBuf;
+
+	if (leng > ARRAY_SSIZE(compressedDataStackBuf))
+		pCompressedData = (char*)Xaligned_alloc(16, leng);
+
+	if (fil.Read(pCompressedData, leng) != leng)
+		return -1;
+
+	int32_t decompressedLength = LZ4_decompress_safe(pCompressedData, (char*)buffer, leng, dasizeof * count);
+
+	if (pCompressedData != compressedDataStackBuf)
+		Xaligned_free(pCompressedData);
+
+	return decompressedLength / dasizeof;
+}
+
 
 void dfwrite_LZ4(const void *buffer, int dasizeof, int count, buildvfs_FILE fil)
 {
diff --git a/source/duke3d/src/demo.cpp b/source/duke3d/src/demo.cpp
index 349d42faf..93fbcf7eb 100644
--- a/source/duke3d/src/demo.cpp
+++ b/source/duke3d/src/demo.cpp
@@ -37,7 +37,7 @@ BEGIN_DUKE_NS
 char g_firstDemoFile[BMAX_PATH];
 
 buildvfs_FILE g_demo_filePtr{};  // write
-buildvfs_kfd g_demo_recFilePtr = buildvfs_kfd_invalid;  // read
+FileReader g_demo_recFilePtr;  // read
 
 int32_t g_demo_cnt;
 int32_t g_demo_goalCnt=0;
@@ -101,8 +101,8 @@ static int32_t G_OpenDemoRead(int32_t g_whichDemo) // 0 = mine
         demofnptr = demofn;
     }
 
-    g_demo_recFilePtr = kopen4loadfrommod(demofnptr, g_loadFromGroupOnly);
-    if (g_demo_recFilePtr == buildvfs_kfd_invalid)
+    g_demo_recFilePtr = fopenFileReader(demofnptr, g_loadFromGroupOnly);
+	if (!g_demo_recFilePtr.isOpen())
         return 0;
 
     Bassert(g_whichDemo >= 1);
@@ -110,7 +110,7 @@ static int32_t G_OpenDemoRead(int32_t g_whichDemo) // 0 = mine
     if (i)
     {
         OSD_Printf(OSD_ERROR "There were errors opening demo %d (code: %d).\n", g_whichDemo, i);
-        kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+        g_demo_recFilePtr.Close();
         return 0;
     }
 
@@ -147,8 +147,7 @@ void G_OpenDemoWrite(void)
 
     if (ud.recstat == 2)
     {
-        kclose(g_demo_recFilePtr);
-        g_demo_recFilePtr = buildvfs_kfd_invalid;
+		g_demo_recFilePtr.Close();
     }
 
     if ((g_player[myconnectindex].ps->gm&MODE_GAME) && g_player[myconnectindex].ps->dead_flag)
@@ -329,13 +328,13 @@ static int32_t Demo_ReadSync(int32_t errcode)
     uint16_t si;
     int32_t i;
 
-    if (kread(g_demo_recFilePtr, &si, sizeof(uint16_t)) != sizeof(uint16_t))
+    if (g_demo_recFilePtr.Read(&si, sizeof(uint16_t)) != sizeof(uint16_t))
         return errcode;
 
     i = si;
     if (demo_hasseeds)
     {
-        if (kread(g_demo_recFilePtr, g_demo_seedbuf, i) != i)
+        if (g_demo_recFilePtr.Read(g_demo_seedbuf, i) != i)
             return errcode;
     }
 
@@ -348,7 +347,7 @@ static int32_t Demo_ReadSync(int32_t errcode)
     {
         int32_t bytes = sizeof(input_t)*i;
 
-        if (kread(g_demo_recFilePtr, recsync, bytes) != bytes)
+        if (g_demo_recFilePtr.Read(recsync, bytes) != bytes)
             return errcode+2;
     }
 
@@ -527,7 +526,7 @@ RECHECK:
         g_player[myconnectindex].ps->gm &= ~MODE_GAME;
         g_player[myconnectindex].ps->gm |= MODE_DEMO;
 
-        lastsyncofs = ktell(g_demo_recFilePtr);
+        lastsyncofs = g_demo_recFilePtr.Tell();
         initsyncofs = lastsyncofs;
         lastsynctic = g_demo_cnt;
         lastsyncclock = (int32_t) totalclock;
@@ -583,7 +582,7 @@ RECHECK:
                     if (Demo_UpdateState(0)==0)
                     {
                         g_demo_cnt = lastsynctic;
-                        klseek(g_demo_recFilePtr, lastsyncofs, SEEK_SET);
+                        g_demo_recFilePtr.Seek(lastsyncofs, FileReader::SeekSet);
                         ud.reccnt = 0;
 
                         totalclock = ototalclock = lockclock = lastsyncclock;
@@ -595,7 +594,7 @@ RECHECK:
                     // update to initial state
                     if (Demo_UpdateState(1) == 0)
                     {
-                        klseek(g_demo_recFilePtr, initsyncofs, SEEK_SET);
+						g_demo_recFilePtr.Seek(initsyncofs, FileReader::SeekSet);
                         g_levelTextTime = 0;
 
                         g_demo_cnt = 1;
@@ -634,7 +633,7 @@ RECHECK:
 
                     bigi = 0;
                     //reread:
-                    if (kread(g_demo_recFilePtr, tmpbuf, 4) != 4)
+                    if (g_demo_recFilePtr.Read(tmpbuf, 4) != 4)
                         CORRUPT(2);
 
                     if (Bmemcmp(tmpbuf, "sYnC", 4)==0)
@@ -655,11 +654,11 @@ RECHECK:
                         }
                         else
                         {
-                            lastsyncofs = ktell(g_demo_recFilePtr);
+                            lastsyncofs = g_demo_recFilePtr.Tell();
                             lastsynctic = g_demo_cnt;
                             lastsyncclock = (int32_t) totalclock;
 
-                            if (kread(g_demo_recFilePtr, tmpbuf, 4) != 4)
+                            if (g_demo_recFilePtr.Read(tmpbuf, 4) != 4)
                                 CORRUPT(7);
                             if (Bmemcmp(tmpbuf, "sYnC", 4))
                                 CORRUPT(8);
@@ -690,7 +689,7 @@ nextdemo:
 nextdemo_nomenu:
                         foundemo = 0;
                         ud.reccnt = 0;
-                        kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+                        g_demo_recFilePtr.Close();
 
                         if (g_demo_goalCnt>0)
                         {
@@ -952,7 +951,7 @@ nextdemo_nomenu:
 #if KRANDDEBUG
                 krd_print("krandplay.log");
 #endif
-                kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+                g_demo_recFilePtr.Close();
             }
 
             return 0;
@@ -960,7 +959,7 @@ nextdemo_nomenu:
     }
 
     ud.multimode = numplayers;  // fixes 2 infinite loops after watching demo
-    kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+	g_demo_recFilePtr.Close();
 
     Demo_FinishProfile();
 
diff --git a/source/duke3d/src/demo.h b/source/duke3d/src/demo.h
index 3be57da92..63699e83d 100644
--- a/source/duke3d/src/demo.h
+++ b/source/duke3d/src/demo.h
@@ -46,7 +46,7 @@ extern int32_t demorec_synccompress_cvar;
 extern int32_t g_demo_cnt;
 extern int32_t g_demo_goalCnt;
 extern int32_t g_demo_paused;
-extern buildvfs_kfd g_demo_recFilePtr;
+extern FileReader g_demo_recFilePtr;
 extern int32_t g_demo_rewind;
 extern int32_t g_demo_showStats;
 extern int32_t g_demo_totalCnt;
diff --git a/source/duke3d/src/gamevars.cpp b/source/duke3d/src/gamevars.cpp
index a75caaad4..77f7e274c 100644
--- a/source/duke3d/src/gamevars.cpp
+++ b/source/duke3d/src/gamevars.cpp
@@ -113,11 +113,11 @@ void Gv_Clear(void)
         hash_free(i);
 }
 
-int Gv_ReadSave(buildvfs_kfd kFile)
+int Gv_ReadSave(FileReader &kFile)
 {
     char tbuf[12];
 
-    if (kread(kFile, tbuf, 12)!=12) goto corrupt;
+    if (kFile.Read(tbuf, 12)!=12) goto corrupt;
     if (Bmemcmp(tbuf, "BEG: EDuke32", 12)) { OSD_Printf("BEG ERR\n"); return 2; }
 
     Gv_Free(); // nuke 'em from orbit, it's the only way to be sure...
@@ -223,7 +223,7 @@ int Gv_ReadSave(buildvfs_kfd kFile)
             }
     }
 
-    if (kread(kFile, tbuf, 12) != 12) return -13;
+    if (kFile.Read(tbuf, 12) != 12) return -13;
     if (Bmemcmp(tbuf, "EOF: EDuke32", 12)) { OSD_Printf("EOF ERR\n"); return 2; }
 
     return 0;
diff --git a/source/duke3d/src/gamevars.h b/source/duke3d/src/gamevars.h
index 970eee591..6a15a1799 100644
--- a/source/duke3d/src/gamevars.h
+++ b/source/duke3d/src/gamevars.h
@@ -158,7 +158,7 @@ void Gv_DumpValues(void);
 void Gv_InitWeaponPointers(void);
 void Gv_RefreshPointers(void);
 void Gv_ResetVars(void);
-int Gv_ReadSave(buildvfs_kfd kFile);
+int Gv_ReadSave(FileReader &kFile);
 void Gv_WriteSave(buildvfs_FILE fil);
 void Gv_Clear(void);
 #else
diff --git a/source/duke3d/src/savegame.cpp b/source/duke3d/src/savegame.cpp
index cf7e0f87f..7ecaee989 100644
--- a/source/duke3d/src/savegame.cpp
+++ b/source/duke3d/src/savegame.cpp
@@ -156,8 +156,8 @@ static void ReadSaveGameHeaders_CACHE1D(CACHE1D_FIND_REC *f)
     for (; f != nullptr; f = f->next)
     {
         char const * fn = f->name;
-        buildvfs_kfd fil = kopen4loadfrommod(fn, 0);
-        if (fil == buildvfs_kfd_invalid)
+        auto fil = fopenFileReader(fn, 0);
+        if (!fil.isOpen())
             continue;
 
         menusave_t & msv = g_internalsaves[g_numinternalsaves];
@@ -200,7 +200,6 @@ static void ReadSaveGameHeaders_CACHE1D(CACHE1D_FIND_REC *f)
         else
             msv.isUnreadable = 1;
 
-        kclose(fil);
     }
 }
 
@@ -303,8 +302,8 @@ void ReadSaveGameHeaders(void)
 
 int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
 {
-    buildvfs_kfd fil = kopen4loadfrommod(fn, 0);
-    if (fil == buildvfs_kfd_invalid)
+    auto fil = fopenFileReader(fn, 0);
+    if (!fil.isOpen())
         return -1;
 
     int32_t i = sv_loadheader(fil, 0, saveh);
@@ -312,7 +311,7 @@ int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
         goto corrupt;
 
     int32_t screenshotofs;
-    if (kread(fil, &screenshotofs, 4) != 4)
+    if (fil.Read(&screenshotofs, 4) != 4)
         goto corrupt;
 
 	TileFiles.tileCreate(TILE_LOADSHOT, 200, 320);
@@ -341,11 +340,9 @@ int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
     }
     tileInvalidate(TILE_LOADSHOT, 0, 255);
 
-    kclose(fil);
     return 0;
 
 corrupt:
-    kclose(fil);
     return 1;
 }
 
@@ -365,9 +362,9 @@ int32_t G_LoadPlayer(savebrief_t & sv)
         int level = -1;
         int skill = -1;
 
-        buildvfs_kfd const fil = kopen4loadfrommod(sv.path, 0);
+		auto fil = fopenFileReader(sv.path, 0);
 
-        if (fil != buildvfs_kfd_invalid)
+        if (fil.isOpen())
         {
             savehead_t h;
             int status = sv_loadheader(fil, 0, &h);
@@ -377,8 +374,6 @@ int32_t G_LoadPlayer(savebrief_t & sv)
                 level = h.levnum;
                 skill = h.skill;
             }
-
-            kclose(fil);
         }
 
         char extfn[BMAX_PATH];
@@ -604,9 +599,9 @@ int32_t G_LoadPlayer(savebrief_t & sv)
         return 0;
     }
 
-    buildvfs_kfd const fil = kopen4loadfrommod(sv.path, 0);
+    auto fil = fopenFileReader(sv.path, 0);
 
-    if (fil == buildvfs_kfd_invalid)
+    if (!fil.isOpen())
         return -1;
 
     ready2send = 0;
@@ -621,7 +616,6 @@ int32_t G_LoadPlayer(savebrief_t & sv)
         else if (h.numplayers != ud.multimode)
             P_DoQuote(QUOTE_SAVE_BAD_PLAYERS, g_player[myconnectindex].ps);
 
-        kclose(fil);
         ototalclock = totalclock;
         ready2send = 1;
 
@@ -689,7 +683,6 @@ int32_t G_LoadPlayer(savebrief_t & sv)
 
     sv_postudload();  // ud.m_XXX = ud.XXX
     VM_OnEvent(EVENT_LOADGAME, g_player[screenpeek].ps->i, screenpeek);
-    kclose(fil);
 
     return 0;
 }
@@ -1010,14 +1003,14 @@ static uint8_t *writespecdata(const dataspec_t *spec, buildvfs_FILE fil, uint8_t
 // (fil>=0 && havedump): first restore dump from file, then restore state from dump
 // (fil<0 && havedump): only restore state from dump
 // (fil>=0 && !havedump): only restore state from file
-static int32_t readspecdata(const dataspec_t *spec, buildvfs_kfd fil, uint8_t **dumpvar)
+static int32_t readspecdata(const dataspec_t *spec, FileReader *fil, uint8_t **dumpvar)
 {
     uint8_t *  dump = dumpvar ? *dumpvar : NULL;
     auto const sptr = spec;
 
     for (; spec->flags != DS_END; spec++)
     {
-        if (fil == buildvfs_kfd_invalid && spec->flags & (DS_NOCHK|DS_STRING|DS_CMP))  // we're updating
+        if (fil == nullptr && spec->flags & (DS_NOCHK|DS_STRING|DS_CMP))  // we're updating
             continue;
 
         if (spec->flags & (DS_LOADFN|DS_SAVEFN))
@@ -1031,7 +1024,7 @@ static int32_t readspecdata(const dataspec_t *spec, buildvfs_kfd fil, uint8_t **
         {
             static char cmpstrbuf[32];
             int const siz  = (spec->flags & DS_STRING) ? Bstrlen((const char *)spec->ptr) : spec->size * spec->cnt;
-            int const ksiz = kread(fil, cmpstrbuf, siz);
+            int const ksiz = fil->Read(cmpstrbuf, siz);
 
             if (ksiz != siz || Bmemcmp(spec->ptr, cmpstrbuf, siz))
             {
@@ -1061,12 +1054,12 @@ static int32_t readspecdata(const dataspec_t *spec, buildvfs_kfd fil, uint8_t **
         if (!ptr || !cnt)
             continue;
 
-        if (fil != buildvfs_kfd_invalid)
+        if (fil != nullptr)
         {
             auto const mem  = (dump && (spec->flags & DS_NOCHK) == 0) ? dump : (uint8_t *)ptr;
             bool const comp = !((spec->flags & DS_CNTMASK) == 0 && spec->size * cnt <= savegame_comprthres);
             int const  siz  = comp ? cnt : cnt * spec->size;
-            int const  ksiz = comp ? kdfread_LZ4(mem, spec->size, siz, fil) : kread(fil, mem, siz);
+            int const  ksiz = comp ? kdfread_LZ4(mem, spec->size, siz, *fil) : fil->Read(mem, siz);
 
             if (ksiz != siz)
             {
@@ -1571,7 +1564,7 @@ static const dataspec_t svgm_anmisc[] =
 static dataspec_gv_t *svgm_vars=NULL;
 #endif
 static uint8_t *dosaveplayer2(buildvfs_FILE fil, uint8_t *mem);
-static int32_t doloadplayer2(buildvfs_kfd fil, uint8_t **memptr);
+static int32_t doloadplayer2(FileReader &fil, uint8_t **memptr);
 static void postloadplayer(int32_t savegamep);
 
 // SVGM snapshot system
@@ -1803,11 +1796,11 @@ int32_t sv_saveandmakesnapshot(buildvfs_FILE fil, char const *name, int8_t spot,
 }
 
 // if file is not an EDuke32 savegame/demo, h->headerstr will be all zeros
-int32_t sv_loadheader(buildvfs_kfd fil, int32_t spot, savehead_t *h)
+int32_t sv_loadheader(FileReader &fil, int32_t spot, savehead_t *h)
 {
     int32_t havedemo = (spot < 0);
 
-    if (kread(fil, h, sizeof(savehead_t)) != sizeof(savehead_t))
+    if (fil.Read(h, sizeof(savehead_t)) != sizeof(savehead_t))
     {
         OSD_Printf("%s %d header corrupt.\n", havedemo ? "Demo":"Savegame", havedemo ? -spot : spot);
         Bmemset(h->headerstr, 0, sizeof(h->headerstr));
@@ -1863,7 +1856,7 @@ int32_t sv_loadheader(buildvfs_kfd fil, int32_t spot, savehead_t *h)
     return 0;
 }
 
-int32_t sv_loadsnapshot(buildvfs_kfd fil, int32_t spot, savehead_t *h)
+int32_t sv_loadsnapshot(FileReader &fil, int32_t spot, savehead_t *h)
 {
     uint8_t *p;
     int32_t i;
@@ -1886,14 +1879,14 @@ int32_t sv_loadsnapshot(buildvfs_kfd fil, int32_t spot, savehead_t *h)
     OSD_Printf("sv_loadsnapshot: snapshot size: %d bytes.\n", h->snapsiz);
 #endif
 
-    if (kread(fil, &i, 4) != 4)
+    if (fil.Read(&i, 4) != 4)
     {
         OSD_Printf("sv_snapshot: couldn't read 4 bytes after header.\n");
         return 7;
     }
     if (i > 0)
     {
-        if (klseek(fil, i, SEEK_SET) != i)
+        if (fil.Seek(i, FileReader::SeekSet) != i)
         {
             OSD_Printf("sv_snapshot: failed skipping over the screenshot.\n");
             return 8;
@@ -1976,11 +1969,11 @@ uint32_t sv_writediff(buildvfs_FILE fil)
     return diffsiz;
 }
 
-int32_t sv_readdiff(buildvfs_kfd fil)
+int32_t sv_readdiff(FileReader &fil)
 {
     int32_t diffsiz;
 
-    if (kread(fil, &diffsiz, sizeof(uint32_t)) != sizeof(uint32_t))
+    if (fil.Read(&diffsiz, sizeof(uint32_t)) != sizeof(uint32_t))
         return -1;
 
     if (savegame_diffcompress)
@@ -1990,7 +1983,7 @@ int32_t sv_readdiff(buildvfs_kfd fil)
     }
     else
     {
-        if (kread(fil, svdiff, diffsiz) != diffsiz)
+        if (fil.Read(svdiff, diffsiz) != diffsiz)
             return -2;
     }
 
@@ -2353,16 +2346,16 @@ void El_FreeSaveCode(void)
 }
 #endif
 
-static int32_t doloadplayer2(buildvfs_kfd fil, uint8_t **memptr)
+static int32_t doloadplayer2(FileReader &fil, uint8_t **memptr)
 {
     uint8_t *mem = memptr ? *memptr : NULL;
 #ifdef DEBUGGINGAIDS
     uint8_t *tmem=mem;
     int32_t t=timerGetTicks();
 #endif
-    if (readspecdata(svgm_udnetw, fil, &mem)) return -2;
+    if (readspecdata(svgm_udnetw, &fil, &mem)) return -2;
     PRINTSIZE("ud");
-    if (readspecdata(svgm_secwsp, fil, &mem)) return -4;
+    if (readspecdata(svgm_secwsp, &fil, &mem)) return -4;
     PRINTSIZE("sws");
 #ifdef LUNATIC
     {
@@ -2371,9 +2364,9 @@ static int32_t doloadplayer2(buildvfs_kfd fil, uint8_t **memptr)
             return ret;
     }
 #endif
-    if (readspecdata(svgm_script, fil, &mem)) return -5;
+    if (readspecdata(svgm_script, &fil, &mem)) return -5;
     PRINTSIZE("script");
-    if (readspecdata(svgm_anmisc, fil, &mem)) return -6;
+    if (readspecdata(svgm_anmisc, &fil, &mem)) return -6;
     PRINTSIZE("animisc");
 
 #if !defined LUNATIC
@@ -2407,13 +2400,13 @@ int32_t sv_updatestate(int32_t frominit)
     if (frominit)
         Bmemcpy(svsnapshot, svinitsnap, svsnapsiz);
 
-    if (readspecdata(svgm_udnetw, buildvfs_kfd_invalid, &p)) return -2;
-    if (readspecdata(svgm_secwsp, buildvfs_kfd_invalid, &p)) return -4;
-    if (readspecdata(svgm_script, buildvfs_kfd_invalid, &p)) return -5;
-    if (readspecdata(svgm_anmisc, buildvfs_kfd_invalid, &p)) return -6;
+    if (readspecdata(svgm_udnetw, nullptr, &p)) return -2;
+    if (readspecdata(svgm_secwsp, nullptr, &p)) return -4;
+    if (readspecdata(svgm_script, nullptr, &p)) return -5;
+    if (readspecdata(svgm_anmisc, nullptr, &p)) return -6;
 
 #if !defined LUNATIC
-    if (readspecdata((const dataspec_t *)svgm_vars, buildvfs_kfd_invalid, &p)) return -8;
+    if (readspecdata((const dataspec_t *)svgm_vars, nullptr, &p)) return -8;
 #endif
 
     if (p != pbeg+svsnapsiz)
diff --git a/source/duke3d/src/savegame.h b/source/duke3d/src/savegame.h
index d5cd12ab5..d9fc62985 100644
--- a/source/duke3d/src/savegame.h
+++ b/source/duke3d/src/savegame.h
@@ -120,10 +120,10 @@ extern menusave_t * g_menusaves;
 extern uint16_t g_nummenusaves;
 
 int32_t sv_updatestate(int32_t frominit);
-int32_t sv_readdiff(buildvfs_kfd fil);
+int32_t sv_readdiff(FileReader& fil);
 uint32_t sv_writediff(buildvfs_FILE fil);
-int32_t sv_loadheader(buildvfs_kfd fil, int32_t spot, savehead_t *h);
-int32_t sv_loadsnapshot(buildvfs_kfd fil, int32_t spot, savehead_t *h);
+int32_t sv_loadheader(FileReader &fil, int32_t spot, savehead_t *h);
+int32_t sv_loadsnapshot(FileReader &fil, int32_t spot, savehead_t *h);
 int32_t sv_saveandmakesnapshot(buildvfs_FILE fil, char const *name, int8_t spot, int8_t recdiffsp, int8_t diffcompress, int8_t synccompress, bool isAutoSave = false);
 void sv_freemem();
 void G_DeleteSave(savebrief_t const & sv);
diff --git a/source/rr/src/demo.cpp b/source/rr/src/demo.cpp
index 85c0718e6..0ed663a55 100644
--- a/source/rr/src/demo.cpp
+++ b/source/rr/src/demo.cpp
@@ -37,7 +37,7 @@ BEGIN_RR_NS
 char g_firstDemoFile[BMAX_PATH];
 
 buildvfs_FILE g_demo_filePtr{};  // write
-buildvfs_kfd g_demo_recFilePtr = buildvfs_kfd_invalid;  // read
+FileReader g_demo_recFilePtr;
 
 int32_t g_demo_cnt;
 int32_t g_demo_goalCnt=0;
@@ -101,8 +101,8 @@ static int32_t G_OpenDemoRead(int32_t g_whichDemo) // 0 = mine
         demofnptr = demofn;
     }
 
-    g_demo_recFilePtr = kopen4loadfrommod(demofnptr, g_loadFromGroupOnly);
-    if (g_demo_recFilePtr == buildvfs_kfd_invalid)
+    g_demo_recFilePtr = fopenFileReader(demofnptr, g_loadFromGroupOnly);
+    if (!g_demo_recFilePtr.isOpen())
         return 0;
 
     Bassert(g_whichDemo >= 1);
@@ -110,7 +110,7 @@ static int32_t G_OpenDemoRead(int32_t g_whichDemo) // 0 = mine
     if (i)
     {
         OSD_Printf(OSD_ERROR "There were errors opening demo %d (code: %d).\n", g_whichDemo, i);
-        kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+		g_demo_recFilePtr.Close();
         return 0;
     }
 
@@ -147,8 +147,7 @@ void G_OpenDemoWrite(void)
 
     if (ud.recstat == 2)
     {
-        kclose(g_demo_recFilePtr);
-        g_demo_recFilePtr = buildvfs_kfd_invalid;
+		g_demo_recFilePtr.Close();
     }
 
     if ((g_player[myconnectindex].ps->gm&MODE_GAME) && g_player[myconnectindex].ps->dead_flag)
@@ -329,13 +328,13 @@ static int32_t Demo_ReadSync(int32_t errcode)
     uint16_t si;
     int32_t i;
 
-    if (kread(g_demo_recFilePtr, &si, sizeof(uint16_t)) != sizeof(uint16_t))
+    if (g_demo_recFilePtr.Read(&si, sizeof(uint16_t)) != sizeof(uint16_t))
         return errcode;
 
     i = si;
     if (demo_hasseeds)
     {
-        if (kread(g_demo_recFilePtr, g_demo_seedbuf, i) != i)
+        if (g_demo_recFilePtr.Read(g_demo_seedbuf, i) != i)
             return errcode;
     }
 
@@ -348,7 +347,7 @@ static int32_t Demo_ReadSync(int32_t errcode)
     {
         int32_t bytes = sizeof(input_t)*i;
 
-        if (kread(g_demo_recFilePtr, recsync, bytes) != bytes)
+        if (g_demo_recFilePtr.Read(recsync, bytes) != bytes)
             return errcode+2;
     }
 
@@ -527,7 +526,7 @@ RECHECK:
         g_player[myconnectindex].ps->gm &= ~MODE_GAME;
         g_player[myconnectindex].ps->gm |= MODE_DEMO;
 
-        lastsyncofs = ktell(g_demo_recFilePtr);
+        lastsyncofs = g_demo_recFilePtr.Tell();
         initsyncofs = lastsyncofs;
         lastsynctic = g_demo_cnt;
         lastsyncclock = (int32_t) totalclock;
@@ -583,8 +582,8 @@ RECHECK:
                     if (Demo_UpdateState(0)==0)
                     {
                         g_demo_cnt = lastsynctic;
-                        klseek(g_demo_recFilePtr, lastsyncofs, SEEK_SET);
-                        ud.reccnt = 0;
+						g_demo_recFilePtr.Seek(lastsyncofs, FileReader::SeekSet);
+						ud.reccnt = 0;
 
                         totalclock = ototalclock = lockclock = lastsyncclock;
                     }
@@ -595,8 +594,8 @@ RECHECK:
                     // update to initial state
                     if (Demo_UpdateState(1) == 0)
                     {
-                        klseek(g_demo_recFilePtr, initsyncofs, SEEK_SET);
-                        g_levelTextTime = 0;
+						g_demo_recFilePtr.Seek(initsyncofs, FileReader::SeekSet);
+						g_levelTextTime = 0;
 
                         g_demo_cnt = 1;
                         ud.reccnt = 0;
@@ -634,7 +633,7 @@ RECHECK:
 
                     bigi = 0;
                     //reread:
-                    if (kread(g_demo_recFilePtr, tmpbuf, 4) != 4)
+                    if (g_demo_recFilePtr.Read(tmpbuf, 4) != 4)
                         CORRUPT(2);
 
                     if (Bmemcmp(tmpbuf, "sYnC", 4)==0)
@@ -655,11 +654,11 @@ RECHECK:
                         }
                         else
                         {
-                            lastsyncofs = ktell(g_demo_recFilePtr);
+                            lastsyncofs = g_demo_recFilePtr.Tell();
                             lastsynctic = g_demo_cnt;
                             lastsyncclock = (int32_t) totalclock;
 
-                            if (kread(g_demo_recFilePtr, tmpbuf, 4) != 4)
+                            if (g_demo_recFilePtr.Read(tmpbuf, 4) != 4)
                                 CORRUPT(7);
                             if (Bmemcmp(tmpbuf, "sYnC", 4))
                                 CORRUPT(8);
@@ -690,7 +689,7 @@ nextdemo:
 nextdemo_nomenu:
                         foundemo = 0;
                         ud.reccnt = 0;
-                        kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+						g_demo_recFilePtr.Close();
 
                         if (g_demo_goalCnt>0)
                         {
@@ -957,7 +956,7 @@ nextdemo_nomenu:
 #if KRANDDEBUG
                 krd_print("krandplay.log");
 #endif
-                kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+				g_demo_recFilePtr.Close();
             }
 
             return 0;
@@ -965,7 +964,7 @@ nextdemo_nomenu:
     }
 
     ud.multimode = numplayers;  // fixes 2 infinite loops after watching demo
-    kclose(g_demo_recFilePtr); g_demo_recFilePtr = buildvfs_kfd_invalid;
+	g_demo_recFilePtr.Close();
 
     Demo_FinishProfile();
 
diff --git a/source/rr/src/demo.h b/source/rr/src/demo.h
index b2cd42c5e..eebee5885 100644
--- a/source/rr/src/demo.h
+++ b/source/rr/src/demo.h
@@ -24,6 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #define demo_h_
 
 #include "compat.h"
+#include "files.h"
 
 BEGIN_RR_NS
 
@@ -46,7 +47,7 @@ extern int32_t demorec_synccompress_cvar;
 extern int32_t g_demo_cnt;
 extern int32_t g_demo_goalCnt;
 extern int32_t g_demo_paused;
-extern int32_t g_demo_recFilePtr;
+extern FileReader g_demo_recFilePtr;
 extern int32_t g_demo_rewind;
 extern int32_t g_demo_showStats;
 extern int32_t g_demo_totalCnt;
diff --git a/source/rr/src/savegame.cpp b/source/rr/src/savegame.cpp
index db255f458..33b43c487 100644
--- a/source/rr/src/savegame.cpp
+++ b/source/rr/src/savegame.cpp
@@ -151,8 +151,8 @@ static void ReadSaveGameHeaders_CACHE1D(CACHE1D_FIND_REC *f)
     for (; f != nullptr; f = f->next)
     {
         char const * fn = f->name;
-        int32_t fil = kopen4loadfrommod(fn, 0);
-        if (fil == -1)
+        auto fil = fopenFileReader(fn, 0);
+        if (!fil.isOpen())
             continue;
 
         menusave_t & msv = g_internalsaves[g_numinternalsaves];
@@ -178,8 +178,6 @@ static void ReadSaveGameHeaders_CACHE1D(CACHE1D_FIND_REC *f)
         }
         else
             msv.isUnreadable = 1;
-
-        kclose(fil);
     }
 }
 
@@ -282,8 +280,8 @@ void ReadSaveGameHeaders(void)
 
 int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
 {
-    int32_t fil = kopen4loadfrommod(fn, 0);
-    if (fil == -1)
+    auto fil = fopenFileReader(fn, 0);
+    if (!fil.isOpen())
         return -1;
 
     int32_t i = sv_loadheader(fil, 0, saveh);
@@ -291,7 +289,7 @@ int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
         goto corrupt;
 
     int32_t screenshotofs;
-    if (kread(fil, &screenshotofs, 4) != 4)
+    if (fil.Read(&screenshotofs, 4) != 4)
         goto corrupt;
 
 	TileFiles.tileCreate(TILE_LOADSHOT, 200, 320);
@@ -309,11 +307,9 @@ int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
     }
     tileInvalidate(TILE_LOADSHOT, 0, 255);
 
-    kclose(fil);
     return 0;
 
 corrupt:
-    kclose(fil);
     return 1;
 }
 
@@ -326,9 +322,9 @@ static int different_user_map;
 // XXX: keyboard input 'blocked' after load fail? (at least ESC?)
 int32_t G_LoadPlayer(savebrief_t & sv)
 {
-    int const fil = kopen4loadfrommod(sv.path, 0);
+    auto fil = fopenFileReader(sv.path, 0);
 
-    if (fil == -1)
+    if (!fil.isOpen())
         return -1;
 
     ready2send = 0;
@@ -343,7 +339,6 @@ int32_t G_LoadPlayer(savebrief_t & sv)
         else if (h.numplayers != ud.multimode)
             P_DoQuote(QUOTE_SAVE_BAD_PLAYERS, g_player[myconnectindex].ps);
 
-        kclose(fil);
         ototalclock = totalclock;
         ready2send = 1;
 
@@ -405,7 +400,6 @@ int32_t G_LoadPlayer(savebrief_t & sv)
     }
 
     sv_postudload();  // ud.m_XXX = ud.XXX
-    kclose(fil);
 
     return 0;
 }
@@ -437,36 +431,6 @@ static void G_RestoreTimers(void)
 
 //////////
 
-#ifdef __ANDROID__
-static void G_SavePalette(void)
-{
-    int32_t pfil;
-
-    if ((pfil = kopen4load("palette.dat", 0)) != -1)
-    {
-        int len = kfilelength(pfil);
-
-        FILE *fil = fopen("palette.dat", "rb");
-
-        if (!fil)
-        {
-            fil = fopen("palette.dat", "wb");
-
-            if (fil)
-            {
-                char *buf = (char *) Xaligned_alloc(16, len);
-
-                kread(pfil, buf, len);
-                fwrite(buf, len, 1, fil);
-                fclose(fil);
-
-                Bfree(buf);
-            }
-        }
-        else fclose(fil);
-    }
-}
-#endif
 
 void G_DeleteSave(savebrief_t const & sv)
 {
@@ -735,14 +699,14 @@ static uint8_t *writespecdata(const dataspec_t *spec, FILE *fil, uint8_t *dump)
 // (fil>=0 && havedump): first restore dump from file, then restore state from dump
 // (fil<0 && havedump): only restore state from dump
 // (fil>=0 && !havedump): only restore state from file
-static int32_t readspecdata(const dataspec_t *spec, int32_t fil, uint8_t **dumpvar)
+static int32_t readspecdata(const dataspec_t *spec, FileReader *fil, uint8_t **dumpvar)
 {
     uint8_t *  dump = dumpvar ? *dumpvar : NULL;
     auto const sptr = spec;
 
     for (; spec->flags != DS_END; spec++)
     {
-        if (fil < 0 && spec->flags & (DS_NOCHK|DS_STRING|DS_CMP))  // we're updating
+        if (fil == nullptr && spec->flags & (DS_NOCHK|DS_STRING|DS_CMP))  // we're updating
             continue;
 
         if (spec->flags & (DS_LOADFN|DS_SAVEFN))
@@ -756,7 +720,7 @@ static int32_t readspecdata(const dataspec_t *spec, int32_t fil, uint8_t **dumpv
         {
             static char cmpstrbuf[32];
             int const siz  = (spec->flags & DS_STRING) ? Bstrlen((const char *)spec->ptr) : spec->size * spec->cnt;
-            int const ksiz = kread(fil, cmpstrbuf, siz);
+            int const ksiz = fil->Read(cmpstrbuf, siz);
 
             if (ksiz != siz || Bmemcmp(spec->ptr, cmpstrbuf, siz))
             {
@@ -786,12 +750,12 @@ static int32_t readspecdata(const dataspec_t *spec, int32_t fil, uint8_t **dumpv
         if (!ptr || !cnt)
             continue;
 
-        if (fil >= 0)
+        if (fil->isOpen())
         {
             auto const mem  = (dump && (spec->flags & DS_NOCHK) == 0) ? dump : (uint8_t *)ptr;
             bool const comp = !((spec->flags & DS_CNTMASK) == 0 && spec->size * cnt <= savegame_comprthres);
             int const  siz  = comp ? cnt : cnt * spec->size;
-            int const  ksiz = comp ? kdfread_LZ4(mem, spec->size, siz, fil) : kread(fil, mem, siz);
+            int const  ksiz = comp ? kdfread_LZ4(mem, spec->size, siz, *fil) : fil->Read(mem, siz);
 
             if (ksiz != siz)
             {
@@ -1318,7 +1282,7 @@ static const dataspec_t svgm_anmisc[] =
 };
 
 static uint8_t *dosaveplayer2(FILE *fil, uint8_t *mem);
-static int32_t doloadplayer2(int32_t fil, uint8_t **memptr);
+static int32_t doloadplayer2(FileReader &fil, uint8_t **memptr);
 static void postloadplayer(int32_t savegamep);
 
 // SVGM snapshot system
@@ -1463,11 +1427,11 @@ int32_t sv_saveandmakesnapshot(FILE *fil, char const *name, int8_t spot, int8_t
 }
 
 // if file is not an EDuke32 savegame/demo, h->headerstr will be all zeros
-int32_t sv_loadheader(int32_t fil, int32_t spot, savehead_t *h)
+int32_t sv_loadheader(FileReader &fil, int32_t spot, savehead_t *h)
 {
     int32_t havedemo = (spot < 0);
 
-    if (kread(fil, h, sizeof(savehead_t)) != sizeof(savehead_t))
+    if (fil.Read(h, sizeof(savehead_t)) != sizeof(savehead_t))
     {
         OSD_Printf("%s %d header corrupt.\n", havedemo ? "Demo":"Savegame", havedemo ? -spot : spot);
         Bmemset(h->headerstr, 0, sizeof(h->headerstr));
@@ -1523,7 +1487,7 @@ int32_t sv_loadheader(int32_t fil, int32_t spot, savehead_t *h)
     return 0;
 }
 
-int32_t sv_loadsnapshot(int32_t fil, int32_t spot, savehead_t *h)
+int32_t sv_loadsnapshot(FileReader &fil, int32_t spot, savehead_t *h)
 {
     uint8_t *p;
     int32_t i;
@@ -1546,14 +1510,14 @@ int32_t sv_loadsnapshot(int32_t fil, int32_t spot, savehead_t *h)
     OSD_Printf("sv_loadsnapshot: snapshot size: %d bytes.\n", h->snapsiz);
 #endif
 
-    if (kread(fil, &i, 4) != 4)
+    if (fil.Read(&i, 4) != 4)
     {
         OSD_Printf("sv_snapshot: couldn't read 4 bytes after header.\n");
         return 7;
     }
     if (i > 0)
     {
-        if (klseek(fil, i, SEEK_SET) != i)
+        if (fil.Seek(i, FileReader::SeekSet) != i)
         {
             OSD_Printf("sv_snapshot: failed skipping over the screenshot.\n");
             return 8;
@@ -1633,11 +1597,11 @@ uint32_t sv_writediff(FILE *fil)
     return diffsiz;
 }
 
-int32_t sv_readdiff(int32_t fil)
+int32_t sv_readdiff(FileReader &fil)
 {
     int32_t diffsiz;
 
-    if (kread(fil, &diffsiz, sizeof(uint32_t)) != sizeof(uint32_t))
+    if (fil.Read(&diffsiz, sizeof(uint32_t)) != sizeof(uint32_t))
         return -1;
 
     if (savegame_diffcompress)
@@ -1647,7 +1611,7 @@ int32_t sv_readdiff(int32_t fil)
     }
     else
     {
-        if (kread(fil, svdiff, diffsiz) != diffsiz)
+        if (fil.Read(svdiff, diffsiz) != diffsiz)
             return -2;
     }
 
@@ -1828,20 +1792,20 @@ static uint8_t *dosaveplayer2(FILE *fil, uint8_t *mem)
     return mem;
 }
 
-static int32_t doloadplayer2(int32_t fil, uint8_t **memptr)
+static int32_t doloadplayer2(FileReader &fil, uint8_t **memptr)
 {
     uint8_t *mem = memptr ? *memptr : NULL;
 #ifdef DEBUGGINGAIDS
     uint8_t *tmem=mem;
     int32_t t=timerGetTicks();
 #endif
-    if (readspecdata(svgm_udnetw, fil, &mem)) return -2;
+    if (readspecdata(svgm_udnetw, &fil, &mem)) return -2;
     PRINTSIZE("ud");
-    if (readspecdata(svgm_secwsp, fil, &mem)) return -4;
+    if (readspecdata(svgm_secwsp, &fil, &mem)) return -4;
     PRINTSIZE("sws");
-    if (readspecdata(svgm_script, fil, &mem)) return -5;
+    if (readspecdata(svgm_script, &fil, &mem)) return -5;
     PRINTSIZE("script");
-    if (readspecdata(svgm_anmisc, fil, &mem)) return -6;
+    if (readspecdata(svgm_anmisc, &fil, &mem)) return -6;
     PRINTSIZE("animisc");
 
     if (memptr)
@@ -1856,10 +1820,10 @@ int32_t sv_updatestate(int32_t frominit)
     if (frominit)
         Bmemcpy(svsnapshot, svinitsnap, svsnapsiz);
 
-    if (readspecdata(svgm_udnetw, -1, &p)) return -2;
-    if (readspecdata(svgm_secwsp, -1, &p)) return -4;
-    if (readspecdata(svgm_script, -1, &p)) return -5;
-    if (readspecdata(svgm_anmisc, -1, &p)) return -6;
+    if (readspecdata(svgm_udnetw, nullptr, &p)) return -2;
+    if (readspecdata(svgm_secwsp, nullptr, &p)) return -4;
+    if (readspecdata(svgm_script, nullptr, &p)) return -5;
+    if (readspecdata(svgm_anmisc, nullptr, &p)) return -6;
 
     if (p != pbeg+svsnapsiz)
     {
diff --git a/source/rr/src/savegame.h b/source/rr/src/savegame.h
index ce54f2bbe..4aadb3fbf 100644
--- a/source/rr/src/savegame.h
+++ b/source/rr/src/savegame.h
@@ -111,10 +111,10 @@ extern menusave_t * g_menusaves;
 extern uint16_t g_nummenusaves;
 
 int32_t sv_updatestate(int32_t frominit);
-int32_t sv_readdiff(int32_t fil);
+int32_t sv_readdiff(FileReader &fil);
 uint32_t sv_writediff(FILE *fil);
-int32_t sv_loadheader(int32_t fil, int32_t spot, savehead_t *h);
-int32_t sv_loadsnapshot(int32_t fil, int32_t spot, savehead_t *h);
+int32_t sv_loadheader(FileReader& fil, int32_t spot, savehead_t* h);
+int32_t sv_loadsnapshot(FileReader& fil, int32_t spot, savehead_t* h);
 int32_t sv_saveandmakesnapshot(FILE *fil, char const *name, int8_t spot, int8_t recdiffsp, int8_t diffcompress, int8_t synccompress, bool isAutoSave = false);
 void sv_freemem();
 void G_DeleteSave(savebrief_t const & sv);
diff --git a/source/rr/src/screens.cpp b/source/rr/src/screens.cpp
index cd06dd132..958a088e4 100644
--- a/source/rr/src/screens.cpp
+++ b/source/rr/src/screens.cpp
@@ -1540,17 +1540,10 @@ void G_DisplayLogo(void)
 
         if (!I_CheckAllInput() && g_noLogoAnim == 0)
         {
-            int32_t i;
             Net_GetPackets();
 
-            i = kopen4loadfrommod("3dr.ivf", 0);
-
-            if (i == -1)
-                i = kopen4loadfrommod("3dr.anm", 0);
-
-            if (i != -1)
+			if (testkopen("3dr.ivf", 0) || testkopen("3dr.anm", 0))
             {
-                kclose(i);
                 Anim_Play("3dr.anm");
                 G_FadePalette(0, 0, 0, 252);
                 I_ClearAllInput();