From e4fb99cb5c894e88facd727969c1a78820147c75 Mon Sep 17 00:00:00 2001 From: Super <38338700+superpowers04@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:05:56 -0400 Subject: [PATCH] Add support for importing mod zips from clipboard --- source/AnimationDebug.hx | 5 +- source/ImportMod.hx | 164 +++++++++++++++++++++++++++++- source/MainMenuState.hx | 37 +++++-- source/Overlay.hx | 13 ++- source/SELoader.hx | 14 +++ source/se/SESave.hx | 1 + source/se/states/DownloadState.hx | 158 ++++++++++++++++++++++++++++ version.downloadMe | 5 +- 8 files changed, 383 insertions(+), 14 deletions(-) create mode 100644 source/se/states/DownloadState.hx diff --git a/source/AnimationDebug.hx b/source/AnimationDebug.hx index f77152d..a3227ae 100644 --- a/source/AnimationDebug.hx +++ b/source/AnimationDebug.hx @@ -140,7 +140,10 @@ class AnimationDebug extends MusicBeatState #else public static function fileDrop(file:String){ // Normal filesystem/file is used here because we aren't working within the game's folder. We need absolute paths - + if(file.startsWith('https://') || file.startsWith('https://')){ + FlxG.switchState(new se.states.DownloadState(file,'./requestedFile',ImportMod.ImportModFromFolder.fromZip.bind(null,_))); + return; + } #if windows file = file.replace("\\","/"); // Windows uses \ at times but we use / around here #end diff --git a/source/ImportMod.hx b/source/ImportMod.hx index f441c16..a4e0d63 100644 --- a/source/ImportMod.hx +++ b/source/ImportMod.hx @@ -48,7 +48,7 @@ class ImportMod extends DirectoryListing } } - +typedef ZipEntries = haxe.ds.List; class ImportModFromFolder extends MusicBeatState { var loadingText:FlxText; @@ -58,6 +58,7 @@ class ImportModFromFolder extends MusicBeatState var songName:EReg = ~/.+\/(.*?)\//g; var songsImported:Int = 0; var importExisting:Bool = false; + var callback:()->Void; var name:String; var folder:String; @@ -69,16 +70,166 @@ class ImportModFromFolder extends MusicBeatState var songList:Array = []; var txt:String = ''; var acceptInput = false; + public static function fromZip(path:String = 'requestedFile',?_:Dynamic){ + var input = SELoader.read(path); + try{ + + var entries:ZipEntries = null; + try{ + entries = haxe.zip.Reader.readZip(input); + if(entries == null) throw('Zip entries are null. Invalid zip provided?'); + }catch(e){ + throw('Unable to read zip: $e'); + } + var metadata:haxe.zip.Entry = null; + var subfolder:String = ""; + var canBreakMeta=false; + var canBreakSubfolder=false; + var assumedType:String = ""; + for(entry in entries){ + if(entry.fileName.substring(entry.fileName.length-1)=='/'){ + if(canBreakMeta || assumedType != "") continue; + var name = entry.fileName; + if(name.contains('/manifest') || name.contains('/mods')){ + assumedType="exec"; + }else if(!name.contains('assets/') && (name.contains('characters/') || name.contains('charts/') || name.contains('scripts/'))){ + assumedType="pack"; + } + + continue; + } + if(!canBreakMeta && assumedType == ""){ + if(entry.fileName.endsWith('character.png')){ + assumedType="char"; + } + if(entry.fileName.endsWith('Inst.ogg')){ + assumedType="chart"; + } + } + if(entry.fileName.toLowerCase().endsWith("semetadata.txt")){ + metadata = entry; + if(canBreakSubfolder) break; + canBreakMeta=true; + assumedType=""; + continue; + } + if(!canBreakSubfolder && entry.fileName.contains('/')){ + var sub = entry.fileName.substring(0,entry.fileName.indexOf('/')); + if(subfolder == ""){ + subfolder = sub; + }else if(subfolder != sub){ + canBreakSubfolder = true; + subfolder = ""; + + } + }else{ + if(canBreakMeta) break; + canBreakSubfolder = true; + } + + } + + var metaContent:Map = []; + if(metadata != null){ + + entries.remove(metadata); + var meta = haxe.zip.Reader.unzip(metadata).toString(); + if(meta.contains('=')){ + var metaList = meta.split('\n'); + for(meta in metaList){ + var index = meta.indexOf('='); + if(index > -1){ + metaContent[meta.substring(0,index).toLowerCase()] = meta.substring(index+1); + } + } + }else{ + metaContent['type'] = meta; + } + }else if(assumedType == ""){ + throw('Unable to auto-detect zip type'); + }else{ + metaContent['type'] = assumedType; + } + switch(metaContent['type']){ + case "character" | "char":{ + var char = metaContent['charname'] ?? metaContent['name'] ?? subfolder ?? 'unlabelled-${Date.now().getTime()}'; + + var charFolder = metaContent['pack'] != null ? './mods/packs/${metaContent["pack"]}/characters/$char' : './mods/characters/$char'; + extractContent(input,entries,charFolder,subfolder); + } + case "chart" | "song":{ + var char = metaContent['songname'] ?? metaContent['name'] ?? subfolder ?? 'unlabelled-${Date.now().getTime()}'; + + var charFolder = metaContent['pack'] != null ? './mods/packs/${metaContent["pack"]}/charts/$char' : './mods/charts/$char'; + extractContent(input,entries,charFolder,subfolder); + } + case "pack":{ + var char = metaContent['name'] ?? subfolder ?? 'unlabelled-${Date.now().getTime()}'; + + var charFolder = './mods/packs/$char/'; + extractContent(input,entries,charFolder,subfolder); + } + case "exec" | "psych":{ + var char = metaContent['name'] ?? subfolder ?? 'unlabelled-${Date.now().getTime()}'; + + var charFolder = './mods/packs/$char/'; + var newEntries:ZipEntries = new ZipEntries(); + for(entry in entries){ + if(!entry.fileName.contains('assets/') && !entry.fileName.contains('mods/')) continue; + newEntries.push(entry); + } + extractContent(input,newEntries,charFolder,subfolder); + } + default: + throw('Unrecognised mod type ${metaContent['type']}'); + } + Options.ReloadCharlist.RELOAD(); + }catch(e){ + input.close(); + trace('${e}\n${e.stack}'); + if(path != "" && SELoader.exists(path) && !SELoader.isDirectory(path)){ + try{ + // sys.io.FileSystem.deleteFile(path); + + }catch(e){ + trace('Unable to delete file $path;$e'); + } + } + throw(e); + } + } + public static function extractContent(input:sys.io.FileInput,entries:ZipEntries,startPath:String,subFolder:String){ + startPath = SELoader.cleanPath(SELoader.getPath(startPath)); + if(SELoader.exists(startPath) || SELoader.isDirectory(startPath)){ + startPath+=Date.now().getTime(); + } + if(SELoader.exists(startPath) || SELoader.isDirectory(startPath)){ + throw('$startPath already exists, unable to extract!'); + } + for(entry in entries){ + var name = entry.fileName; + if(name.substring(name.length-1)=='/') continue; + if(subFolder!="") name=name.substring(subFolder.length+1); + name = startPath+'/'+name; + SELoader.createDirectory(name.substring(0,name.lastIndexOf('/'))); + var output = SELoader.write(name); + var bytes = haxe.zip.Reader.unzip(entry); + output.writeBytes(bytes,0,bytes.length); + output.close(); + } + + } + function changeText(str){ txt = str; // loadingText.screenCenter(X); return; } - public function new (folder:String,name:String,?importExisting:Bool = false) + public function new (folder:String,name:String,?importExisting:Bool = false,?callback:()->Void) { super(); - + this.callback = callback; this.name = name; this.folder = folder; this.importExisting = importExisting; @@ -169,7 +320,12 @@ class ImportModFromFolder extends MusicBeatState if(acceptInput) { if ((done && FlxG.keys.justPressed.ANY) || FlxG.keys.justPressed.ESCAPE) { - FlxG.switchState(new MainMenuState()); + if(callback != null){ + callback(); + }else{ + FlxG.switchState(new MainMenuState()); + } + } if(!selectedLength){ if(FlxG.keys.justPressed.ENTER){ diff --git a/source/MainMenuState.hx b/source/MainMenuState.hx index 8b2b3cb..b86518f 100644 --- a/source/MainMenuState.hx +++ b/source/MainMenuState.hx @@ -263,24 +263,47 @@ class MainMenuState extends SickMenuState { // super.beatHit(); // // if(char != null && char.animation.curAnim.finished) char.dance(true); // } + var showedImportClipboard:Bool = false; + override function onFocus(){ + + var clip = lime.system.Clipboard.text; + if(clip.startsWith('https://') && clip.contains('.zip')){ + if(showedImportClipboard) return; + showedImportClipboard = true; + if(!otherMenu){ + otherSwitch(); + } + descriptions[0]= "Detected a link to a zip file in your clipboard. Press this to import it"; + + changeSelection(); + }else if(showedImportClipboard){ + showedImportClipboard=false; + if(otherMenu){ + options[0]= "import clipboard"; + descriptions[0]= 'Treats the contents of your clipboard like you\'ve dragged and dropped it onto the game'; + if(curSelected == 0) changeSelection(); + } + } + } override function changeSelection(change:Int = 0){ // if(char != null && change != 0) char.playAnim(Note.noteAnims[if(change > 0)1 else 2],true); MainMenuState.errorMessage = ""; super.changeSelection(change); } - #if(android) inline static #end var otherMenu:Bool = false; - #if !mobile + #if(mobile) + inline static var otherMenu:Bool = false; + function otherSwitch(){} + #else + public var otherMenu:Bool = false; function otherSwitch(){ - options = ["deprecated freeplay","download charts","download characters","import charts from mods","changelog", 'credits']; - descriptions = ['Play any song from the main game or your assets folder',"Download charts made for or ported to Super Engine","Download characters made for or ported to Super Engine",'Convert charts from other mods to work here. Will put them in Modded Songs',"Read the latest changes for the engine","Check out the awesome people who helped with this engine in some way"]; + options = ["import clipboard","deprecated freeplay","download charts","download characters","import charts from mods","changelog", 'credits']; + descriptions = ['Treats the contents of your clipboard like you\'ve dragged and dropped it onto the game','Play any song from the main game or your assets folder',"Download charts made for or ported to Super Engine","Download characters made for or ported to Super Engine",'Convert charts from other mods to work here. Will put them in Modded Songs',"Read the latest changes for the engine","Check out the awesome people who helped with this engine in some way"]; // if (TitleState.osuBeatmapLoc != '') {options.push("osu beatmaps"); descriptions.push("Play osu beatmaps converted over to FNF");} options.push("back"); descriptions.push("Go back to the main menu"); curSelected = 0; - #if(!mobile) otherMenu = true; - #end selected = false; callInterp('otherSwitch',[]); if(cancelCurrentFunction) return; @@ -381,6 +404,8 @@ class MainMenuState extends SickMenuState { loading = true; MainMenuState.handleError('Offline songs have been moved to the modded songs list!'); // FlxG.switchState(new onlinemod.OfflineMenuState()); + case "import clipboard": + AnimationDebug.fileDrop(lime.system.Clipboard.text); case 'changelog' | 'update': FlxG.switchState(new OutdatedSubState()); // case "Setup characters": diff --git a/source/Overlay.hx b/source/Overlay.hx index 61a428f..82292bd 100644 --- a/source/Overlay.hx +++ b/source/Overlay.hx @@ -548,6 +548,7 @@ class ConsoleInput extends TextField{ ['-- Utilities --'], ["mainmenu",'Return to the main menu'], + ["importurl",'Attempts to download and import a zip from a url'], ["reload",'Reloads current state'], ["switchstate",'Switch to a state, Case/path sensitive!'], ["defines",'Prints a bunch of defines, used for debugging purposes'], @@ -608,8 +609,8 @@ class ConsoleInput extends TextField{ Console.showConsole = false; FlxG.resetState(); case 'mainmenu': - Console.showConsole = false; MainMenuState.handleError(""); + Console.showConsole = false; case 'getvalue': try{ var ret = '${ConsoleUtils.getValueFromPath(null,args[1])}'; @@ -648,6 +649,16 @@ class ConsoleInput extends TextField{ // Console.print('PlayState:${cpp.Stdlib.sizeof(PlayState)}'); // } + case 'importurl' : + // Console.print(haxe.CallStack.exceptionStack(true)); + Console.showConsole = false; + var url:String = args[1]; + if(url == null || !url.startsWith('http://') && !url.startsWith('https://')) { + Console.print('Invalid URL'); + return 'Invalid URL'; + } + FlxG.switchState(new se.states.DownloadState(url,'./requestedFile',ImportMod.ImportModFromFolder.fromZip.bind(null,_))); + return null; case 'trace' : // Console.print(haxe.CallStack.exceptionStack(true)); text = text.substring(text.indexOf(' ')); diff --git a/source/SELoader.hx b/source/SELoader.hx index a947dc8..b0875a2 100644 --- a/source/SELoader.hx +++ b/source/SELoader.hx @@ -5,6 +5,8 @@ package; import sys.io.File; +import sys.io.FileOutput; +import sys.io.FileInput; import sys.FileSystem; import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.FlxGraphic; @@ -548,6 +550,18 @@ class SELoader { public static function isDirectory(path:String):Bool{ return FileSystem.isDirectory(getPath(path)); } + public static function write(path:String,binary:Bool=true):FileOutput{ + return File.write(getPath(path)); + } + public static function read(path:String,binary:Bool=true):FileInput{ + return File.read(getPath(path)); + } + public static function cleanPath(path:String):String{ + return path.replace('..\\','').replace('../',''); + } + public static function append(path:String,binary:Bool=true):FileOutput{ + return File.append(getPath(path)); + } public static function createDirectory(path:String){ return FileSystem.createDirectory(getPath(path)); } diff --git a/source/se/SESave.hx b/source/se/SESave.hx index 47046cb..a35629f 100644 --- a/source/se/SESave.hx +++ b/source/se/SESave.hx @@ -171,6 +171,7 @@ typedef YourMother = Dynamic; var animDebugMusic:Bool=true; var optionsMusic:Bool=true; var ignoreErrors:Bool=false; + var repoURL:String=""; public function new(){} } \ No newline at end of file diff --git a/source/se/states/DownloadState.hx b/source/se/states/DownloadState.hx new file mode 100644 index 00000000..86d3184 --- /dev/null +++ b/source/se/states/DownloadState.hx @@ -0,0 +1,158 @@ +package se.states; + +import flixel.FlxG; +import flixel.FlxSprite; +import flixel.sound.FlxSound; +import openfl.media.Sound; +import lime.media.AudioBuffer; +import flixel.ui.FlxBar; +import flixel.text.FlxText; +import flixel.util.FlxColor; +import flixel.util.FlxAxes; +import flixel.util.FlxTimer; + +import haxe.io.Bytes; +import openfl.utils.ByteArray; +import openfl.net.Socket; +import sys.io.File; +import sys.io.FileOutput; +import sys.FileSystem; +// import openfl.filesystem.File; +// import openfl.filesystem.FileStream; +import lime.net.HTTPRequest; +import openfl.events.Event; +import openfl.events.ProgressEvent; +import openfl.events.IOErrorEvent; + +class DownloadState extends MusicBeatState +{ + public var loadingText:FlxText; + public var fileSizeText:FlxText; + public var loadingBar:FlxBar; + public var progress:Float = 0; + public var url:String= ""; + public var path:String = ""; + public var callback:Dynamic->Void; + public var socket:HTTPRequest; + public var file:File; + public var output:FileOutput; + public var canClose:Bool = true; + + public function new(url:String, path:String, callback:Dynamic->Void) + { + super(); + + this.url = url; + + this.callback = callback; + this.path = path; + trace(url); + } + + override function create() + { + var bg:FlxSprite = SELoader.loadFlxSprite('assets/images/onlinemod/online_bg2.png',true); + add(bg); + + + loadingText = new FlxText(FlxG.width/4, FlxG.height/2 - 36, FlxG.width, "Waiting..."); + loadingText.setFormat(CoolUtil.font, 28, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + add(loadingText); + + + fileSizeText = new FlxText(FlxG.width/4, FlxG.height/2 - 32, FlxG.width/2, "?/?"); + fileSizeText.setFormat(CoolUtil.font, 24, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + add(fileSizeText); + + + loadingBar = new FlxBar(0, 0, LEFT_TO_RIGHT, 640, 10, this, 'progress', 0, 1); + loadingBar.createFilledBar(FlxColor.RED, FlxColor.LIME, true, FlxColor.BLACK); + loadingBar.screenCenter(FlxAxes.XY); + add(loadingBar); + + + super.create(); + canClose = false; + output = SELoader.write(path); + socket = new HTTPRequest(url); + socket.load(url).onComplete(onData).onProgress(updateProgress).onError(onError); + // socket = new Socket(); + // socket.addEventListener(ProgressEvent.SOCKET_DATA,onData); + // socket.addEventListener(IOErrorEvent.IO_ERROR,onIOError); + // socket.addEventListener(Event.CLOSE,onClose); + // socket.connect(url); + + } + // public dynamic function onClose(e:Event){ + // if(canClose) return; + // try{ + + // output.close(); + // }catch(e){} + // callback(e); + // try{ + // socket.close(); + // }catch(e){} + // } + public dynamic function onError(e:Dynamic){ + try{ + output.close(); + }catch(e){} + fileSizeText.text = ""; + loadingText.text = 'Error! Press any key to exit\n\n${e}'; + trace('Error! ${e}'); + canClose = true; + } + // public dynamic function onIOError(e:IOErrorEvent){ + // try{ + // output.close(); + // socket.close(); + // }catch(e){} + // fileSizeText.text = 'Error! ${e.text}\n\n Press any key to exit'; + // trace('Error! ${e.text}'); + // canClose = true; + // } + public dynamic function onData(bytes:Bytes){ + + fileSizeText.text = "Writing to file"; + output.writeBytes(bytes,0,bytes.length); + output.close(); + fileSizeText.text = "Finished."; + try{ + loadingText.text = 'Finished, Press any key to exit..'; + canClose = true; + callback(null); + }catch(e){ + fileSizeText.text = ""; + loadingText.text = 'Error! Press any key to exit\n\n${e.details()}\n${e.stack}'; + trace('Error! ${e.details()}\n ${e.stack}'); + } + + } + // public dynamic function onData(e:ProgressEvent){ + // var data:ByteArray = new ByteArray(); + // socket.readBytes(data); + + // output.writeBytes(data,output.position,data.bytesAvailable); + // updateProgress(e.bytesLoaded,e.bytesTotal); + // } + function updateProgress(bytesReceived:Float,fileSize:Float){ + progress = Math.min(1, bytesReceived / fileSize); + + if (fileSize > 1000000) { //MB + fileSizeText.text = Std.int(bytesReceived/10000)/100 + "/" + Std.int(fileSize/10000)/100 + "MB"; + } else {//KB + fileSizeText.text = Std.int(bytesReceived/10)/100 + "/" + Std.int(fileSize/10)/100 + "KB"; + } + } + override function update(elapsed:Float) { + if(canClose){ + if(FlxG.keys.justPressed.ANY){ + FlxG.switchState(new MainMenuState()); + } + } + + super.update(elapsed); + } + +} diff --git a/version.downloadMe b/version.downloadMe index 272596f..657132b 100644 --- a/version.downloadMe +++ b/version.downloadMe @@ -1,4 +1,4 @@ -24.07.31.2037; +24.08.01.2221; * Better Psych character support(Still kinda experimental but should be more stable) * Fix some small errors * Fix characters not loading due to using ID instead of folderName @@ -7,4 +7,5 @@ * Fix stages not loading * Options/Animation debug music * Remove FlxKey from being used in input -* SaveIcon when game is saving \ No newline at end of file +* SaveIcon when game is saving +* Add support for importing mod zips from clipboard \ No newline at end of file