Skip to content

Commit

Permalink
Prepare Funker Selector for Funkin' v0.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AbnormalPoof committed Feb 23, 2025
1 parent 2628bc3 commit 36b12ce
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 494 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased] - 2025-??-??
### Added
- v0.6.0 support
- Added basic mouse support. You can now click through the menu and select characters using only the mouse.
- This is not mobile support.
- HOME and END now jump to the far left and far right of the menu respectively.
### Changed
- Complete refactor of the code for better readability.
- All scripts now follow a naming scheme, using a `FS_` prefix.
- A good portion of the code has been split into smaller modules.
- This should hopefully make the code a lot easier to go through, since it isn't so convoluted anymore.
- Optimized JSON parsing, the code for that should be a lot cleaner!
### Fixed
- Fixed an issue where Girlfriend was layered on top of the opponent and player if you had a speaker character selected in the Tankman Battlefield stage.

## [2.0.0] - 2024-12-17
### Added
Expand Down
12 changes: 12 additions & 0 deletions _merge/data/stages/tankmanBattlefield.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"op": "replace",
"path": "/characters/bf/zIndex",
"value": 150
},
{
"op": "replace",
"path": "/characters/dad/zIndex",
"value": 150
}
]
7 changes: 3 additions & 4 deletions data/funkerSelector/dad.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
"text": "The father of Girlfriend and an ex-rockstar, he is very disapproving of Girlfriend's relationship with Boyfriend.",
"unlockCondition": "Beat Week 1 on Hard."
},
"characterMenu":
{
"position": [100, 50],
"characterMenu": {
"position": [150, 50],
"scale": 0.75
}
}
}
84 changes: 10 additions & 74 deletions scripts/modules/FS_CharacterDataHandler.hxc
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterDataParser;
import funkin.save.Save;
import funkin.util.MemoryUtil;
import funkin.util.SerializerUtil;
import funkin.util.assets.DataAssets;
import haxe.ds.StringMap;
import haxe.ds.Either;
import hxjsonast.Parser;
import hxjsonast.Tools;
import thx.Objects;

/**
* A module responsible for handling character caches and data.
Expand Down Expand Up @@ -41,8 +43,6 @@ class FS_CharacterDataHandler extends Module

/**
* The default character data. This is used as base.
*
* TODO: Use this in `parseJSONData()` when v0.6.0 is out.
*/
var FS_DEFAULT_CHARACTER_DATA:FS_characterData =
{
Expand All @@ -55,16 +55,16 @@ class FS_CharacterDataHandler extends Module
unlockMethod: null,
description:
{
text: "This is placeholder data! If you're somehow reading this then you probably did something really wrong.",
unlockCondition: "This is placeholder data! If you're somehow reading this then you probably did something really wrong."
text: "No description was specified in the JSON file.",
unlockCondition: "No unlock condition was specified in the JSON file."
},
characterMenu:
{
position: [200, 0],
scale: 1.0,
isPixel: false,
flipX: false,
selectedAnim: null
selectedAnim: "hey"
},
suffixes:
{
Expand Down Expand Up @@ -189,72 +189,23 @@ class FS_CharacterDataHandler extends Module
// Check if the file exists
if (Assets.exists(filePath))
{
var parsedData = SerializerUtil.fromJSON(Assets.getText(filePath));
var parsedData = Tools.getValue(Parser.parse(Assets.getText(filePath), filePath));

if (parsedData == null)
{
trace("[Funker Selector] Failed to parse " + Assets.getPath(filePath) + "!");
return null;
}

// I basically just put this all in one big object so that I don't have to do a bunch of null checks lol
var result:FS_characterData =
{
version: nullCoalesce(parsedData?.version, "1.0.0"),
characterID: nullCoalesce(parsedData?.characterID, null),
characterType: nullCoalesce(parsedData?.characterType, "bf"),
mustUnlock: nullCoalesce(parsedData?.mustUnlock, false),
voiceID: nullCoalesce(parsedData?.voiceID, parsedData?.characterID),
introSwapFrame: nullCoalesce(parsedData?.introSwapFrame, 3),
unlockMethod: nullCoalesce(parsedData?.unlockMethod, null),
description:
{
text: nullCoalesce(parsedData?.description?.text, "No description was specified in the JSON file."),
unlockCondition: nullCoalesce(parsedData?.description?.unlockCondition, "No unlock condition was specified in the JSON file.")
},
characterMenu:
{
position: nullCoalesce(parsedData?.characterMenu?.position, [0, 0]),
scale: calculateCharacterScale(parsedData.characterMenu?.scale, parsedData?.characterMenu?.isPixel),
isPixel: nullCoalesce(parsedData?.characterMenu?.isPixel, false),
flipX: nullCoalesce(parsedData?.characterMenu?.flipX, false),
selectedAnim: nullCoalesce(parsedData?.characterMenu?.selectedAnim, "hey")
},
// We still set suffixes to null to account for scripts.
suffixes:
{
gameOverMusic: nullCoalesce(parsedData?.suffixes?.gameOverMusic, null),
blueBall: nullCoalesce(parsedData?.suffixes?.blueBall, null),
pauseMusic: nullCoalesce(parsedData?.suffixes?.pauseMusic, null)
},
characterVariations: nullCoalesce(parsedData?.characterVariations, [])
};
// Merge the parsed data with the default character data, basically layering the parsed object on top of the default one.
var result:FS_characterData = Objects.deepCombine(Objects.clone(FS_DEFAULT_CHARACTER_DATA), parsedData);

return result;
}

return null;
}

/**
* Calculates the scale of the character.
* @return The calculated scale value.
*/
function calculateCharacterScale(scale:Float, isPixel:Bool):Float
{
if (scale == null)
{
return 1;
}

if (isPixel)
{
return scale * 6;
}

return scale;
}

/**
* Whether or not the character should be shown.
* @param characterID The character ID we're using.
Expand Down Expand Up @@ -316,7 +267,7 @@ class FS_CharacterDataHandler extends Module
return false;
}
default:
trace("[Funker Selector] Unlock Method " + unlockMethod.type + " not recognized.");
trace("[Funker Selector] Unlock Method " + unlockMethod?.type + " not recognized.");
return false;
}
}
Expand Down Expand Up @@ -487,21 +438,6 @@ class FS_CharacterDataHandler extends Module
}
MemoryUtil.collect(true);
}

/**
* HELPER FUNCTIONS
* These are functions that are simply nice to have. They make the code cleaner.
*/
/**
* Imitates null coalescing since HScript doesn't support it yet.
* @param value The value to check.
* @param fallback The fallback value.
* @return The value if it's not null, otherwise the fallback value.
*/
public function nullCoalesce(value:Dynamic, fallback:Dynamic):Dynamic
{
return value != null ? value : fallback;
}
}

/**
Expand Down
75 changes: 26 additions & 49 deletions scripts/modules/FS_CoreModule.hxc
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ class FS_CoreModule extends Module
{
var fs_characterData:Dynamic = FS_CharacterDataHandler.scriptCall('getCharacterData', [characterID]);
var oldChar:BaseCharacter = null;

var voiceID:String = fs_characterData?.voiceID;
var voiceID:String = fs_characterData?.voiceID ?? characterID;
var variationSuffix:String = PlayState.instance.currentVariation != 'default' ? '-' + PlayState.instance.currentVariation : '';
var voiceFile:String = Paths.voices(PlayState.instance.currentSong.id, '-' + voiceID + variationSuffix);

Expand Down Expand Up @@ -224,12 +223,12 @@ class FS_CoreModule extends Module
}
else
{
variationMatch = true; // Assume true if songVariation does not exist.
variationMatch = true; // Assume true if `songVariation` does not exist.
}

if (songMatch && variationMatch)
{
fs_characterData = nullCoalesce(FS_CharacterDataHandler.scriptCall('getCharacterData', [variation.characterID]), fs_characterData);
fs_characterData = FS_CharacterDataHandler.scriptCall('getCharacterData', [variation.characterID]) ?? fs_characterData;

if (FS_CharacterDataHandler.scriptCall('charJSONCheck', [variation.characterID]))
{
Expand Down Expand Up @@ -270,9 +269,9 @@ class FS_CoreModule extends Module

if (characterType == CharacterType.BF)
{
PauseSubState.musicSuffix = nullCoalesce(fs_characterData.suffixes.pauseMusic, PauseSubState.musicSuffix);
GameOverSubState.musicSuffix = nullCoalesce(fs_characterData.suffixes.gameOverMusic, GameOverSubState.musicSuffix);
GameOverSubState.blueBallSuffix = nullCoalesce(fs_characterData.suffixes.blueBall, GameOverSubState.blueBallSuffix);
PauseSubState.musicSuffix = fs_characterData?.suffixes?.pauseMusic ?? PauseSubState.musicSuffix;
GameOverSubState.musicSuffix = fs_characterData?.suffixes?.gameOverMusic ?? GameOverSubState.musicSuffix;
GameOverSubState.blueBallSuffix = fs_characterData?.suffixes?.blueBall ?? GameOverSubState.blueBallSuffix;
}

trace('[Funker Selector] Suffixes\n\nPause Music: ' + PauseSubState.musicSuffix + '\nGame Over Music: ' + GameOverSubState.musicSuffix
Expand All @@ -290,8 +289,8 @@ class FS_CoreModule extends Module
var result:VoicesGroup = new VoicesGroup();
var songVoices:Array<String> = currentChart.buildVoiceList();

result.addPlayerVoice(FunkinSound.load(nullCoalesce(voiceList[0], songVoices[0])));
result.addOpponentVoice(FunkinSound.load(nullCoalesce(voiceList[1], songVoices[1])));
result.addPlayerVoice(FunkinSound.load(voiceList[0] ?? songVoices[0]));
result.addOpponentVoice(FunkinSound.load(voiceList[1] ?? songVoices[1]));

result.playerVoicesOffset = currentChart.offsets.getVocalOffset(currentChart.characters.player);
result.opponentVoicesOffset = currentChart.offsets.getVocalOffset(currentChart.characters.opponent);
Expand All @@ -309,8 +308,8 @@ class FS_CoreModule extends Module
super.onCreate(event);
// Cache the sprites for JSON characters.
// This is only done once.
if (FS_SaveDataHandler.scriptCall('getPreference', ["preloadSprites"])
&& !FS_SaveDataHandler.scriptCall('getPreference', ["potatoMode"])) FS_CharacterDataHandler.scriptCall('cacheJSONSparrows');
if (FS_SaveDataHandler.scriptGet('saveData').preferences.preloadSprites
&& !FS_SaveDataHandler.scriptGet('saveData').preferences.potatoMode) FS_CharacterDataHandler.scriptCall('cacheJSONSparrows');
}

override function onUpdate(event:UpdateScriptEvent):Void
Expand Down Expand Up @@ -426,15 +425,15 @@ class FS_CoreModule extends Module
var prefs = event.targetState.pages.get("preferences");
if (prefs != null)
{
prefs.add(prefs.items.createItem(120, 120 * prefs.items.length + 30, "-- FUNKER SELECTOR --", "bold", () -> {})).fireInstantly = true;

prefs.createPrefItemCheckbox("Simplify UI", "Simplifies the UI and disables character sprite caching.", (value) -> {
FS_SaveDataHandler.scriptCall('setPreference', ["potatoMode", value]);
}, FS_SaveDataHandler.scriptCall('getPreference', ["potatoMode"]));
FS_SaveDataHandler.scriptGet('saveData').preferences.potatoMode = value;
FS_SaveDataHandler.scriptCall('writeSaveData');
}, FS_SaveDataHandler.scriptGet('saveData').preferences.potatoMode);

prefs.createPrefItemCheckbox("Preload Sprites",
"Whether to preload the character sprites or not. Will cause lag on the Character Selection Menu if off!", (value) -> {
FS_SaveDataHandler.scriptCall('setPreference', ["preloadSprites", value]);
FS_SaveDataHandler.scriptGet('saveData').preferences.preloadSprites = value;
FS_SaveDataHandler.scriptCall('writeSaveData');
if (value)
{
FS_CharacterDataHandler.scriptCall("cacheJSONSparrows");
Expand All @@ -444,11 +443,12 @@ class FS_CoreModule extends Module
// Remove the JSON Characters from memory when the user disables the option.
FS_CharacterDataHandler.scriptCall("purgeJSONSparrowCache");
}
}, FS_SaveDataHandler.scriptCall('getPreference', ["preloadSprites"]));
}, FS_SaveDataHandler.scriptGet('saveData').preferences.preloadSprites);

prefs.createPrefItemEnum('Menu SFX', 'Change the SFX used for the Character Menu.',
["funkin" => "Funkin' Main Menu", "charSelect" => "Funkin' Character Select"], (value) -> {
FS_SaveDataHandler.scriptCall('setPreference', ["preferredSFX", value]);
FS_SaveDataHandler.scriptGet('saveData').preferences.preferredSFX = value;
FS_SaveDataHandler.scriptCall('writeSaveData');
if (value == "charSelect")
{
FunkinSound.playOnce(Paths.sound('CS_confirm'));
Expand All @@ -457,14 +457,13 @@ class FS_CoreModule extends Module
{
FunkinSound.playOnce(Paths.sound('confirmMenu'));
}
}, FS_SaveDataHandler.scriptCall('getPreference', ["preferredSFX"]));
}, FS_SaveDataHandler.scriptGet('saveData').preferences.preferredSFX);

prefs.createPrefItemCheckbox("DJ Replacement", "When enabled, the Freeplay DJ can be swapped out for the currently selected character's own DJ.",
(value) -> {
FS_SaveDataHandler.scriptCall('setPreference', ["djSwapping", value]);
}, FS_SaveDataHandler.scriptCall('getPreference', ["djSwapping"]));

prefs.add(prefs.items.createItem(120, 120 * prefs.items.length, "-------------------", "bold", () -> {})).fireInstantly = true;
FS_SaveDataHandler.scriptGet('saveData').preferences.djSwapping = value;
FS_SaveDataHandler.scriptCall('writeSaveData');
}, FS_SaveDataHandler.scriptGet('saveData').preferences.djSwapping);
}
}
}
Expand Down Expand Up @@ -499,26 +498,16 @@ class FS_CoreModule extends Module
|| PlayState.instance.isMinimalMode
|| PlayState.instance.isChartingMode) return;

// Reset the voiceList, swap out the characters, and
// replace vocals if any are found.

// Setting voiceList to null in order to reset the voices,
// since module variables persist between state changes.
voiceList = [null, null];

// Replace and swap out the characters.
replaceChar(FS_SaveDataHandler.scriptGet('saveData').characterIDs.bf, CharacterType.BF);
replaceChar(FS_SaveDataHandler.scriptGet('saveData').characterIDs.gf, CharacterType.GF);
replaceChar(FS_SaveDataHandler.scriptGet('saveData').characterIDs.dad, CharacterType.DAD);
PlayState.instance.currentStage.refresh();

// If at least ONE of them was changed (means a vocal file was found), replace the vocals.
if (voiceList[0] != null || voiceList[1] != null)
{
trace('[Funker Selector] Voice List:\n\nPlayer: ' + voiceList[0] + '\nOpponent: ' + voiceList[1]);
PlayState.instance.vocals.stop();
PlayState.instance.vocals = replaceVocals(voiceList);
}
trace('[Funker Selector] Voice List:\n\nPlayer: ' + voiceList[0] + '\nOpponent: ' + voiceList[1]);
PlayState.instance.vocals.stop();
PlayState.instance.vocals = replaceVocals(voiceList);
}

/**
Expand All @@ -531,18 +520,6 @@ class FS_CoreModule extends Module
*/
public function isDebugBuild():Bool
{
// Debug builds use a different background color.
return Application.current.window.context.attributes.background == 0xFFFF00FF;
}

/**
* Imitates null coalescing since HScript doesn't support it yet.
* @param value The value to check.
* @param fallback The fallback value.
* @return The value if it's not null, otherwise the fallback value.
*/
public function nullCoalesce(value:Dynamic, fallback:Dynamic):Dynamic
{
return value != null ? value : fallback;
return Constants?.DEBUG_BUILD ?? false;
}
}
Loading

0 comments on commit 36b12ce

Please sign in to comment.