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:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1456,6 +1456,7 @@ final class CoolUtil
|
||||
|
||||
return toProperty.setValue(fromProperty.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PropertyInfo {
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user