diff --git a/building/libs.xml b/building/libs.xml
index 363fce77..0d8bc1c5 100644
--- a/building/libs.xml
+++ b/building/libs.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/source/funkin/backend/assets/AssetsLibraryList.hx b/source/funkin/backend/assets/AssetsLibraryList.hx
index e6247d49..0108a047 100644
--- a/source/funkin/backend/assets/AssetsLibraryList.hx
+++ b/source/funkin/backend/assets/AssetsLibraryList.hx
@@ -8,7 +8,19 @@ import lime.utils.AssetLibrary;
import haxe.ds.Map;
class AssetsLibraryList extends AssetLibrary {
+
public var libraries:Array = [];
+ public var cleanLibraries(get, never):Array;
+ function get_cleanLibraries():Array {
+ return [for (l in libraries) getCleanLibrary(l)];
+ }
+
+ // is true if any library in `libraries` contains some kind of compressed library.
+ public var hasCompressedLibrary(get, never):Bool;
+ function get_hasCompressedLibrary():Bool {
+ for (l in libraries) if (getCleanLibrary(l).isCompressed) return true;
+ return false;
+ }
@:allow(funkin.backend.system.Main)
@:allow(funkin.backend.system.MainState)
diff --git a/source/funkin/backend/assets/ModsFolder.hx b/source/funkin/backend/assets/ModsFolder.hx
index b59b605c..8eb9dd43 100644
--- a/source/funkin/backend/assets/ModsFolder.hx
+++ b/source/funkin/backend/assets/ModsFolder.hx
@@ -83,10 +83,11 @@ class ModsFolder {
*/
public static function loadModLib(path:String, force:Bool = false, ?modName:String) {
#if MOD_SUPPORT
- if (FileSystem.exists('$path.zip'))
- return loadLibraryFromZip('$path'.toLowerCase(), '$path.zip', force, modName);
- else
- return loadLibraryFromFolder('$path'.toLowerCase(), '$path', force, modName);
+ for (ext in Flags.ALLOWED_ZIP_EXTENSIONS) {
+ if (!FileSystem.exists('$path.$ext')) continue;
+ return loadLibraryFromZip('$path'.toLowerCase(), '$path.$ext', force, modName);
+ }
+ return loadLibraryFromFolder('$path'.toLowerCase(), '$path', force, modName);
#else
return null;
@@ -96,27 +97,16 @@ class ModsFolder {
public static function getModsList():Array {
var mods:Array = [];
#if MOD_SUPPORT
- if (!FileSystem.exists(modsPath)) {
- // Mods directory does not exist yet, create it
- FileSystem.createDirectory(modsPath);
- }
+ // Mods directory does not exist yet, create it
+ if (!FileSystem.exists(modsPath)) FileSystem.createDirectory(modsPath);
final modsList:Array = FileSystem.readDirectory(modsPath);
- if (modsList == null || modsList.length <= 0)
- return mods;
+ if (modsList == null || modsList.length <= 0) return mods;
for (modFolder in modsList) {
- if (FileSystem.isDirectory(modsPath + modFolder)) {
- mods.push(modFolder);
- } else {
- var ext = Path.extension(modFolder).toLowerCase();
- switch(ext) {
- case 'zip':
- // is a zip mod!!
- mods.push(Path.withoutExtension(modFolder));
- }
- }
+ if (FileSystem.isDirectory(modsPath + modFolder)) mods.push(modFolder);
+ else if (Flags.ALLOWED_ZIP_EXTENSIONS.contains(Path.extension(modFolder))) mods.push(Path.withoutExtension(modFolder));
}
#end
return mods;
@@ -128,7 +118,9 @@ class ModsFolder {
#if TRANSLATIONS_SUPPORT
if(skipTranslated && (l is TranslatedAssetLibrary)) continue;
#end
- if (l is ScriptedAssetLibrary || l is IModsAssetLibrary) libs.push(cast(l, IModsAssetLibrary));
+ // No need to check for it being a `ScriptedAssetLibrary`, if `ScriptedAssetLibrary` extends ModsFolderLibrary, which implements `IModsAssetLibrary`
+ // If you have to revert this change then uhhhhh wasn't me, trust 🙏
+ if (/*l is ScriptedAssetLibrary ||*/ l is IModsAssetLibrary) libs.push(cast(l, IModsAssetLibrary));
}
return libs;
}
diff --git a/source/funkin/backend/assets/ZipFolderLibrary.hx b/source/funkin/backend/assets/ZipFolderLibrary.hx
index e8c1a46e..d12afb45 100644
--- a/source/funkin/backend/assets/ZipFolderLibrary.hx
+++ b/source/funkin/backend/assets/ZipFolderLibrary.hx
@@ -1,11 +1,14 @@
package funkin.backend.assets;
+import funkin.backend.system.Flags;
+
import haxe.io.Path;
import lime.graphics.Image;
import lime.media.AudioBuffer;
import lime.text.Font;
import lime.utils.Bytes;
import openfl.utils.AssetLibrary;
+import sys.io.File;
#if MOD_SUPPORT
import funkin.backend.utils.SysZip.SysZipEntry;
@@ -15,15 +18,16 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
public var basePath:String;
public var modName:String;
public var libName:String;
- public var useImageCache:Bool = false;
public var prefix = 'assets/';
-
+
public var zip:SysZip;
public var assets:Map = [];
public var lowerCaseAssets:Map = [];
public var nameMap:Map = [];
- public function new(basePath:String, libName:String, ?modName:String) {
+ public var PRELOAD_VIDEOS:Bool = true;
+
+ public function new(basePath:String, libName:String, ?modName:String, ?preloadVideos:Bool = true) {
this.libName = libName;
this.basePath = basePath;
@@ -31,24 +35,59 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
this.modName = (modName == null) ? libName : modName;
zip = SysZip.openFromFile(basePath);
- zip.read();
for(entry in zip.entries) {
- if (entry.fileName.length < 0 || entry.fileName.endsWith("/"))
- continue;
+ if (entry.fileName.length < 0 || entry.fileName.endsWith("/")) continue;
- lowerCaseAssets[entry.fileName.toLowerCase()] = assets[entry.fileName.toLowerCase()] = assets[entry.fileName] = entry;
- nameMap.set(entry.fileName.toLowerCase(), entry.fileName);
+ var name:String = entry.fileName.toLowerCase(); // calling .toLowerCase a million times is never the solution
+ lowerCaseAssets[name] = assets[name] = assets[entry.fileName] = entry;
+ nameMap.set(name, entry.fileName);
}
super();
+
+ isCompressed = true;
+
+ // don't override default value of true if the file exists.
+ // by default `PRELOAD_VIDEOS` is true so you will never need to add this file, but in the case of it being false this is a backup method.
+ PRELOAD_VIDEOS = (!PRELOAD_VIDEOS) ? exists("assets/data/PRECACHE_VIDEOS", "TEXT") : PRELOAD_VIDEOS;
+
+ // if (PRELOAD_VIDEOS) precacheVideos(); // we do this in `MainState` now to handle for `Flags.VIDEO_EXT` :)
+ }
+
+ public function precacheVideos() {
+ _videoExtensions = [Flags.VIDEO_EXT];
+
+ videoCacheRemap = [];
+ for (entry in zip.entries) {
+ var name = entry.fileName.toLowerCase();
+ if (_videoExtensions.contains(Path.extension(name))) getPath(prefix+name);
+ }
+
+ var count:Int = 0;
+ for (_ in videoCacheRemap.keys()) count++;
+ if (count <= 0) return;
+ trace('Precached $count video${(count == 1) ? "" : "s"}');
+ }
+
+ // Now we have supports for videos in ZIP!!
+ public var _videoExtensions:Array = [Flags.VIDEO_EXT];
+ public var videoCacheRemap:Map = [];
+ public function getVideoRemap(originalPath:String):String {
+ if (!_videoExtensions.contains(Path.extension(_parsedAsset))) return originalPath;
+ if (videoCacheRemap.exists(originalPath)) return videoCacheRemap.get(originalPath);
+
+ // We adding the length of the string to counteract folder in folder naming duplicates.
+ var newPath = './.temp/${_parsedAsset.length}-zipvideo-${_parsedAsset.split("/").pop()}';
+ File.saveBytes(newPath, unzip(assets[_parsedAsset]));
+ videoCacheRemap.set(originalPath, newPath);
+ return newPath;
}
function toString():String {
- return '(ZipFolderLibrary: $libName/$modName)';
+ return '(ZipFolderLibrary: $libName/$modName | ${zip.entries.length} entries | Detected Video Extensions: ${_videoExtensions.join(", ")})';
}
public var _parsedAsset:String;
-
public override function getAudioBuffer(id:String):AudioBuffer {
__parseAsset(id);
return AudioBuffer.fromBytes(unzip(assets[_parsedAsset]));
@@ -71,15 +110,12 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
return getAssetPath();
}
-
-
- public inline function unzip(f:SysZipEntry)
- return f == null ? null : zip.unzipEntry(f);
+ public inline function unzip(f:SysZipEntry) return (f == null) ? null : zip.unzipEntry(f);
public function __parseAsset(asset:String):Bool {
if (!asset.startsWith(prefix)) return false;
_parsedAsset = asset.substr(prefix.length);
- if(ModsFolder.useLibFile) {
+ if (ModsFolder.useLibFile) {
var file = new haxe.io.Path(_parsedAsset);
if(file.file.startsWith("LIB_")) {
var library = file.file.substr(4);
@@ -90,8 +126,7 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
}
_parsedAsset = _parsedAsset.toLowerCase();
- if(nameMap.exists(_parsedAsset))
- _parsedAsset = nameMap.get(_parsedAsset);
+ if (nameMap.exists(_parsedAsset)) _parsedAsset = nameMap.get(_parsedAsset);
return true;
}
@@ -106,9 +141,8 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
return assets[_parsedAsset] != null;
}
- private function getAssetPath() {
- trace('[ZIP]$basePath/$_parsedAsset');
- return '[ZIP]$basePath/$_parsedAsset';
+ private inline function getAssetPath() {
+ return getVideoRemap('$basePath/$_parsedAsset');
}
// TODO: rewrite this to 1 function, like ModsFolderLibrary
@@ -157,18 +191,6 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
return content;
}
- public override function list(type:String):Array {
- return[for(k=>e in nameMap) '$prefix$e'];
- }
-
- // Backwards compat
-
- @:noCompletion public var zipPath(get, set):String;
- @:noCompletion private inline function get_zipPath():String {
- return basePath;
- }
- @:noCompletion private inline function set_zipPath(value:String):String {
- return basePath = value;
- }
+ public override function list(type:String):Array { return [for(k=>e in nameMap) '$prefix$e']; }
}
#end
\ No newline at end of file
diff --git a/source/funkin/backend/scripting/GlobalScript.hx b/source/funkin/backend/scripting/GlobalScript.hx
index ec381ea2..9c065619 100644
--- a/source/funkin/backend/scripting/GlobalScript.hx
+++ b/source/funkin/backend/scripting/GlobalScript.hx
@@ -106,12 +106,15 @@ class GlobalScript {
public static function onModSwitch(newMod:String) {
destroy();
scripts = new ScriptPack("GlobalScript");
- for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) {
- var path = Paths.script('data/global/LIB_$i');
+ for (lib in funkin.backend.assets.ModsFolder.getLoadedModsLibs()) {
+ var modName = lib.modName;
+ var path = Paths.script('data/global/LIB_$modName');
var script = Script.create(path);
- if (script is DummyScript)
- continue;
- script.remappedNames.set(script.fileName, '$i:${script.fileName}');
+ if (script is DummyScript) continue;
+ script.remappedNames.set(script.fileName, '$modName:${script.fileName}');
+ // so you can get the current mod's library in GloablScript :)
+ // you should not make this a static variable then all scripts will try to reference the 1 static variable, which will be overwritten :yoikes:
+ script.set("MOD_LIBRARY", lib);
scripts.add(script);
script.load();
}
diff --git a/source/funkin/backend/system/Flags.hx b/source/funkin/backend/system/Flags.hx
index c18104dd..cac69e81 100644
--- a/source/funkin/backend/system/Flags.hx
+++ b/source/funkin/backend/system/Flags.hx
@@ -19,9 +19,11 @@ class Flags {
// -- Codename's Addon Config --
@:bypass public static var addonFlags:Map = [];
-
public static var CURRENT_API_VERSION:Int = 2;
+ // -- Codename's ZipFolderLibrary Config --
+ public static var ALLOWED_ZIP_EXTENSIONS:Array = ["zip"];
+
// -- Codename's Mod Config --
public static var MOD_NAME:String = "";
public static var MOD_DESCRIPTION:String = "";
diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx
index d98caf7f..0b6da723 100644
--- a/source/funkin/backend/system/MainState.hx
+++ b/source/funkin/backend/system/MainState.hx
@@ -7,12 +7,14 @@ import flixel.FlxState;
import funkin.backend.assets.AssetsLibraryList;
import funkin.backend.assets.ModsFolder;
import funkin.backend.assets.ModsFolderLibrary;
+import funkin.backend.assets.ZipFolderLibrary;
import funkin.backend.chart.EventsData;
import funkin.backend.system.framerate.Framerate;
import funkin.editors.ModConfigWarning;
import funkin.menus.TitleState;
import haxe.io.Path;
+
@dox(hide)
typedef AddonInfo = {
var name:String;
@@ -58,12 +60,32 @@ class MainState extends FlxState {
var _highPriorityAddons:Array = [];
var _noPriorityAddons:Array = [];
+ var quick_modsPath = ModsFolder.modsPath + ModsFolder.currentModFolder;
+
+ // handing if the loading mod (before it's properly loaded) is a compressed mod
+ // we just need to use `Paths.assetsTree.hasCompressedLibrary` to complete valid checks for actual loaded compressed mods
+ var isZipMod = false;
+
+ // If we know it's a compressed mod, then we can check if it's using the `cnemod` folder path.
+ // All it is really is a folder with the mod's name, then a compressed file called "cnemod.[zip|7z|rar|etc]"
+ var isCneMod = false;
+
+ // We are doing it like this because think about it: it's 1 for loop lol
+ // We just need to know if any of these values is true, so if only one is true and we are not close to being done in the loop, that's fine.
+ //
+ for (ext in Flags.ALLOWED_ZIP_EXTENSIONS) {
+ if (FileSystem.exists(quick_modsPath+"."+ext)) isZipMod = true;
+ if (FileSystem.exists(quick_modsPath+"/cnemod."+ext)) isCneMod = true;
+ if (isZipMod && isCneMod) break;
+ }
+
+ // We get the addons folder from relative space (`./`) and then our mod's addons.
var addonPaths = [
ModsFolder.addonsPath,
- (
- ModsFolder.currentModFolder != null ?
- ModsFolder.modsPath + ModsFolder.currentModFolder + "/addons/" :
- null
+ // So to check the mod's addons folder, we need to decompress it. Which is impossible* in this stage of the loading library process.
+ // TODO: Write a function when the library is loaded to decompress the contents and then load the libraries :)
+ ( (ModsFolder.currentModFolder != null && !isZipMod) ?
+ quick_modsPath + "/addons/" : null
)
];
@@ -73,12 +95,8 @@ class MainState extends FlxState {
for (addon in FileSystem.readDirectory(path)) {
if (!FileSystem.isDirectory(path + addon)) {
- switch(Path.extension(addon).toLowerCase()) {
- case 'zip':
- addon = Path.withoutExtension(addon);
- default:
- continue;
- }
+ if (Flags.ALLOWED_ZIP_EXTENSIONS.contains(Path.extension(addon))) addon = Path.withoutExtension(addon);
+ else continue;
}
var data:AddonInfo = {
@@ -101,9 +119,14 @@ class MainState extends FlxState {
#if MOD_SUPPORT
for (addon in _lowPriorityAddons)
loadLib(addon.path, ltrim(addon.name, "[LOW]"));
-
- if (ModsFolder.currentModFolder != null)
- loadLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder);
+
+ if (ModsFolder.currentModFolder != null) {
+ // isCneMod is a guarentee to be a zip mod because we just checked for it, so this will always load as a CompressedLibrary
+ if (isCneMod)
+ loadLib(quick_modsPath + "/cnemod", ModsFolder.currentModFolder);
+ else
+ loadLib(quick_modsPath, ModsFolder.currentModFolder);
+ }
for (addon in _noPriorityAddons)
loadLib(addon.path, addon.name);
@@ -135,9 +158,15 @@ class MainState extends FlxState {
CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN);
#end
+ for (lib in ModsFolder.getLoadedModsLibs()) {
+ if (!(lib is ZipFolderLibrary)) continue;
+ if (cast(lib, ZipFolderLibrary).PRELOAD_VIDEOS) cast(lib, ZipFolderLibrary).precacheVideos();
+ }
+
var startState:Class = Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState;
- if (Options.devMode && Options.allowConfigWarning) {
+ // In this case if the mod we just loaded a compressed modpack, we can't edit or modify files without decompressing it.
+ if (Options.devMode && Options.allowConfigWarning && !isZipMod) {
var lib:ModsFolderLibrary;
for (e in Paths.assetsTree.libraries) if ((lib = cast AssetsLibraryList.getCleanLibrary(e)) is ModsFolderLibrary
&& lib.modName == ModsFolder.currentModFolder)
diff --git a/source/funkin/backend/system/macros/Macros.hx b/source/funkin/backend/system/macros/Macros.hx
index 202ceb2e..a9a388a3 100644
--- a/source/funkin/backend/system/macros/Macros.hx
+++ b/source/funkin/backend/system/macros/Macros.hx
@@ -72,6 +72,7 @@ class Macros {
final fields:Array = Context.getBuildFields(), pos:Position = Context.currentPos();
fields.push({name: 'tag', access: [APublic], pos: pos, kind: FVar(macro :funkin.backend.assets.AssetSource)});
+ fields.push({name: 'isCompressed', access: [APublic], pos: pos, kind: FVar(macro :Bool, macro false)});
return fields;
}
@@ -111,19 +112,19 @@ class Macros {
public static function buildOpenflAssets():Array {
final fields:Array = Context.getBuildFields(), pos:Position = Context.currentPos();
for (f in fields) switch (f.name) {
- case "allowCompressedTextures": fields.remove(f);
+ case "allowHardwareTextures": fields.remove(f);
default:
}
- fields.push({name: 'allowCompressedTextures', access: [APublic, AStatic], pos: pos, kind: FProp("get", "set", macro :Bool)});
- fields.push({name: '__allowCompressedTextures', access: [APrivate, AStatic], pos: pos, kind: FVar(macro :Null)});
+ fields.push({name: 'allowHardwareTextures', access: [APublic, AStatic], pos: pos, kind: FProp("get", "set", macro :Bool)});
+ fields.push({name: '__allowHardwareTextures', access: [APrivate, AStatic], pos: pos, kind: FVar(macro :Null)});
- fields.push({name: "get_allowCompressedTextures", access: [APublic, AStatic, AInline], pos: pos, kind: FFun({ret: macro :Bool, args: [], expr: macro {
- return __allowCompressedTextures != null ? __allowCompressedTextures : !funkin.backend.system.Main.forceGPUOnlyBitmapsOff && funkin.options.Options.gpuOnlyBitmaps;
+ fields.push({name: "get_allowHardwareTextures", access: [APublic, AStatic, AInline], pos: pos, kind: FFun({ret: macro :Bool, args: [], expr: macro {
+ return __allowHardwareTextures != null ? __allowHardwareTextures : !funkin.backend.system.Main.forceGPUOnlyBitmapsOff && funkin.options.Options.gpuOnlyBitmaps;
}})});
- fields.push({name: "set_allowCompressedTextures", access: [APublic, AStatic, AInline], pos: pos, kind: FFun({ret: macro :Bool, args: [{name: "value", type: macro :Bool}], expr: macro {
- __allowCompressedTextures = value;
- return get_allowCompressedTextures();
+ fields.push({name: "set_allowHardwareTextures", access: [APublic, AStatic, AInline], pos: pos, kind: FFun({ret: macro :Bool, args: [{name: "value", type: macro :Bool}], expr: macro {
+ __allowHardwareTextures = value;
+ return get_allowHardwareTextures();
}})});
return fields;
diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx
index c756dd76..098e755f 100644
--- a/source/funkin/backend/utils/CoolUtil.hx
+++ b/source/funkin/backend/utils/CoolUtil.hx
@@ -1456,6 +1456,7 @@ final class CoolUtil
return toProperty.setValue(fromProperty.getValue());
}
+
}
class PropertyInfo {
diff --git a/source/funkin/backend/utils/SysZip.hx b/source/funkin/backend/utils/SysZip.hx
index b1c013fb..126bb15d 100644
--- a/source/funkin/backend/utils/SysZip.hx
+++ b/source/funkin/backend/utils/SysZip.hx
@@ -2,157 +2,143 @@ package funkin.backend.utils;
#if sys
import haxe.io.Input;
-import haxe.zip.Entry;
-import haxe.zip.InflateImpl;
import haxe.zip.Reader;
import sys.io.File;
import sys.io.FileInput;
+import haxe.io.Bytes;
+
/**
* Class that extends Reader allowing you to load ZIP entries without blowing your RAM up!!
- * Half of the code is taken from haxe libraries btw
+ * ~~Half of the code is taken from haxe libraries btw~~ Reworked by ItsLJcool to actually work for zip files.
*/
-class SysZip extends Reader {
- var input:Input;
+class SysZip {
var fileInput:FileInput;
+ var filePath:String;
- public var entries:List;
+ public var entries:List = new List();
/**
* Opens a zip from a specified path.
- * @param path Path to the zip file.
+ * @param path Path to the zip file. (With the extension)
*/
- public static function openFromFile(path:String) {
-
- return new SysZip(File.read(path, true));
- }
+ public static function openFromFile(path:String) { return new SysZip(path); } // keeping for compatibility.
/**
- * Creates a new SysZip from a specified file input.
- * @param input File input.
+ * Creates a new SysZip from a specified path.
+ * @param path Path to the zip file. (With the extension)
*/
- public function new(input:FileInput) {
- super(input);
- fileInput = input;
- }
+ public function new(path:String) {
+ this.filePath = path;
+ fileInput = File.read(path, true);
- /**
- * Reads all the data present in a specified entry.
- * NOTE: If the entry is compressed, the data won't be decompressed. For decompression, use `unzipEntry`.
- * @param e Entry
- */
- public function readEntryData(e:SysZipEntry) {
- var bytes:haxe.io.Bytes = null;
- var buf = null;
- var tmp = null;
-
- fileInput.seek(e.seekPos, SeekBegin);
- if (e.crc32 == null) {
- if (e.compressed) {
- #if neko
- // enter progressive mode : we use a different input which has
- // a temporary buffer, this is necessary since we have to uncompress
- // progressively, and after that we might have pending read data
- // that needs to be processed
- var bufSize = 65536;
- if (buf == null) {
- buf = new haxe.io.BufferInput(i, haxe.io.Bytes.alloc(bufSize));
- tmp = haxe.io.Bytes.alloc(bufSize);
- i = buf;
- }
- var out = new haxe.io.BytesBuffer();
- var z = new neko.zip.Uncompress(-15);
- z.setFlushMode(neko.zip.Flush.SYNC);
- while (true) {
- if (buf.available == 0)
- buf.refill();
- var p = bufSize - buf.available;
- if (p != buf.pos) {
- // because of lack of "srcLen" in zip api, we need to always be stuck to the buffer end
- buf.buf.blit(p, buf.buf, buf.pos, buf.available);
- buf.pos = p;
- }
- var r = z.execute(buf.buf, buf.pos, tmp, 0);
- out.addBytes(tmp, 0, r.write);
- buf.pos += r.read;
- buf.available -= r.read;
- if (r.done)
- break;
- }
- bytes = out.getBytes();
- #else
- var bufSize = 65536;
- if (tmp == null)
- tmp = haxe.io.Bytes.alloc(bufSize);
- var out = new haxe.io.BytesBuffer();
- var z = new InflateImpl(i, false, false);
- while (true) {
- var n = z.readBytes(tmp, 0, bufSize);
- out.addBytes(tmp, 0, n);
- if (n < bufSize)
- break;
- }
- bytes = out.getBytes();
- #end
- } else
- bytes = i.read(e.dataSize);
- e.crc32 = i.readInt32();
- if (e.crc32 == 0x08074b50)
- e.crc32 = i.readInt32();
- e.dataSize = i.readInt32();
- e.fileSize = i.readInt32();
- // set data to uncompressed
- e.dataSize = e.fileSize;
- e.compressed = false;
- } else
- bytes = i.read(e.dataSize);
- return bytes;
+ updateEntries(); // automatic but if you feel like you don't want it to be automatic, you can remove this.
}
/**
* Unzips and returns all of the data present in an entry.
* @param f Entry to read from.
*/
- public function unzipEntry(f:SysZipEntry) {
- var data = readEntryData(f);
+ public function unzipEntry(f:SysZipEntry):Bytes {
+ if (f.fileSize <= 0) return Bytes.alloc(0);
+
+ fileInput.seek(f.seekPos, SeekBegin);
+ var data = fileInput.read(f.compressedSize);
+
+ if (!f.compressed) return data;
- if (!f.compressed)
- return data;
var c = new haxe.zip.Uncompress(-15);
- var s = haxe.io.Bytes.alloc(f.fileSize);
+ var s = Bytes.alloc(f.fileSize);
var r = c.execute(data, 0, s, 0);
c.close();
- if (!r.done || r.read != data.length || r.write != f.fileSize)
- throw "Invalid compressed data for " + f.fileName;
- data = s;
- return data;
+
+ if (!r.done || r.read != data.length || r.write != f.fileSize) throw 'Invalid compressed data for ${f.fileName} | ${f.compressedSize} -> ${f.fileSize}';
+ return s;
}
- public override function read():List {
- if (entries != null)
- return entries;
- entries = new List();
+ /**
+ * Updates the `entries` list with the current contents of the zip file.
+ * This is done when the zip is read from SysZip the first time, but if you REALLY need to re-update the entries, you can call this again.
+ *
+ * Note: Calling this function will hold up the game as it has to read the ENTIRE zip, so if it's large like 1GiB or more, it might take a second or more.
+ */
+ public function updateEntries() {
+ if (entries.length > 0) {
+ entries.clear();
+ entries = new List();
+ }
+
+ // --- locate End of Central Directory (EOCD) ---
+ var fileSize:Int = sys.FileSystem.stat(this.filePath).size; // probably need a better way to check the size of the file.
+ var scanSize:Int = (65535 < fileSize) ? 65535 : fileSize;
+
+ // It seems this usually ends up being 0 anyways, but for cases where it might not be?? I'd just make sure. but Someone do some digging I don't know if this required.
+ fileInput.seek(fileSize - scanSize, SeekBegin);
+
+ var buf = fileInput.read(scanSize);
+ var b = new haxe.io.BytesInput(buf);
+ // I LOVE USING MAGIC NUMBERS AND FORGETTING WHAT THEY DO 🔥🔥🔥🔥🔥🔥
+ b.position = (buf.length - 22) + 16; // offset to start of central directory
+
+ // --- read central directory ---
+ fileInput.seek(b.readInt32(), SeekBegin);
while (true) {
- var e = readEntryHeader();
- if (e == null)
- break;
+ if (fileInput.readInt32() != 0x02014b50) break; // central dir file header signature
+
+ fileInput.seek(6, SeekCur); // version/flags
+ var compression_method = fileInput.readUInt16();
+ fileInput.seek(8, SeekCur); // time/date + CRC32 (4, 4)
+ var compressed_size = fileInput.readInt32();
+ var uncompressed_size = fileInput.readInt32();
+ var nameLen = fileInput.readUInt16();
+ var extraLen = fileInput.readUInt16();
+ var commentLen = fileInput.readUInt16();
+ fileInput.seek(8, SeekCur); // skip disk number/start attrs
+ var localHeaderOffset = fileInput.readInt32();
+
+ var name = fileInput.read(nameLen).toString();
+
+ // skip central directory extra/comment
+ fileInput.seek(extraLen + commentLen, SeekCur);
+
+ // --- compute correct seekPos using local header ---
+ var curPos = fileInput.tell();
+ // I also forgor what the `+ 26` is for, so uh my b chat
+ fileInput.seek(localHeaderOffset + 26, SeekBegin);
+ var localNameLen = fileInput.readUInt16();
+ var localExtraLen = fileInput.readUInt16();
+ fileInput.seek(curPos, SeekBegin);
+
+ // I completely forgot that we don't really need to log the FOLDER of the content because we only care about where the contents are.
+ // the folders are labled as 0 bytes anyways so this will save on storing non-required data.
+ if (name.endsWith("/")) continue;
- var zipEntry:SysZipEntry = cast e;
- zipEntry.seekPos = fileInput.tell();
+ var zipEntry:SysZipEntry = {
+ fileName: name,
+ fileSize: uncompressed_size,
+ // I don't remember what the `+ 30` is for, but probably to offset something
+ seekPos: (localHeaderOffset + 30 + localNameLen + localExtraLen),
+ compressedSize: compressed_size,
+ compressed: (compression_method == 8),
+ };
entries.add(zipEntry);
- fileInput.seek(e.dataSize, SeekCur);
}
- return entries;
}
+ /**
+ * calling `dispose` doesn't actually kill the class, you can still access the entries.
+ * disposing of SysZip will free the compressed file from being used by the engine.
+ */
public function dispose() {
- if (input != null)
- input.close();
+ if (fileInput != null) fileInput.close();
}
}
typedef SysZipEntry = {
- > Entry,
+ var fileName:String;
+ var fileSize:Int;
var seekPos:Int;
+ var compressedSize:Int;
+ var compressed:Bool;
}
#end
\ No newline at end of file
diff --git a/source/funkin/editors/EditorTreeMenu.hx b/source/funkin/editors/EditorTreeMenu.hx
index 1a7a0e37..e9bb7ab3 100644
--- a/source/funkin/editors/EditorTreeMenu.hx
+++ b/source/funkin/editors/EditorTreeMenu.hx
@@ -20,6 +20,8 @@ class EditorTreeMenu extends funkin.options.TreeMenu {
bg.antialiasing = true;
setBackgroundRotation(-5);
super.createPost();
+
+ if (Paths.assetsTree.hasCompressedLibrary) warnCompressLibrary();
}
public inline function setBackgroundRotation(rotation:Float) {
@@ -58,6 +60,21 @@ class EditorTreeMenu extends funkin.options.TreeMenu {
bg.colorTransform.greenMultiplier = FlxMath.lerp(1, color.greenFloat, 0.25);
bg.colorTransform.blueMultiplier = FlxMath.lerp(1, color.blueFloat, 0.25);
}
+
+ private function warnCompressLibrary() {
+ var warningMessage = "It seems you have libraries loaded that are compressed, and can not have files written to them.\n
+ This is just a friendly reminder that if you're loading a Mod and wish to edit files, you need to uncompress it to be able to use any editors!\n\nCompressed Libraries: ";
+ var compressedList = Paths.assetsTree.libraries.filter(l -> funkin.backend.assets.AssetsLibraryList.getCleanLibrary(l).isCompressed);
+ var modNameList = [for (l in compressedList) {
+ l = funkin.backend.assets.AssetsLibraryList.getCleanLibrary(l);
+ if (l is funkin.backend.assets.IModsAssetLibrary) cast(l, funkin.backend.assets.IModsAssetLibrary).modName;
+ }];
+ warningMessage += modNameList.join(", ");
+ var zipLibraryWarning = new funkin.editors.ui.UIWarningSubstate("Compressed Library Detected!", warningMessage, [{label: "Ok", color: 0x969533, onClick: (state) -> {} }], false);
+
+ openSubState(zipLibraryWarning);
+ }
+
}
class EditorTreeMenuScreen extends funkin.options.TreeMenuScreen {
diff --git a/source/funkin/game/cutscenes/VideoCutscene.hx b/source/funkin/game/cutscenes/VideoCutscene.hx
index 12945814..0e8fbe74 100644
--- a/source/funkin/game/cutscenes/VideoCutscene.hx
+++ b/source/funkin/game/cutscenes/VideoCutscene.hx
@@ -103,14 +103,9 @@ class VideoCutscene extends Cutscene {
FlxTween.tween(loadingBackdrop, {alpha: 1}, 0.5, {ease: FlxEase.sineInOut});
Main.execAsync(function() {
- if (localPath.startsWith("[ZIP]")) {
- // ZIP PATH: EXPORT
- // TODO: this but better and more ram friendly
- localPath = './.temp/video-${curVideo++}.mp4';
- File.saveBytes(localPath, Assets.getBytes(path));
- }
-
- if (video.load(localPath)) new FlxTimer().start(0.001, function(_) { mutex.acquire(); onReady(); mutex.release(); });
+ if (video.load(localPath)) new FlxTimer().start(0.001, function(_) {
+ mutex.acquire(); onReady(); mutex.release();
+ });
else { mutex.acquire(); close(); mutex.release(); }
});
@@ -182,6 +177,7 @@ class VideoCutscene extends Cutscene {
}
public inline function onReady() {
+ trace("VideoCutscene: Ready");
FlxTween.cancelTweensOf(loadingBackdrop);
FlxTween.tween(loadingBackdrop, {alpha: 0}, 0.7, {ease: FlxEase.sineInOut, onComplete: function(_) {
loadingBackdrop.destroy();
diff --git a/source/funkin/menus/ModSwitchMenu.hx b/source/funkin/menus/ModSwitchMenu.hx
index 977f74fc..3414af7f 100644
--- a/source/funkin/menus/ModSwitchMenu.hx
+++ b/source/funkin/menus/ModSwitchMenu.hx
@@ -34,7 +34,7 @@ class ModSwitchMenu extends MusicBeatSubstate {
alphabets = new FlxTypedGroup();
for(mod in mods) {
- var a = new Alphabet(0, 0, mod == null ? TU.translate("mods.disableMods") : mod, "bold");
+ var a = new Alphabet(0, 0, mod == null ? TU.translate("mods.disableMods") : Path.withoutExtension(mod), "bold");
if(mod == ModsFolder.currentModFolder)
a.color = FlxColor.LIME;
a.isMenuItem = true;