Hi! I’m peony, the developer of Silicate — one of the most advanced bots for Geometry Dash. One of my first challenges to figure out was how to unlock RobTop’s 240tps cap — and I found interesting information along the way.
Let’s start off with how the game’s physics update function even works. When digging into the decompiled C code (I’m using IDA Pro) you may notice the following line (I’ve cleaned it up a bit for readability sake):
dt = GJBaseGameLayer::getModifiedDelta(this, dt);
Okay! This is a lead — this function is inlined on MacOS (so you may not have the same experience when decompiling on that platform) but it’s there on Android and Windows. A naive approach would be to hook this function and override the delta returned:
float getModifiedDelta(float dt) {
GJBaseGameLayer::getModifiedDelta(dt);
auto& updater = Bot::get()->m_updater;
float wantedDt =
updater.getPhysicsDt() * fminf(m_gameState.m_timeWarp, 1.0f);
return wantedDt;
}
This is a direct copy from the Silicate source code. I’m calling the original function due to it changing some internal things in the GJBaseGameLayer
. But — this is roughly everything you need… right?
This is what zBot used to do — and what Mega Hack still does! — and it doesn’t work. Only hooking getModifiedDelta
won’t get us anywhere — in fact, time warp physics change when doing that.
So, how do we fix this? Well, an easy solution would also be feeding your wanted dt into the original function — like this:
void update(float dt) {
auto& updater = Bot::get()->m_updater;
// function simplified for brevity!
// also, feeding into update is more consistent as the original dt
// may be used in one specific circumstance
GJBaseGameLayer::update(updater.getPhysicsDt());
}
This works great and fixes most of the underlying issues — 240tps works great, and higher tpses work great too! Time warp works correctly too.
So what about lower tpses? This level counts TPS and displays it as a label: 63486708
. Let’s set our TPS to 60 and enter the level:
Huh. What? We just hooked the getModifiedDelta function and made sure everything works! There has to be something else at play. Let’s look into GJBaseGameLayer::update
again:
fractionOf240 = (int)fmaxf(1.0, roundf((float)(dt * 60.0 / fminf(m_gameState.m_timeWarp, 1.0)) * 4.0));
That’s a mouthful but it certainly leads us closer to the solution. This variable stores information how much of a fraction the current dt is to 1/240 — for 60tps this is 4. The fractionOf240
variable is being put into r11, so let’s hook the instruction right after the final move and edit the step count to always be what we want, which is one.
// i'm using safetyhook for all my midhooks
static void physStepCountMidhookStep1(SafetyHookContext& ctx) {
auto bot = Bot::get();
if (!PlayLayer::get()) return;
ctx.r11 = 1;
}
Great! This fixes the issues with low TPS, and now we just need to restore the original dt at the very end of the function for visibility updates to work:
static void physStepCountMidhookStep2(SafetyHookContext& ctx) {
auto bot = Bot::get();
if (!PlayLayer::get()) return;
ctx.xmm15.f64[0] =
bot->m_updater.getPhysicsDt() * bot->m_updater.estimatedStepCount;
}
Let’s enter the TPS test level again and set our TPS to 60:
This is great! We’ve successfully fixed the TPS bypass and it works well on all TPS values now. Unfortunately, this approach isn’t the most obvious one — and so only two 2.2 bots — Silicate and TCBot — have solved this problem correctly. This is a widespread issue and should be solved — I’ve provided the necessary information to fix the issue yourself. If you have any questions about my fix, feel free to reach out to me on Discord at peony
.
Thank you for reading!
An additional thank you goes out to the developer of TCBot — chagh.dev on Discord — who helped discover the issue with lower TPSes.