GD Resources logo

Chapter 1: Custom Levels #

OmgRod avatar
Written by:
OmgRod
Owner

A lot of the code in the following tutorial comes from GD Surge by OmgRod (which parts of it come from other existing GD fangames like Lunar and Odyssey), so credit to the developers of these mods for writing this code!

Prerequisites #

  • Rod's Fangame Tools (Geode mod)
  • Geode SDK
  • A PC (mobile could work but it's really annoying)
  • Permission to use certain songs (if applicable)

A YouTube tutorial for this is coming very soon... 👀

Creating custom savefiles (CRITICAL) #

Before we even start messing with the GD main levels, we MUST create custom savefiles for the specific mod (to prevent getting leaderboard banned). To do this, we hook GManager to ensure the savefile that is created/loaded has a slightly different name:

cpp
1#include <Geode/Geode.hpp>
2#include <Geode/modify/GManager.hpp>
3
4using namespace geode::prelude;
5
6class $modify(MyGManager, GManager) {
7 void setup() {
8 auto compare = std::string(m_fileName);
9 compare.insert(std::string_view(compare).find(".dat"), "[YOUR FANGAME NAME PREFERABLY WITHOUT SPACES/SPECIAL CHARACTERS]");
10 m_fileName = compare;
11 GManager::setup();
12 }
13};

How does GD get its main levels? #

GD gets its main level metadata from the LevelTools::getLevel function and the actual level strings from the LocalLevelManager::getMainLevelString function.

Okay, so now how do we replace the levels with our own ones? #

It's really simple! Geode allows you to hook (modify) classes and functions. If we hook these two classes, we can change how vanilla GD gets its levels!

But, before we do this, we need to find a way to get our levels in the game in the first place.

Getting level strings #

Using Rod's Fangame Tools mod from Geode, we can extract any level string straight from the level editor.

  1. 1

    Go to the mod's settings and ensure that the "Level Copy Type" setting is set to Encoded, and that the "Copy Level String" setting is enabled.

  2. 2

    Open your desired level. You should see a page that looks as shown below:

    A page showing the level's info (with editor/play/share buttons in the middle) and an editable level name/description
  3. 3

    Click the button to the right of your screen. It should say "Copy Data". If a notification saying, "Encoded level copied to clipboard successfully!" appeared for you, this should've worked successfully!

Adding level data to resources #

Geode allows you to easily add resources (files along with your base mod) to your mod. To do so, add a new resources key to your mod.json as shown below:

json
1{
2 ...
3 "resources": {
4 "files": [
5 "res/levels/*.txt"
6 ]
7 }
8}

If you have another name for your resources folder, feel free to use that instead - you are not limited to the res/levels path.

Next, create a text file for each level inside your levels folder and paste the encoded level data into it.

For example:

  • res/levels/level1.txt
  • res/levels/level2.txt
  • res/levels/level3.txt

The number in the file name corresponds directly to the level ID used in GD (e.g. level1.txt -> Level 1). If negative level IDs are used (for custom or extended implementations), you can name files using the same pattern, such as level-1.txt.

Registering the custom levels in GD #

Now that we have the custom level(s) in the mod's resources folder, we can start writing code to register the levels in GD. To do this, we will first hook LocalLevelManager to instead get the main level strings from our mod's resources:

cpp
1#include <Geode/Geode.hpp>
2#include <Geode/modify/LocalLevelManager.hpp>
3
4using namespace geode::prelude;
5
6class $modify(MyLocalLevelManager, LocalLevelManager) {
7 gd::string getMainLevelString(int id) {
8 auto file = CCString::createWithFormat("level%i.txt"_spr, id);
9 if(file == nullptr) return "";
10 auto content = CCString::createWithContentsOfFile(file->getCString());
11 if(content == nullptr) return "";
12
13 return gd::string(content->getCString());
14 }
15};

Although we have overwritten the level string, we must also limit the number of levels being displayed in the main levels page and display custom metadata for the levels. To do this, we will also hook LevelTools:

cpp
1#include <Geode/Geode.hpp>
2#include <Geode/modify/LevelTools.hpp>
3
4using namespace geode::prelude;
5
6class $modify(MyLevelTools, LevelTools) {
7 GJGameLevel* getLevel(int levelID, bool loaded) {
8 GJGameLevel* level = GJGameLevel::create();
9 level->m_levelName = getAudioTitle(levelID - 1);
10 level->m_audioTrack = levelID - 1;
11
12 switch(levelID) {
13 case 1:
14 setLevelInfo(level, 4, GJDifficulty::Hard, 0);
15 level->m_coins = 3;
16 break;
17 case 2:
18 setLevelInfo(level, 6, GJDifficulty::Harder, 0);
19 level->m_coins = 3;
20 break;
21 }
22 if (!loaded) {
23 level->m_levelString = LocalLevelManager::sharedState()->getMainLevelString(levelID);
24 }
25 level->m_levelID = levelID;
26 level->m_levelType = GJLevelType::Saved;
27 return level;
28 }
29};

Adding custom artists (annoying but required) #

In your fangame, you will likely have completely different artists for your levels. With this, you will also need to find their socials (music video, Newgrounds, YouTube, and Facebook). But don't worry if you can't find any specific one, as you can just set the URL to empty ("") and GD will not render that specific button.

cpp
1// Still in the same LevelTools hook
2$modify(MyLevelTools, LevelTools) {
3 // Where you set your song names
4 gd::string getAudioTitle(int audioID) {
5 switch(audioID) {
6 case -1: return "Stay Inside Me";
7 case 0: return "Badland";
8 case 1: return "Unstable Geometry";
9 default: return "";
10 }
11 }
12
13 // Where you set WHERE the song files are
14 gd::string getAudioFileName(int audioID) {
15 auto saveDir = Mod::get()->getSaveDir();
16 switch(audioID) {
17 case -1: return "StayInsideMe.mp3";
18 case 0: return (const char*)((saveDir / "songs" / "Badland.mp3").u8string().c_str());
19 case 1: return (const char*)((saveDir / "songs" / "UnstableGeometry.mp3").u8string().c_str());
20 default: return (const char*)((saveDir / "songs" / fmt::format("{}.ogg", audioID)).u8string().c_str());;
21 }
22 }
23
24 // Where you set the artists list
25 gd::string nameForArtist(int artistID) {
26 switch(artistID) {
27 case 1: return "Boom Kitty";
28 case 2: return "Waterflame";
29 default: return "";
30 }
31 }
32
33 // Where you assign each song an artist
34 int artistForAudio(int audioID) {
35 switch(audioID) {
36 case -1: return 4;
37 case 0: return 1;
38 case 1: return 2;
39 default: return 0;
40 }
41 }
42};

Adding the songs #

You may add the songs in your mod by updating the mod.json:

json
1{
2 ...
3 "resources": {
4 "files": [
5 "res/levels/*.txt",
6 "res/music.zip"
7 ]
8 }
9}

Inside the music.zip should be every song in your mod (if you want to learn how to zip files, google it 😊).

Now, you may be asking: "Why the HELL do we need to put a bunch of songs in a zip file??". And that's an excellent question! The reason why we do this is to save mod filesize since the Geode index has pretty small filesize limits (250Mb). Although 250Mb might not seem like a large amount, if you have enough songs and levels in your mod, you will easily exceed this limit.

You can also try the online song download method but a full explanation for this in this tutorial would take a while to write. 😟

Unzipping the songs #

To unzip the songs, write the following code (straight from GD Surge):

cpp
1#include <Geode/Geode.hpp>
2#include <Geode/utils/file.hpp>
3
4using namespace geode::prelude;
5
6$on_mod(Loaded) {
7 auto musicPath = Mod::get()->getResourcesDir() / "music.zip";
8 auto unzip = geode::utils::file::Unzip::create(musicPath);
9 if (unzip.isErr()) {
10 FLAlertLayer::create(
11 "Error",
12 "Failed to unzip music files.",
13 "OK"
14 )->show();
15 return;
16 }
17
18 auto& archive = unzip.unwrap();
19 auto success = archive.extractAllTo(Mod::get()->getSaveDir());
20 if (!success.isOk()) {
21 FLAlertLayer::create(
22 "Error",
23 "Failed to unzip music files.",
24 "OK"
25 )->show();
26 return;
27 }
28}

Making levels playable #

If you attempt to open a level in your mod's current state, it may say "Failed to load" when you open the level. To fix this, you need to hook LevelTools again and ensure each time the game checks the level's integrity, it returns true:

cpp
1class $modify(MyLevelTools, LevelTools) {
2 bool verifyLevelIntegrity(gd::string verifyString, int levelID) {
3 return true;
4 }
5};

Now, you should have a fully working custom level select screen (with fully custom levels) in GD. If you still need help, feel free to ask a question on GitHub!