Skip to content

Commit

Permalink
Add support for importing mod zips from clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
superpowers04 committed Aug 2, 2024
1 parent 3e7b891 commit e4fb99c
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 14 deletions.
5 changes: 4 additions & 1 deletion source/AnimationDebug.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
164 changes: 160 additions & 4 deletions source/ImportMod.hx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ImportMod extends DirectoryListing
}
}


typedef ZipEntries = haxe.ds.List<haxe.zip.Entry>;
class ImportModFromFolder extends MusicBeatState
{
var loadingText:FlxText;
Expand All @@ -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;
Expand All @@ -69,16 +70,166 @@ class ImportModFromFolder extends MusicBeatState
var songList:Array<SongInfo> = [];
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<String,String> = [];
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;
Expand Down Expand Up @@ -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){
Expand Down
37 changes: 31 additions & 6 deletions source/MainMenuState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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":
Expand Down
13 changes: 12 additions & 1 deletion source/Overlay.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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])}';
Expand Down Expand Up @@ -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(' '));
Expand Down
14 changes: 14 additions & 0 deletions source/SELoader.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down
1 change: 1 addition & 0 deletions source/se/SESave.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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(){}
}
Loading

0 comments on commit e4fb99c

Please sign in to comment.