From d67f537cdfb0384ab025fe356e042209664950c0 Mon Sep 17 00:00:00 2001 From: Furo Date: Sat, 29 Mar 2025 16:43:22 +0100 Subject: [PATCH] added vslice fnfc format (psych import soon) --- source/funkin/backend/chart/BaseGameParser.hx | 144 ++++++++++- source/funkin/backend/chart/Chart.hx | 2 +- .../editors/charter/CharterSelection.hx | 8 +- .../editors/charter/SongCreationScreen.hx | 241 +++++++++++++++--- source/funkin/editors/ui/UIFileExplorer.hx | 7 +- 5 files changed, 362 insertions(+), 40 deletions(-) diff --git a/source/funkin/backend/chart/BaseGameParser.hx b/source/funkin/backend/chart/BaseGameParser.hx index 02c89c81..b05e7ca8 100644 --- a/source/funkin/backend/chart/BaseGameParser.hx +++ b/source/funkin/backend/chart/BaseGameParser.hx @@ -4,8 +4,142 @@ import funkin.backend.chart.ChartData.ChartMetaData; // These new structures are kinda a mess to port, i love and hate them at the same time; why the hell are every difficulty in the same file??? - Nex class BaseGameParser { - public static function parseChart(data:Dynamic, metaData:Dynamic, result:ChartData) { - // TO DO + public static function parseChart(data:Dynamic, metaData:Dynamic, events:Dynamic, result:ChartData) { + // base fnf chart parsing + var data:Array = data; + var metadata:SwagMetadata = metaData; + var events:Array = events; + + result.stage = metadata.playData.stage; + + var p2isGF:Bool = false; + result.strumLines.push({ + characters: [metadata.playData.characters.player], + type: 1, + position: "boyfriend", + notes: [], + vocalsSuffix: "-Player" + }); + result.strumLines.push({ + characters: [metadata.playData.characters.opponent], + type: 0, + position: (p2isGF = metadata.playData.characters.opponent.startsWith("gf")) ? "girlfriend" : "dad", + notes: [], + vocalsSuffix: "-Opponent" + }); + var gfName = metadata.playData.characters.girlfriend; + if (!p2isGF && gfName != "none") { + result.strumLines.push({ + characters: [gfName], + type: 2, + position: "girlfriend", + notes: [], + visible: false, + }); + } + + var firstTimeChange = metadata.timeChanges[0]; + result.meta.bpm = firstTimeChange.bpm; + result.meta.needsVoices = true; + + for (note in data) + { + var daNoteType:Null = null; + if (note.k != null) + daNoteType = Chart.addNoteType(result, note.k); + + var daNoteData:Int = Std.int(note.d % 8); + var isMustHit:Bool = Math.floor(daNoteData / 4) == 0; + + result.strumLines[isMustHit ? 0 : 1].notes.push({ + time: note.t, + id: daNoteData % 4, + type: daNoteType, + sLen: note.l + }); + } + + for (event in events) + { + switch (event.e) + { + case "FocusCamera": + result.events.push({ + time: event.t, + name: "Camera Movement", + params: [event.v.char] + }); + } + } + + /*var camFocusedBF:Bool = false; + var altAnims:Bool = false; + var beatsPerMeasure:Float = data.beatsPerMeasure.getDefault(Flags.DEFAULT_BEATS_PER_MEASURE); + var curBPM:Float = firstTimeChange.bpm; + var curTime:Float = 0; + var curCrochet:Float = ((60 / curBPM) * 1000); + + if (data.notes != null) for(section in data.notes) { + if (section == null) { + curTime += curCrochet * beatsPerMeasure; + continue; // Yoshi Engine charts crash fix + } + + if (camFocusedBF != (camFocusedBF = section.mustHitSection)) { + result.events.push({ + time: curTime, + name: "Camera Movement", + params: [camFocusedBF ? 1 : 0] + }); + } + + if (section.altAnim == null) section.altAnim = false; + if (altAnims != (altAnims = section.altAnim)) { + result.events.push({ + time: curTime, + name: "Alt Animation Toggle", + params: [altAnims, false, 0] + }); + } + + if (section.sectionNotes != null) for(note in section.sectionNotes) { + if (note[1] < 0) continue; + + var daStrumTime:Float = note[0]; + var daNoteData:Int = Std.int(note[1] % 8); + var daNoteType:Int = Std.int(note[1] / 8); + var gottaHitNote:Bool = daNoteData >= 4 ? !section.mustHitSection : section.mustHitSection; + + if (note.length > 2) { + if (note[3] is Int && data.noteTypes != null) + daNoteType = Chart.addNoteType(result, data.noteTypes[Std.int(note[3])-1]); + else if (note[3] is String) + daNoteType = Chart.addNoteType(result, note[3]); + } else { + if(data.noteTypes != null) + daNoteType = Chart.addNoteType(result, data.noteTypes[daNoteType-1]); + } + + result.strumLines[gottaHitNote ? 1 : 0].notes.push({ + time: daStrumTime, + id: daNoteData % 4, + type: daNoteType, + sLen: note[2] + }); + } + + if (section.changeBPM && section.bpm != curBPM) { + curCrochet = ((60 / (curBPM = section.bpm)) * 1000); + + result.events.push({ + time: curTime, + name: "BPM Change", + params: [section.bpm] + }); + } + + curTime += curCrochet * beatsPerMeasure; + }*/ } public static function parseMeta(data:Dynamic, result:ChartMetaData) { @@ -29,7 +163,7 @@ class BaseGameParser { public static function encodeMeta(meta:ChartMetaData, ?chart:ChartData):SwagMetadata { var addVars:Dynamic = meta.customValues; var defStage:String = addVars.stage != null ? addVars.stage : Flags.DEFAULT_STAGE; - var defChars:SwagCharactersList = addVars.characters != null ? addVars.characters : {player: Flags.DEFAULT_CHARACTER, girlfriend: Flags.DEFAULT_GIRLFRIEND, opponent: Flags.DEFAULT_OPPONENT}; + var defChars:SwagCharactersList = addVars.characters != null ? addVars.characters : {player: Flags.DEFAULT_CHARACTER, girlfriend: Flags.DEFAULT_GIRLFRIEND, opponent: Flags.DEFAULT_OPPONENT, playerVocals: [], opponentVocals: [], instrumental: '', altInstrumentals: []}; var defTimeCh:Array; if (addVars.timeChanges != null && addVars.timeChanges.length > 0) { @@ -141,6 +275,10 @@ typedef SwagCharactersList = var player:String; var girlfriend:String; var opponent:String; + var instrumental:String; + var altInstrumentals:Array; + var opponentVocals:Array; + var playerVocals:Array; } typedef SwagSongOffsets = diff --git a/source/funkin/backend/chart/Chart.hx b/source/funkin/backend/chart/Chart.hx index 07b2a2ae..79901b41 100644 --- a/source/funkin/backend/chart/Chart.hx +++ b/source/funkin/backend/chart/Chart.hx @@ -177,7 +177,7 @@ class Chart { #if REGION if (data != null) switch (detectChartFormat(data)) { case CODENAME: - // backward compat on events since its caused problems + // backward compat on events since it caused problems var eventTypesToString:Map = [ -1 => "HScript Call", 0 => "Unknown", diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index 3384967c..41e719fd 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -93,7 +93,7 @@ class CharterSelection extends EditorTreeMenu { } #if sys - public function saveSong(creation:SongCreationData) { + public function saveSong(creation:SongCreationData, ?callback:String -> SongCreationData -> Void) { var songAlreadyExists:Bool = [for (s in freeplayList.songs) s.name.toLowerCase()].contains(creation.meta.name.toLowerCase()); if (songAlreadyExists) { @@ -117,8 +117,14 @@ class CharterSelection extends EditorTreeMenu { CoolUtil.safeSaveFile('$songFolder/meta.json', Json.stringify(creation.meta, Flags.JSON_PRETTY_PRINT)); if (creation.instBytes != null) sys.io.File.saveBytes('$songFolder/song/Inst.${Flags.SOUND_EXT}', creation.instBytes); if (creation.voicesBytes != null) sys.io.File.saveBytes('$songFolder/song/Voices.${Flags.SOUND_EXT}', creation.voicesBytes); + + if (creation.playerVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Player.${Flags.SOUND_EXT}', creation.playerVocals); + if (creation.oppVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Opponent.${Flags.SOUND_EXT}', creation.oppVocals); #end + if (callback != null) + callback(songFolder, creation); + var option = new EditorIconOption(creation.meta.name, "Press ACCEPT to choose a difficulty to edit.", creation.meta.icon, function() { curSong = creation.meta; var list:Array = [ diff --git a/source/funkin/editors/charter/SongCreationScreen.hx b/source/funkin/editors/charter/SongCreationScreen.hx index becf6efd..52eb8d7f 100644 --- a/source/funkin/editors/charter/SongCreationScreen.hx +++ b/source/funkin/editors/charter/SongCreationScreen.hx @@ -3,17 +3,23 @@ package funkin.editors.charter; import flixel.group.FlxGroup; import flixel.text.FlxText.FlxTextFormat; import flixel.text.FlxText.FlxTextFormatMarkerPair; -import funkin.backend.chart.ChartData.ChartMetaData; +import funkin.backend.chart.BaseGameParser; +import funkin.backend.chart.ChartData; +import funkin.backend.utils.ZipUtil; +import haxe.Json; import haxe.io.Bytes; +import haxe.io.BytesInput; typedef SongCreationData = { var meta:ChartMetaData; var instBytes:Bytes; var voicesBytes:Bytes; + @:optional var playerVocals:Bytes; + @:optional var oppVocals:Bytes; } class SongCreationScreen extends UISubstateWindow { - private var onSave:Null -> Void = null; + private var onSave:Null -> Null SongCreationData -> Void> -> Void = null; public var songNameTextBox:UITextBox; public var bpmStepper:UINumericStepper; @@ -21,6 +27,7 @@ class SongCreationScreen extends UISubstateWindow { public var stepsPerBeatStepper :UINumericStepper; public var instExplorer:UIFileExplorer; public var voicesExplorer:UIFileExplorer; + public var importFrom:UIButton; public var displayNameTextBox:UITextBox; public var iconTextBox:UITextBox; @@ -30,6 +37,14 @@ class SongCreationScreen extends UISubstateWindow { public var colorWheel:UIColorwheel; public var difficultiesTextBox:UITextBox; + public var engineDropdown:UIDropDown; + public var createSong:UIButton; + + public var importInstExplorer:UIFileExplorer; + public var importVoicesExplorer:UIFileExplorer; + + public var importChartFile:UIFileExplorer; + public var backButton:UIButton; public var saveButton:UIButton; public var closeButton:UIButton; @@ -37,11 +52,21 @@ class SongCreationScreen extends UISubstateWindow { public var songDataGroup:FlxGroup = new FlxGroup(); public var menuDataGroup:FlxGroup = new FlxGroup(); + public var selectFormatGroup:FlxGroup = new FlxGroup(); + public var importAudioGroup:FlxGroup = new FlxGroup(); + public var importChartGroup:FlxGroup = new FlxGroup(); + public var pages:Array = []; public var pageSizes:Array = []; + + public var importPages:Array = []; + public var importPageSizes:Array = []; + + public var isImporting = false; + public var curPage:Int = 0; - public function new(?onSave:SongCreationData->Void) { + public function new(?onSave:SongCreationData -> Null SongCreationData -> Void> -> Void) { super(); if (onSave != null) this.onSave = onSave; } @@ -80,8 +105,6 @@ class SongCreationScreen extends UISubstateWindow { stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54); songDataGroup.add(stepsPerBeatStepper); - var voicesUIText:UIText = null; - instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (res) { var audioPlayer:UIAudioPlayer = new UIAudioPlayer(instExplorer.x + 8, instExplorer.y + 8, res); instExplorer.members.push(audioPlayer); @@ -100,6 +123,14 @@ class SongCreationScreen extends UISubstateWindow { songDataGroup.add(voicesExplorer); addLabelOn(voicesExplorer, "Vocal Audio File"); + importFrom = new UIButton(windowSpr.x + 20, windowSpr.y + windowSpr.bHeight - 16 - 32, "Import From...", function() { + winTitle = "Importing Song"; + isImporting = true; + updatePagesTexts(); + refreshPages(); + }, 150); + songDataGroup.add(importFrom); + var menuTitle:UIText; menuDataGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Menus Data (Freeplay/Story)", 28)); @@ -132,7 +163,53 @@ class SongCreationScreen extends UISubstateWindow { for (checkbox in [opponentModeCheckbox, coopAllowedCheckbox]) {checkbox.y += 6; checkbox.x += 4;} + var menuTitle:UIText; + selectFormatGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Import From:", 28)); + + engineDropdown = new UIDropDown(menuTitle.x, menuTitle.y + menuTitle.height + 36, 320, 32, ["Psych/Legacy FNF", "V-Slice Project (.fnfc)"]); + selectFormatGroup.add(engineDropdown); + addLabelOn(engineDropdown, "Chart Format"); + + createSong = new UIButton(windowSpr.x + 20, windowSpr.y + windowSpr.bHeight - 16 - 32, "< Back", function() { + winTitle = "Creating New Song"; + isImporting = false; + updatePagesTexts(); + refreshPages(); + }, 120); + selectFormatGroup.add(createSong); + + var menuTitle:UIText; + importAudioGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Add Audios", 28)); + + importInstExplorer = new UIFileExplorer(menuTitle.x, menuTitle.y + menuTitle.height + 36, null, null, Flags.SOUND_EXT, function (res) { + var audioPlayer:UIAudioPlayer = new UIAudioPlayer(importInstExplorer.x + 8, importInstExplorer.y + 8, res); + importInstExplorer.members.push(audioPlayer); + importInstExplorer.uiElement = audioPlayer; + }); + importAudioGroup.add(importInstExplorer); + addLabelOn(importInstExplorer, "Inst Audio File").applyMarkup( + "Inst Audio File $* Required$", + [new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]); + + importVoicesExplorer = new UIFileExplorer(importInstExplorer.x + 320 + 26, importInstExplorer.y, null, null, Flags.SOUND_EXT, function (res) { + var audioPlayer:UIAudioPlayer = new UIAudioPlayer(importVoicesExplorer.x + 8, importVoicesExplorer.y + 8, res); + importVoicesExplorer.members.push(audioPlayer); + importVoicesExplorer.uiElement = audioPlayer; + }); + importAudioGroup.add(importVoicesExplorer); + addLabelOn(importVoicesExplorer, "Vocal Audio File"); + + var menuTitle:UIText; + importChartGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Add Chart", 28)); + + importChartFile = new UIFileExplorer(menuTitle.x, menuTitle.y + menuTitle.height + 36, null, null, "fnfc"); + importChartGroup.add(importChartFile); + addLabelOn(importChartFile, "Chart File").applyMarkup( + "Chart File $* Required$", + [new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]); + saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() { + var pages = isImporting ? importPages : pages; if (curPage == pages.length-1) { saveSongInfo(); close(); @@ -165,17 +242,36 @@ class SongCreationScreen extends UISubstateWindow { pages.push(cast add(menuDataGroup)); pageSizes.push(FlxPoint.get(748 - 32 + 40, 400)); + importPages.push(cast add(selectFormatGroup)); + importPageSizes.push(FlxPoint.get(500, 250)); + + importPages.push(cast add(importAudioGroup)); + importPageSizes.push(FlxPoint.get(705, 250)); + + importPages.push(cast add(importChartGroup)); + importPageSizes.push(FlxPoint.get(460, 250)); + refreshPages(); updatePagesTexts(); } public override function update(elapsed:Float) { - if (curPage == 0) { - if (instExplorer.file != null) + if (isImporting) + { + if (curPage == 1) { + importInstExplorer.selectable = importVoicesExplorer.selectable = (engineDropdown.index != 1); + saveButton.selectable = (engineDropdown.index == 1) ? true : (importInstExplorer.file != null); + } else if (curPage == 2) { + importChartFile.fileType = (engineDropdown.index == 1) ? "fnfc" : "json"; + saveButton.selectable = (importChartFile.file != null); + } else saveButton.selectable = true; - else saveButton.selectable = false; - } else - saveButton.selectable = true; + } else { + if (curPage == 0) + saveButton.selectable = (instExplorer.file != null); + else + saveButton.selectable = true; + } saveButton.alpha = saveButton.field.alpha = saveButton.selectable ? 1 : 0.4; super.update(elapsed); @@ -183,10 +279,15 @@ class SongCreationScreen extends UISubstateWindow { function refreshPages() { for (i=>page in pages) - page.visible = page.exists = i == curPage; + page.visible = page.exists = i == curPage && !isImporting; + + for (i=>page in importPages) + page.visible = page.exists = i == curPage && isImporting; } function updatePagesTexts() { + var pageSizes = isImporting ? importPageSizes : pageSizes; + var pages = isImporting ? importPages : pages; windowSpr.bWidth = Std.int(pageSizes[curPage].x); windowSpr.bHeight = Std.int(pageSizes[curPage].y); @@ -194,7 +295,7 @@ class SongCreationScreen extends UISubstateWindow { titleSpr.y = windowSpr.y + ((30 - titleSpr.height) / 2); saveButton.field.text = curPage == pages.length-1 ? "Save & Close" : 'Next >'; - titleSpr.text = 'Creating New Song (${curPage+1}/${pages.length})'; + titleSpr.text = '$winTitle (${curPage+1}/${pages.length})'; backButton.field.text = '< Back'; backButton.visible = backButton.exists = curPage > 0; @@ -202,7 +303,7 @@ class SongCreationScreen extends UISubstateWindow { backButton.x = (saveButton.x = windowSpr.x + windowSpr.bWidth - 20 - 125) - 20 - saveButton.bWidth; closeButton.x = (curPage > 0 ? backButton : saveButton).x - 20 - saveButton.bWidth; - for (button in [saveButton, backButton, closeButton]) + for (button in [saveButton, backButton, closeButton, importFrom, createSong]) button.y = windowSpr.y + windowSpr.bHeight - 16 - 32; } @@ -226,28 +327,100 @@ class SongCreationScreen extends UISubstateWindow { } function saveSongInfo() { - for (stepper in [bpmStepper, beatsPerMeasureStepper, stepsPerBeatStepper]) - @:privateAccess stepper.__onChange(stepper.label.text); + if (isImporting) + { + if (engineDropdown.index == 1) + { + var reader = new ZipReader(new BytesInput(importChartFile.file)); + var fields = reader.read(); + var files:Map = []; - var meta:ChartMetaData = { - name: songNameTextBox.label.text, - bpm: bpmStepper.value, - beatsPerMeasure: Std.int(beatsPerMeasureStepper.value), - stepsPerBeat: Std.int(stepsPerBeatStepper.value), - displayName: displayNameTextBox.label.text, - icon: iconTextBox.label.text, - color: colorWheel.curColorString, - parsedColor: colorWheel.curColor, - opponentModeAllowed: opponentModeCheckbox.checked, - coopAllowed: coopAllowedCheckbox.checked, - difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()], - }; + for(k=>field in fields) { + var fileName = field.fileName; + var fileContent = field.data; + files.set(fileName, fileContent); + } - if (onSave != null) onSave({ - meta: meta, - instBytes: instExplorer.file, - voicesBytes: voicesExplorer.file - }); + var manifest = Json.parse(files.get("manifest.json")); + var vslicemeta = Json.parse(files.get('${manifest.songId}-metadata.json')); + var firstTimeChange = vslicemeta.timeChanges[0]; + var vslicechart = Json.parse(files.get('${manifest.songId}-chart.json')); + var difficulties = Reflect.fields(vslicechart.notes); + + var playData = vslicemeta.playData; + + var instFile = files.get('Inst.${Flags.SOUND_EXT}'); + var voicesFiles = files.get('Voices.${Flags.SOUND_EXT}'); //it may exist + var playerVoices = files.get('Voices-${playData.characters.playerVocals[0]}.${Flags.SOUND_EXT}'); + var oppVoices = files.get('Voices-${playData.characters.opponentVocals[0]}.${Flags.SOUND_EXT}'); + + var meta:ChartMetaData = { + name: manifest.songId, + bpm: CoolUtil.getDefault(firstTimeChange.bpm, 100), + beatsPerMeasure: CoolUtil.getDefault(firstTimeChange.n, Flags.DEFAULT_BEATS_PER_MEASURE), + stepsPerBeat: CoolUtil.getDefault(firstTimeChange.d, 4), + displayName: vslicemeta.songName, + icon: Flags.DEFAULT_HEALTH_ICON, + color: "#FFFFFF", + parsedColor: 0xFFFFFF, + opponentModeAllowed: true, + coopAllowed: true, + difficulties: difficulties, + }; + + if (onSave != null) onSave({ + meta: meta, + instBytes: instFile, + voicesBytes: voicesFiles, + playerVocals: playerVoices, + oppVocals: oppVoices, + }, (songFolder:String, creation: SongCreationData) -> { + #if sys + for (diff in difficulties) + { + var vsliceChartData = Reflect.field(vslicechart.notes, diff); + var chartPath = Paths.chart(manifest.songId, diff); + var base:ChartData = { + strumLines: [], + noteTypes: [], + events: [], + meta: { + name: null + }, + scrollSpeed: Reflect.field(vslicechart.scrollSpeed, diff), + stage: Flags.DEFAULT_STAGE, + codenameChart: true, + fromMods: Paths.assetsTree.existsSpecific(chartPath, "TEXT", MODS) + }; + BaseGameParser.parseChart(vsliceChartData, vslicemeta, vslicechart.events, base); + CoolUtil.safeSaveFile('$songFolder/charts/$diff.json', Json.stringify(base, Flags.JSON_PRETTY_PRINT)); + } + #end + }); + } + } else { + for (stepper in [bpmStepper, beatsPerMeasureStepper, stepsPerBeatStepper]) + @:privateAccess stepper.__onChange(stepper.label.text); + + var meta:ChartMetaData = { + name: songNameTextBox.label.text, + bpm: bpmStepper.value, + beatsPerMeasure: Std.int(beatsPerMeasureStepper.value), + stepsPerBeat: Std.int(stepsPerBeatStepper.value), + displayName: displayNameTextBox.label.text, + icon: iconTextBox.label.text, + color: colorWheel.curColorString, + parsedColor: colorWheel.curColor, + opponentModeAllowed: opponentModeCheckbox.checked, + coopAllowed: coopAllowedCheckbox.checked, + difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()], + }; + + if (onSave != null) onSave({ + meta: meta, + instBytes: instExplorer.file, + voicesBytes: voicesExplorer.file + }, null); + } } - } \ No newline at end of file diff --git a/source/funkin/editors/ui/UIFileExplorer.hx b/source/funkin/editors/ui/UIFileExplorer.hx index a122dcec..41135829 100644 --- a/source/funkin/editors/ui/UIFileExplorer.hx +++ b/source/funkin/editors/ui/UIFileExplorer.hx @@ -15,12 +15,16 @@ class UIFileExplorer extends UISliceSprite { public var uiElement:UISprite; + public var fileType:String = "txt"; + public function new(x:Float, y:Float, ?w:Int, ?h:Int, fileType:String = "txt", ?onFile:Bytes->Void) { super(x, y, (w != null ? w : 320), (h != null ? h : 58), 'editors/ui/inputbox'); + this.fileType = fileType; if (onFile != null) this.onFile = onFile; uploadButton = new UIButton(x + 8, y+ 8, "", function () { + if (!selectable) return; var fileDialog = new FileDialog(); fileDialog.onOpen.add(function(res) { file = cast res; @@ -28,7 +32,7 @@ class UIFileExplorer extends UISliceSprite { if (this.onFile != null) this.onFile(file); }); - fileDialog.open(fileType); + fileDialog.open(this.fileType); }, bWidth - 16, bHeight - 16); members.push(uploadButton); @@ -63,6 +67,7 @@ class UIFileExplorer extends UISliceSprite { } public function removeFile() { + if (!selectable) return; if (uiElement != null) { members.remove(uiElement); uiElement.destroy();