Skip to content

Commit a7b0401

Browse files
authored
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
1 parent a951e38 commit a7b0401

13 files changed

Lines changed: 258 additions & 196 deletions

File tree

source/funkin/backend/assets/AssetsLibraryList.hx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ import lime.utils.AssetLibrary;
88
import haxe.ds.Map;
99

1010
class AssetsLibraryList extends AssetLibrary {
11+
1112
public var libraries:Array<AssetLibrary> = [];
13+
public var cleanLibraries(get, never):Array<AssetLibrary>;
14+
function get_cleanLibraries():Array<AssetLibrary> {
15+
return [for (l in libraries) getCleanLibrary(l)];
16+
}
17+
18+
// is true if any library in `libraries` contains some kind of compressed library.
19+
public var hasCompressedLibrary(get, never):Bool;
20+
function get_hasCompressedLibrary():Bool {
21+
for (l in libraries) if (getCleanLibrary(l).isCompressed) return true;
22+
return false;
23+
}
1224

1325
@:allow(funkin.backend.system.Main)
1426
@:allow(funkin.backend.system.MainState)

source/funkin/backend/assets/ModsFolder.hx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ class ModsFolder {
8383
*/
8484
public static function loadModLib(path:String, force:Bool = false, ?modName:String) {
8585
#if MOD_SUPPORT
86-
if (FileSystem.exists('$path.zip'))
87-
return loadLibraryFromZip('$path'.toLowerCase(), '$path.zip', force, modName);
88-
else
89-
return loadLibraryFromFolder('$path'.toLowerCase(), '$path', force, modName);
86+
for (ext in Flags.ALLOWED_ZIP_EXTENSIONS) {
87+
if (!FileSystem.exists('$path.$ext')) continue;
88+
return loadLibraryFromZip('$path'.toLowerCase(), '$path.$ext', force, modName);
89+
}
90+
return loadLibraryFromFolder('$path'.toLowerCase(), '$path', force, modName);
9091

9192
#else
9293
return null;
@@ -96,27 +97,16 @@ class ModsFolder {
9697
public static function getModsList():Array<String> {
9798
var mods:Array<String> = [];
9899
#if MOD_SUPPORT
99-
if (!FileSystem.exists(modsPath)) {
100-
// Mods directory does not exist yet, create it
101-
FileSystem.createDirectory(modsPath);
102-
}
100+
// Mods directory does not exist yet, create it
101+
if (!FileSystem.exists(modsPath)) FileSystem.createDirectory(modsPath);
103102

104103
final modsList:Array<String> = FileSystem.readDirectory(modsPath);
105104

106-
if (modsList == null || modsList.length <= 0)
107-
return mods;
105+
if (modsList == null || modsList.length <= 0) return mods;
108106

109107
for (modFolder in modsList) {
110-
if (FileSystem.isDirectory(modsPath + modFolder)) {
111-
mods.push(modFolder);
112-
} else {
113-
var ext = Path.extension(modFolder).toLowerCase();
114-
switch(ext) {
115-
case 'zip':
116-
// is a zip mod!!
117-
mods.push(Path.withoutExtension(modFolder));
118-
}
119-
}
108+
if (FileSystem.isDirectory(modsPath + modFolder)) mods.push(modFolder);
109+
else if (Flags.ALLOWED_ZIP_EXTENSIONS.contains(Path.extension(modFolder))) mods.push(Path.withoutExtension(modFolder));
120110
}
121111
#end
122112
return mods;
@@ -128,7 +118,9 @@ class ModsFolder {
128118
#if TRANSLATIONS_SUPPORT
129119
if(skipTranslated && (l is TranslatedAssetLibrary)) continue;
130120
#end
131-
if (l is ScriptedAssetLibrary || l is IModsAssetLibrary) libs.push(cast(l, IModsAssetLibrary));
121+
// No need to check for it being a `ScriptedAssetLibrary`, if `ScriptedAssetLibrary` extends ModsFolderLibrary, which implements `IModsAssetLibrary`
122+
// If you have to revert this change then uhhhhh wasn't me, trust 🙏
123+
if (/*l is ScriptedAssetLibrary ||*/ l is IModsAssetLibrary) libs.push(cast(l, IModsAssetLibrary));
132124
}
133125
return libs;
134126
}

source/funkin/backend/assets/ZipFolderLibrary.hx

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package funkin.backend.assets;
22

3+
import funkin.backend.system.Flags;
4+
35
import haxe.io.Path;
46
import lime.graphics.Image;
57
import lime.media.AudioBuffer;
68
import lime.text.Font;
79
import lime.utils.Bytes;
810
import openfl.utils.AssetLibrary;
11+
import sys.io.File;
912

1013
#if MOD_SUPPORT
1114
import funkin.backend.utils.SysZip.SysZipEntry;
@@ -15,40 +18,76 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
1518
public var basePath:String;
1619
public var modName:String;
1720
public var libName:String;
18-
public var useImageCache:Bool = false;
1921
public var prefix = 'assets/';
20-
22+
2123
public var zip:SysZip;
2224
public var assets:Map<String, SysZipEntry> = [];
2325
public var lowerCaseAssets:Map<String, SysZipEntry> = [];
2426
public var nameMap:Map<String, String> = [];
2527

26-
public function new(basePath:String, libName:String, ?modName:String) {
28+
public var PRELOAD_VIDEOS:Bool = true;
29+
30+
public function new(basePath:String, libName:String, ?modName:String, ?preloadVideos:Bool = true) {
2731
this.libName = libName;
2832

2933
this.basePath = basePath;
3034

3135
this.modName = (modName == null) ? libName : modName;
3236

3337
zip = SysZip.openFromFile(basePath);
34-
zip.read();
3538
for(entry in zip.entries) {
36-
if (entry.fileName.length < 0 || entry.fileName.endsWith("/"))
37-
continue;
39+
if (entry.fileName.length < 0 || entry.fileName.endsWith("/")) continue;
3840

39-
lowerCaseAssets[entry.fileName.toLowerCase()] = assets[entry.fileName.toLowerCase()] = assets[entry.fileName] = entry;
40-
nameMap.set(entry.fileName.toLowerCase(), entry.fileName);
41+
var name:String = entry.fileName.toLowerCase(); // calling .toLowerCase a million times is never the solution
42+
lowerCaseAssets[name] = assets[name] = assets[entry.fileName] = entry;
43+
nameMap.set(name, entry.fileName);
4144
}
4245

4346
super();
47+
48+
isCompressed = true;
49+
50+
// don't override default value of true if the file exists.
51+
// 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.
52+
PRELOAD_VIDEOS = (!PRELOAD_VIDEOS) ? exists("assets/data/PRECACHE_VIDEOS", "TEXT") : PRELOAD_VIDEOS;
53+
54+
// if (PRELOAD_VIDEOS) precacheVideos(); // we do this in `MainState` now to handle for `Flags.VIDEO_EXT` :)
55+
}
56+
57+
public function precacheVideos() {
58+
_videoExtensions = [Flags.VIDEO_EXT];
59+
60+
videoCacheRemap = [];
61+
for (entry in zip.entries) {
62+
var name = entry.fileName.toLowerCase();
63+
if (_videoExtensions.contains(Path.extension(name))) getPath(prefix+name);
64+
}
65+
66+
var count:Int = 0;
67+
for (_ in videoCacheRemap.keys()) count++;
68+
if (count <= 0) return;
69+
trace('Precached $count video${(count == 1) ? "" : "s"}');
70+
}
71+
72+
// Now we have supports for videos in ZIP!!
73+
public var _videoExtensions:Array<String> = [Flags.VIDEO_EXT];
74+
public var videoCacheRemap:Map<String, String> = [];
75+
public function getVideoRemap(originalPath:String):String {
76+
if (!_videoExtensions.contains(Path.extension(_parsedAsset))) return originalPath;
77+
if (videoCacheRemap.exists(originalPath)) return videoCacheRemap.get(originalPath);
78+
79+
// We adding the length of the string to counteract folder in folder naming duplicates.
80+
var newPath = './.temp/${_parsedAsset.length}-zipvideo-${_parsedAsset.split("/").pop()}';
81+
File.saveBytes(newPath, unzip(assets[_parsedAsset]));
82+
videoCacheRemap.set(originalPath, newPath);
83+
return newPath;
4484
}
4585

4686
function toString():String {
47-
return '(ZipFolderLibrary: $libName/$modName)';
87+
return '(ZipFolderLibrary: $libName/$modName | ${zip.entries.length} entries | Detected Video Extensions: ${_videoExtensions.join(", ")})';
4888
}
4989

5090
public var _parsedAsset:String;
51-
5291
public override function getAudioBuffer(id:String):AudioBuffer {
5392
__parseAsset(id);
5493
return AudioBuffer.fromBytes(unzip(assets[_parsedAsset]));
@@ -71,15 +110,12 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
71110
return getAssetPath();
72111
}
73112

74-
75-
76-
public inline function unzip(f:SysZipEntry)
77-
return f == null ? null : zip.unzipEntry(f);
113+
public inline function unzip(f:SysZipEntry) return (f == null) ? null : zip.unzipEntry(f);
78114

79115
public function __parseAsset(asset:String):Bool {
80116
if (!asset.startsWith(prefix)) return false;
81117
_parsedAsset = asset.substr(prefix.length);
82-
if(ModsFolder.useLibFile) {
118+
if (ModsFolder.useLibFile) {
83119
var file = new haxe.io.Path(_parsedAsset);
84120
if(file.file.startsWith("LIB_")) {
85121
var library = file.file.substr(4);
@@ -90,8 +126,7 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
90126
}
91127

92128
_parsedAsset = _parsedAsset.toLowerCase();
93-
if(nameMap.exists(_parsedAsset))
94-
_parsedAsset = nameMap.get(_parsedAsset);
129+
if (nameMap.exists(_parsedAsset)) _parsedAsset = nameMap.get(_parsedAsset);
95130
return true;
96131
}
97132

@@ -106,9 +141,8 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
106141
return assets[_parsedAsset] != null;
107142
}
108143

109-
private function getAssetPath() {
110-
trace('[ZIP]$basePath/$_parsedAsset');
111-
return '[ZIP]$basePath/$_parsedAsset';
144+
private inline function getAssetPath() {
145+
return getVideoRemap('$basePath/$_parsedAsset');
112146
}
113147

114148
// TODO: rewrite this to 1 function, like ModsFolderLibrary
@@ -157,18 +191,6 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
157191
return content;
158192
}
159193

160-
public override function list(type:String):Array<String> {
161-
return[for(k=>e in nameMap) '$prefix$e'];
162-
}
163-
164-
// Backwards compat
165-
166-
@:noCompletion public var zipPath(get, set):String;
167-
@:noCompletion private inline function get_zipPath():String {
168-
return basePath;
169-
}
170-
@:noCompletion private inline function set_zipPath(value:String):String {
171-
return basePath = value;
172-
}
194+
public override function list(type:String):Array<String> { return [for(k=>e in nameMap) '$prefix$e']; }
173195
}
174196
#end

source/funkin/backend/scripting/GlobalScript.hx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,15 @@ class GlobalScript {
106106
public static function onModSwitch(newMod:String) {
107107
destroy();
108108
scripts = new ScriptPack("GlobalScript");
109-
for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) {
110-
var path = Paths.script('data/global/LIB_$i');
109+
for (lib in funkin.backend.assets.ModsFolder.getLoadedModsLibs()) {
110+
var modName = lib.modName;
111+
var path = Paths.script('data/global/LIB_$modName');
111112
var script = Script.create(path);
112-
if (script is DummyScript)
113-
continue;
114-
script.remappedNames.set(script.fileName, '$i:${script.fileName}');
113+
if (script is DummyScript) continue;
114+
script.remappedNames.set(script.fileName, '$modName:${script.fileName}');
115+
// so you can get the current mod's library in GloablScript :)
116+
// you should not make this a static variable then all scripts will try to reference the 1 static variable, which will be overwritten :yoikes:
117+
script.set("MOD_LIBRARY", lib);
115118
scripts.add(script);
116119
script.load();
117120
}

source/funkin/backend/system/Flags.hx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ class Flags {
1919

2020
// -- Codename's Addon Config --
2121
@:bypass public static var addonFlags:Map<String, Dynamic> = [];
22-
2322
public static var CURRENT_API_VERSION:Int = 2;
2423

24+
// -- Codename's ZipFolderLibrary Config --
25+
public static var ALLOWED_ZIP_EXTENSIONS:Array<String> = ["zip"];
26+
2527
// -- Codename's Mod Config --
2628
public static var MOD_NAME:String = "";
2729
public static var MOD_DESCRIPTION:String = "";

source/funkin/backend/system/MainState.hx

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import flixel.FlxState;
77
import funkin.backend.assets.AssetsLibraryList;
88
import funkin.backend.assets.ModsFolder;
99
import funkin.backend.assets.ModsFolderLibrary;
10+
import funkin.backend.assets.ZipFolderLibrary;
1011
import funkin.backend.chart.EventsData;
1112
import funkin.backend.system.framerate.Framerate;
1213
import funkin.editors.ModConfigWarning;
1314
import funkin.menus.TitleState;
1415
import haxe.io.Path;
1516

17+
1618
@dox(hide)
1719
typedef AddonInfo = {
1820
var name:String;
@@ -57,12 +59,32 @@ class MainState extends FlxState {
5759
var _highPriorityAddons:Array<AddonInfo> = [];
5860
var _noPriorityAddons:Array<AddonInfo> = [];
5961

62+
var quick_modsPath = ModsFolder.modsPath + ModsFolder.currentModFolder;
63+
64+
// handing if the loading mod (before it's properly loaded) is a compressed mod
65+
// we just need to use `Paths.assetsTree.hasCompressedLibrary` to complete valid checks for actual loaded compressed mods
66+
var isZipMod = false;
67+
68+
// If we know it's a compressed mod, then we can check if it's using the `cnemod` folder path.
69+
// All it is really is a folder with the mod's name, then a compressed file called "cnemod.[zip|7z|rar|etc]"
70+
var isCneMod = false;
71+
72+
// We are doing it like this because think about it: it's 1 for loop lol
73+
// 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.
74+
//
75+
for (ext in Flags.ALLOWED_ZIP_EXTENSIONS) {
76+
if (FileSystem.exists(quick_modsPath+"."+ext)) isZipMod = true;
77+
if (FileSystem.exists(quick_modsPath+"/cnemod."+ext)) isCneMod = true;
78+
if (isZipMod && isCneMod) break;
79+
}
80+
81+
// We get the addons folder from relative space (`./`) and then our mod's addons.
6082
var addonPaths = [
6183
ModsFolder.addonsPath,
62-
(
63-
ModsFolder.currentModFolder != null ?
64-
ModsFolder.modsPath + ModsFolder.currentModFolder + "/addons/" :
65-
null
84+
// So to check the mod's addons folder, we need to decompress it. Which is impossible* in this stage of the loading library process.
85+
// TODO: Write a function when the library is loaded to decompress the contents and then load the libraries :)
86+
( (ModsFolder.currentModFolder != null && !isZipMod) ?
87+
quick_modsPath + "/addons/" : null
6688
)
6789
];
6890

@@ -72,12 +94,8 @@ class MainState extends FlxState {
7294

7395
for (addon in FileSystem.readDirectory(path)) {
7496
if (!FileSystem.isDirectory(path + addon)) {
75-
switch(Path.extension(addon).toLowerCase()) {
76-
case 'zip':
77-
addon = Path.withoutExtension(addon);
78-
default:
79-
continue;
80-
}
97+
if (Flags.ALLOWED_ZIP_EXTENSIONS.contains(Path.extension(addon))) addon = Path.withoutExtension(addon);
98+
else continue;
8199
}
82100

83101
var data:AddonInfo = {
@@ -100,9 +118,14 @@ class MainState extends FlxState {
100118
#if MOD_SUPPORT
101119
for (addon in _lowPriorityAddons)
102120
loadLib(addon.path, ltrim(addon.name, "[LOW]"));
103-
104-
if (ModsFolder.currentModFolder != null)
105-
loadLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder);
121+
122+
if (ModsFolder.currentModFolder != null) {
123+
// isCneMod is a guarentee to be a zip mod because we just checked for it, so this will always load as a CompressedLibrary
124+
if (isCneMod)
125+
loadLib(quick_modsPath + "/cnemod", ModsFolder.currentModFolder);
126+
else
127+
loadLib(quick_modsPath, ModsFolder.currentModFolder);
128+
}
106129

107130
for (addon in _noPriorityAddons)
108131
loadLib(addon.path, addon.name);
@@ -134,9 +157,15 @@ class MainState extends FlxState {
134157
CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN);
135158
#end
136159

160+
for (lib in ModsFolder.getLoadedModsLibs()) {
161+
if (!(lib is ZipFolderLibrary)) continue;
162+
if (cast(lib, ZipFolderLibrary).PRELOAD_VIDEOS) cast(lib, ZipFolderLibrary).precacheVideos();
163+
}
164+
137165
var startState:Class<FlxState> = Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState;
138166

139-
if (Options.devMode && Options.allowConfigWarning) {
167+
// In this case if the mod we just loaded a compressed modpack, we can't edit or modify files without decompressing it.
168+
if (Options.devMode && Options.allowConfigWarning && !isZipMod) {
140169
var lib:ModsFolderLibrary;
141170
for (e in Paths.assetsTree.libraries) if ((lib = cast AssetsLibraryList.getCleanLibrary(e)) is ModsFolderLibrary
142171
&& lib.modName == ModsFolder.currentModFolder)

source/funkin/backend/system/macros/Macros.hx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class Macros {
6969
final fields:Array<Field> = Context.getBuildFields(), pos:Position = Context.currentPos();
7070

7171
fields.push({name: 'tag', access: [APublic], pos: pos, kind: FVar(macro :funkin.backend.assets.AssetSource)});
72+
fields.push({name: 'isCompressed', access: [APublic], pos: pos, kind: FVar(macro :Bool, macro false)});
7273

7374
return fields;
7475
}

source/funkin/backend/utils/CoolUtil.hx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,7 @@ final class CoolUtil
14561456

14571457
return toProperty.setValue(fromProperty.getValue());
14581458
}
1459+
14591460
}
14601461

14611462
class PropertyInfo {

0 commit comments

Comments
 (0)