Fixed / Refactored SysZip to actually work, and store compressed entries without uncompressing an entire zip (#761)

* well boys I fixed it. you can now have Zipped Mods!!!!!

* comment update

* quick fixes + optimizations

* fixed stream audio bug????

* found the issue ffs

* removing traces oops

* fixed videos from not being able to play. Inspired from VideoCutscene's fix for ZIP videos, and also removed that fix in favor for just fixing the issue entirely

* oop just in case no special characters that kill themself on other shit

* Implemented preloading every video (caching) when loading a ZipFolderLibrary.

Since videos have to be decompressed and be saved as a file anyways this reduces the time to do that, and what not.

* [UNTESTED] Allowed Zip Extensions for allowing any extension in the list to be passed as a `.zip`

* After some more testing loading Zip Addons works as well.

* Added the long awaited .cnemod file type!!!!! (it's just a folder with .cnemod with a .zip inside LMAO)

* Chalking up the Streamed Audio Bug to a mod issue, and also added capabilites to detect if a library is considered compressed

* Added warning to those who try to use the editor with compressed libraries

* ok instead of making it a .cnemod, the zip name will be `cnemod.zip` or what not.

* removing unnessesary traces oops

* clean Library accessor ig then

* fixed small issues and removed an idiot idea

* writing todos and fixing some context that left the room in comments

* Fixed an issue where if you had a file of 0 bytes (nothing) in a zipped library it would FUCKING CRASH the game

* Actually fixed the issue of decompressing 0 byte files

Also stopped saving the data for Folders in Zipped files, as they are 0 bytes anyways.

* Added the features that needed to exist, and removed a stupid line I saw in `ModsFolder` lol

For some reason animation offsets for characters are breaking and I can't tell if its my drive killing itself or cne so uh

* fix an issue that will force the game to crash if first ever mod being loaded is a ZipModLibrary
This commit is contained in:
ItsLJcool
2026-03-15 14:24:47 +07:00
committed by GitHub
parent a951e38dbb
commit a7b040152d
13 changed files with 258 additions and 196 deletions
@@ -8,7 +8,19 @@ import lime.utils.AssetLibrary;
import haxe.ds.Map;
class AssetsLibraryList extends AssetLibrary {
public var libraries:Array<AssetLibrary> = [];
public var cleanLibraries(get, never):Array<AssetLibrary>;
function get_cleanLibraries():Array<AssetLibrary> {
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)
+13 -21
View File
@@ -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<String> {
var mods:Array<String> = [];
#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<String> = 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;
}
@@ -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<String, SysZipEntry> = [];
public var lowerCaseAssets:Map<String, SysZipEntry> = [];
public var nameMap:Map<String, String> = [];
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<String> = [Flags.VIDEO_EXT];
public var videoCacheRemap:Map<String, String> = [];
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<String> {
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<String> { return [for(k=>e in nameMap) '$prefix$e']; }
}
#end
@@ -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();
}
+3 -1
View File
@@ -19,9 +19,11 @@ class Flags {
// -- Codename's Addon Config --
@:bypass public static var addonFlags:Map<String, Dynamic> = [];
public static var CURRENT_API_VERSION:Int = 2;
// -- Codename's ZipFolderLibrary Config --
public static var ALLOWED_ZIP_EXTENSIONS:Array<String> = ["zip"];
// -- Codename's Mod Config --
public static var MOD_NAME:String = "";
public static var MOD_DESCRIPTION:String = "";
+43 -14
View File
@@ -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;
@@ -57,12 +59,32 @@ class MainState extends FlxState {
var _highPriorityAddons:Array<AddonInfo> = [];
var _noPriorityAddons:Array<AddonInfo> = [];
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
)
];
@@ -72,12 +94,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 = {
@@ -100,9 +118,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);
@@ -134,9 +157,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<FlxState> = 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)
@@ -69,6 +69,7 @@ class Macros {
final fields:Array<Field> = 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;
}
+1
View File
@@ -1456,6 +1456,7 @@ final class CoolUtil
return toProperty.setValue(fromProperty.getValue());
}
}
class PropertyInfo {
+97 -111
View File
@@ -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<SysZipEntry>;
public var entries:List<SysZipEntry> = 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<Entry> {
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
+17
View File
@@ -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 {
@@ -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();
+1 -1
View File
@@ -34,7 +34,7 @@ class ModSwitchMenu extends MusicBeatSubstate {
alphabets = new FlxTypedGroup<Alphabet>();
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;
+3 -2
View File
@@ -249,9 +249,10 @@ class Assets
}
#if (lime_vorbis && lime > "7.9.0" && !macro)
if (Options.streamedMusic) {
var path = getPath(id);
var bytes = getBytes(id);
if (bytes == null) return null;
// TODO: What if it is a WAV or non-Vorbis file?
var vorbisFile = VorbisFile.fromFile(path);
var vorbisFile = VorbisFile.fromBytes(bytes);
if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile));
}
#end