Skip to content

Commit

Permalink
init v1
Browse files Browse the repository at this point in the history
  • Loading branch information
cyn0x8 committed Nov 14, 2024
0 parents commit 13668d9
Show file tree
Hide file tree
Showing 20 changed files with 1,634 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.hxc linguist-language=Haxe
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_STORE

.vs
.vscode
9 changes: 9 additions & 0 deletions CODE_LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2024 cyn0x8

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Binary file added _polymod_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions _polymod_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"title": "modlauncher",
"description": "an ingame launcher for friday night funkin mod states",
"homepage": "https://git.gay/cyn0x8/modlauncher",

"contributors": [
{"name": "cyn0x8", "role": "coder"}
],

"license": "MIT",

"api_version": "0.5.0",
"mod_version": "1.0.0"
}
Binary file added fonts/modlauncher/jetbrains_mono_medium.ttf
Binary file not shown.
Binary file added images/modlauncher/menu/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/modlauncher/meta/example_bound.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/modlauncher/meta/example_none.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/modlauncher/meta/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# modlauncher

an ingame launcher for friday night funkin mod states

## overview

just drag `modlauncher.zip` into your mods folder and you're good to go!

when in the main menu, press `TAB` to open the launcher ui and your `BACK` key to close it

in the launcher, you will see banners of any mods that have bound to the launcher

you can navigate between them with your `UI_LEFT` and `UI_RIGHT` keys, and press your `ACCEPT` key to "launch" the selected mod

you can also cancel the selection by pressing `BACK` before the state transition starts

## screenshots

![example_none](./images/modlauncher/meta/example_none.png)

![example_bound](./images/modlauncher/meta/example_bound.png)

## for developers

to bind your mod, you must call `bind` from the `MODLAUNCHER_Registry` module and pass in a single struct with the following fields:

|field|type|description|
|-|-|-|
|`name`|`String`|the name of your mod as it will appear in the launcher|
|`target`|`String`|class name of the `ScriptedMusicBeatState` you want to open|
|`logo_path`|`String`|the path to your mod's logo (will be passed into `Paths.image`)<br>defaults to the modlauncher icon|
|`select_sound_path`|`Null<String>`|the path to your mod's select sound (will be passed into `Paths.sound`)<br>defaults to `"confirmMenu"`|
|`select_duration`|`Null<Float>`|the time in seconds from when the player selcts your mod to the end of the state transition, minimum `1.5`<br>the selection is cancellable until `0.5` seconds before this duration (when the state transition starts)<br>defaults to `1.5`|
|`on_setup`|`Null<(Dynamic)->Void>`|callback to run when modlauncher is injected into the main menu, useful for setting up your mod banner in the launcher|
|`on_update`|`Null<(Dynamic, Float)->Void>`|callback to run every frame while the launcher is open, useful for animating your banner<br>the 2nd parameter is delta time in seconds|
|`on_focus`|`Null<(Dynamic)->Void>`|callback to run when your mod is "focused" on|
|`on_unfocus`|`Null<(Dynamic)->Void>`|callback to run when another mod is focused on away from yours|
|`on_select`|`Null<(Dynamic)->Void>`|callback to run when your mod is selected|
|`on_cancel`|`Null<(Dynamic)->Void>`|callback to run when your mod selection is cancelled before the state transition starts|
|`on_init`|`Null<(Dynamic)->Void>`|callback to run after your mod is selected and right before your target state is initialized, useful for "initializing" your mod|

the `Dynamic` parameter passed into the callbacks is the same as the struct you passed into `bind`, but with a few more fields used for the banner:

|field|type|description|
|-|-|-|
|`cam`|`FunkinCamera`|the camera of your mod's banner in the launcher|
|`bg_group`|`FlxTypedSpriteGroup`|bg group for your mod's banner|
|`ui_group`|`FlxTypedSpriteGroup`|ui group for your mod's banner|
|`logo`|`FunkinSprite`|the logo of your mod, part of `ui_group`|

if you modify this parameter, it will carry over into the callbacks

---

example binding:

```haxe
import flixel.FlxG;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.modding.module.ModuleHandler;
import funkin.modding.module.ScriptedModule;
class MyMod_LauncherBinding extends ScriptedModule {
public function new() {
super("MyMod_LauncherBinding");
}
override public function onCreate(event:ScriptEvent):Void {
try_bind();
}
private function try_bind():Void {
var launcher:Null<ScriptedModule> = null;
if ((launcher = ModuleHandler.getModule("MODLAUNCHER_Registry")) != null) {
launcher.scriptCall("bind", [{
name: "My Mod",
target: "MyMod_InitState",
logo_path: "my_mod/logo",
select_sound_path: "my_mod/launcher_select",
select_sound_length: 2.5,
on_setup: function(data:Dynamic):Void {
data.cam.bgColor = 0xff808080;
data.logo.scale.set(0.5, 0.5);
data.logo.updateHitbox();
},
on_select: function(data:Dynamic):Void {
data.cam.flash(0xffffffff, 0.5, null, true);
FlxTween.globalManager.cancelTweensOf(data.logo.scale);
data.logo.scale.set(0.45, 0.45);
FlxTween.tween(data.logo.scale, {x: 0.75, y: 0.75}, 1.5, {ease: FlxEase.expoOut});
},
on_cancel: function(data:Dynamic):Void {
FlxTween.globalManager.cancelTweensOf(data.logo.scale);
FlxTween.tween(data.logo.scale, {x: 0.5, y: 0.5}, 0.5, {ease: FlxEase.expoOut});
}
on_init: function(data:Dynamic):Void {
ModuleHandler.getModule("MyMod_Globals").scriptSet("my_variable", true);
}
}]);
}
// bind to other fnf mod launchers/managers maybe? up to you
FlxG.signals.postStateSwitch.addOnce(function():Void {
try_bind();
});
}
}
```

> [!note]
> bound mods do not persist through polymod reload! you must re-bind your mods on post state switch
84 changes: 84 additions & 0 deletions scripts/modlauncher/modules/Injector.hxc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import flixel.FlxG;
import flixel.FlxState;
import flixel.addons.transition.TransitionData;
import flixel.addons.transition.TransitionCameraMode;
import flixel.math.FlxPoint;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;

import funkin.graphics.FunkinCamera;
import funkin.modding.base.ScriptedFlxSpriteGroup;
import funkin.modding.module.ModuleHandler;
import funkin.modding.module.ScriptedModule;
import funkin.ui.mainmenu.MainMenuState;

class MODLAUNCHER_Injector extends ScriptedModule {
public function new() {
super("MODLAUNCHER_Injector", 2147483647);
}

private var cam_ui:FunkinCamera = null;
private var cam_bg:FunkinCamera = null;

private var menu:ScriptedModule = null;

override public function onStateChangeEnd(event:StateChangeScriptEvent):Void {
if (Std.isOfType(event.targetState, MainMenuState)) {
try_inject(event.targetState);
FlxG.signals.preStateSwitch.addOnce(function():Void {
destroy();
});
}
}

private var injected:Bool = false;
public function try_inject(state:FlxState):Void {
if (injected) {
return;
}

cam_bg = new FunkinCamera("modlauncher_bg", 0, 0, 1280, 720, 0);
cam_bg.bgColor = 0x00000000;
FlxG.cameras.add(cam_bg, false);

cam_ui = new FunkinCamera("modlauncher_cam", 0, 0, 1280, 720, 1);
cam_ui.bgColor = 0x00000000;
FlxG.cameras.add(cam_ui, false);

menu = ModuleHandler.getModule("MODLAUNCHER_LauncherMenu");
menu.scriptCall("setup");

FlxG.state.add(menu.scriptGet("menu"));

menu.scriptCall("fix_scrolls");

FlxG.state.leftWatermarkText.cameras = [cam_ui];
FlxG.state.remove(FlxG.state.leftWatermarkText, true);
FlxG.state.add(FlxG.state.leftWatermarkText);

FlxG.cameras.remove(cam_ui, false);
FlxG.cameras.add(cam_ui, false);

injected = true;
}

private function destroy():Void {
if (injected) {
menu.scriptCall("destroy");
menu = null;

FlxG.cameras.remove(cam_ui);
cam_ui.destroy();
cam_ui = null;

FlxG.state.leftWatermarkText.cameras = [FlxG.camera];
}

injected = false;
}

override public function onDestroy(event:ScriptEvent):Void {
destroy();
}
}
64 changes: 64 additions & 0 deletions scripts/modlauncher/modules/Registry.hxc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Float;
import String;

import haxe.ds.StringMap;

import thx.Objects;
import thx.Types;

import funkin.modding.module.ModuleHandler;
import funkin.modding.module.ScriptedModule;

import StringTools;

// see readme for documentation

class MODLAUNCHER_Registry extends ScriptedModule {
public function new() {
super("MODLAUNCHER_Registry", -2147483646);
}

public var registry:StringMap<Dynamic> = new StringMap();
public function bind(data:Dynamic):Null<Dynamic> {
if (!Types.isAnonymousObject(data)) {
return null;
}

var target:Null<String> = Objects.getPath(data, "target");
if (target == null || !Std.isOfType(target, String) || StringTools.trim(target) == "") {
return null;
}

var name:Null<String> = Objects.getPath(data, "name");
if (name == null || !Std.isOfType(name, String) || StringTools.trim(name) == "") {
return null;
}

var logo_path:Null<String> = Objects.getPath(data, "logo_path");
if (logo_path == null || !Std.isOfType(logo_path, String)) {
Objects.setPath(data, "logo_path", "modlauncher/meta/icon");
}

var select_sound_path:Null<String> = Objects.getPath(data, "select_sound_path");
if (select_sound_path == null || !Std.isOfType(select_sound_path, String) || StringTools.trim(select_sound_path) == "") {
select_sound_path = "confirmMenu";
}

var select_duration:Null<Float> = Objects.getPath(data, "select_duration");
if (select_duration == null || !Std.isOfType(select_duration, Float)) {
Objects.setPath(data, "select_duration", 1.5);
}

Objects.setPath(data, "select_duration", Math.max(1.5, select_duration));

for (func_name in ["on_setup", "on_update", "on_focus", "on_unfocus", "on_select", "on_cancel", "on_init"]) {
if (Objects.getPath(data, func_name) == null) {
Objects.setPath(data, func_name, function():Void {});
}
}

registry.set(name, data);

return data;
}
}
64 changes: 64 additions & 0 deletions scripts/modlauncher/modules/Reloader.hxc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import haxe.ds.IntMap;

import thx.Objects;

import flixel.FlxG;

import funkin.modding.base.ScriptedMusicBeatState;
import funkin.modding.module.ModuleHandler;
import funkin.modding.module.ScriptedModule;

typedef Reload = {
module:String,
variables:Array<()->Dynamic>
}

class MODLAUNCHER_Reloader extends ScriptedModule {
public function new() {
super("MODLAUNCHER_Reloader", -2147483647);
}

public var on_reload_pre:IntMap<Reload> = new IntMap();
public var on_reload_post:IntMap<Reload> = new IntMap();
private var reload:(IntMap<Reload>)->Void = function(reload_map:IntMap<Reload>):void {
var keys:Array<Int> = new Array();
for (key in reload_map.keys()) {
keys.push(key);
}

keys.sort(function(a:Int, b:Int):Int {
return a - b;
});

for (key in keys) {
ModuleHandler.getModule(reload_map.get(key).module).scriptGet("initialize")();

if (Objects.getPath(reload_map.get(key), "variables") != null) {
for (variable in reload_map.get(key).variables) {
ModuleHandler.getModule(reload_map.get(key).module).scriptSet(
variable().name,
variable().value
);
}
}
}
}

override public function onDestroy(event:ScriptEvent):Void {
FlxG.signals.preStateSwitch.addOnce(function():Void {
reload(on_reload_pre);
});

FlxG.signals.postStateSwitch.addOnce(function():Void {
reload(on_reload_post);
});
}

public function clear():Void {
on_reload_pre.clear();
on_reload_pre = new IntMap();

on_reload_post.clear();
on_reload_post = new IntMap();
}
}
Loading

0 comments on commit 13668d9

Please sign in to comment.