diff --git a/hacks/level.json b/hacks/level.json index 2f1dd42..8d0c8a3 100644 --- a/hacks/level.json +++ b/hacks/level.json @@ -189,6 +189,10 @@ } ] }, + { + "type": "embedded", + "hack": "startpos_switch" + }, { "type": "embedded", "hack": "auto_pickup_coins" diff --git a/src/bindings/GameObject.h b/src/bindings/GameObject.h index e8008a3..521df0b 100644 --- a/src/bindings/GameObject.h +++ b/src/bindings/GameObject.h @@ -3,13 +3,229 @@ namespace robtop { - class GameObject + class CCSpritePlus : public cocos2d::CCSprite + { + uint8_t pad_1[0x20]; + + public: + cocos2d::CCArray *m_followers; + CCSpritePlus *m_followingSprite; + bool m_hasFollower; + bool m_propagateScaleChanges; + bool m_propagateFlipChanges; + }; + + class GameObject : public CCSpritePlus { public: inline uint32_t get_id() { - uint32_t id = ((uint32_t*)this)[0xE1]; - return id; + return m_objectID; + } + inline cocos2d::CCPoint get_position() + { + return m_startPosition; + } + inline float get_x() + { + return m_startPosition.x; + } + inline float get_y() + { + return m_startPosition.y; } + + uint8_t pad_1[0xf]; + bool m_hasExtendedCollision; + uint8_t pad_2[0x13]; + cocos2d::CCSprite *m_baseSprite; + cocos2d::CCSprite *m_detailSprite; + uint8_t pad_3[0x64]; + std::string m_particleString; + uint8_t pad_4[0x1]; + bool m_particleUseObjectColor; + uint8_t pad_5[0x32]; + int m_linkedGroup; + uint8_t pad_6[0x23]; + int m_uniqueID; + uint32_t m_objectType; + uint8_t pad_7[0x14]; + double m_realXPosition; + double m_realYPosition; + cocos2d::CCPoint m_startPosition; + uint8_t pad_8[0x1]; + bool m_hasNoAudioScale; + uint8_t pad_9[0x2a]; + short m_enterChannel; + short m_objectMaterial; + uint8_t pad_10[0x4]; + bool m_hasNoGlow; + int m_targetColor; + int m_objectID; + short m_customColorType; + bool m_isDontEnter; + bool m_isDontFade; + bool m_hasNoEffects; + bool m_hasNoParticles; + uint8_t pad_11[0x16]; + int m_property53 = sizeof(std::string); + uint8_t pad_12[0x18]; + void *m_baseColor; + void *m_detailColor; + int m_zLayer; + int m_zOrder; + uint8_t pad_13[0x10]; + bool m_shouldUpdateColorSprite; + uint8_t pad_14[0x1]; + bool m_hasGroupParent; + bool m_hasAreaParent; + float m_scaleX; + float m_scaleY; + std::array *m_groups; + short m_groupCount; + bool m_hasGroupParentsString; + uint8_t pad_15[0xf]; + short m_editorLayer; + short m_editorLayer2; + uint8_t pad_16[0x8]; + bool m_isNoTouch; + uint8_t pad_17[0x2c]; + bool m_isHighDetail; + uint8_t pad_18[0x11]; + bool m_isPassable; + bool m_isHide; + bool m_isNonStickX; + bool m_isNonStickY; + bool m_isIceBlock; + bool m_isGripSlope; + bool m_isScaleStick; + bool m_isExtraSticky; + bool m_isDontBoostY; + bool m_isDontBoostX; + uint8_t pad_19[0x11]; + int m_property155; + int m_property156; + uint8_t pad_20[0x26]; + }; + + class EnhancedGameObject : public GameObject + { + bool m_hasCustomAnimation; + bool m_hasCustomRotation; + bool m_disableRotation; + float m_rotationSpeed; + bool m_animationRandomizedStart; + float m_animationSpeed; + bool m_animationShouldUseSpeed; + bool m_animateOnTrigger; + bool m_disableDelayedLoop; + bool m_disableAnimShine; + int m_singleFrame; + bool m_animationOffset; + bool m_animateOnlyWhenActive; + bool m_isNoMultiActivate; + bool m_isMultiActivate; + }; + + class EffectGameObject : public EnhancedGameObject + { + cocos2d::ccColor3B m_triggerTargetColor; + float m_duration; + float m_opacity; + int m_targetGroupID; + int m_centerGroupID; + bool m_isTouchTriggered; + bool m_isSpawnTriggered; + bool m_hasCenterEffect; + float m_shakeStrength; + float m_shakeInterval; + bool m_tintGround; + bool m_usesPlayerColor1; + bool m_usesPlayerColor2; + bool m_usesBlending; + float m_moveOffsetX; + float m_moveOffsetY; + int m_easingType; + float m_easingRate; + bool m_lockToPlayerX; + bool m_lockToPlayerY; + bool m_lockToCameraX; + bool m_lockToCameraY; + bool m_useMoveTarget; + int m_moveTargetMode; + float m_moveModX; + float m_moveModY; + bool m_property393; + bool m_isDirectionFollowSnap360; + int m_targetModCenterID; + float m_directionModeDistance; + bool m_isDynamicMode; + bool m_isSilent; + float m_rotationDegrees; + int m_times360; + bool m_lockObjectRotation; + int m_rotationTargetID; + float m_rotationOffset; + int m_dynamicModeEasing; + float m_followXMod; + float m_followYMod; + float m_followYSpeed; + float m_followYDelay; + int m_followYOffset; + float m_followYMaxSpeed; + float m_fadeInDuration; + float m_holdDuration; + float m_fadeOutDuration; + int m_pulseMode; + int m_pulseTargetType; + cocos2d::ccHSVValue m_hsvValue; + int m_copyColorID; + bool m_copyOpacity; + bool m_pulseMainOnly; + bool m_pulseDetailOnly; + bool m_pulseExclusive; + bool m_property210; + bool m_activateGroup; + bool m_touchHoldMode; + int m_touchToggleMode; + int m_touchPlayerMode; + bool m_isDualMode; + int m_animationID; + bool m_isMultiActivate; + bool m_triggerOnExit; + int m_itemID2; + int m_property534; + int m_itemID; + bool m_targetPlayer1; + bool m_targetPlayer2; + bool m_followCPP; + bool m_subtractCount; + bool m_collectibleIsPickupItem; + bool m_collectibleIsToggleTrigger; + int m_collectibleParticleID; + int m_collectiblePoints; + bool m_hasNoAnimation; + float m_gravityValue; + bool m_isSinglePTouch; + float m_zoomValue; + bool m_cameraIsFreeMode; + bool m_cameraEditCameraSettings; + float m_cameraEasingValue; + float m_cameraPaddingValue; + bool m_cameraDisableGridSnap; + bool m_property118; + float m_timeWarpTimeMod; + bool m_showGamemodeBorders; + int m_ordValue; + int m_channelValue; + bool m_isReverse; + int m_secretCoinID; + bool m_ignoreGroupParent; + bool m_ignoreLinkedObjects; + }; + + class StartPosObject : public EffectGameObject + { + int m_unknown; }; } \ No newline at end of file diff --git a/src/bindings/PlayLayer.h b/src/bindings/PlayLayer.h index d13d9d4..e12b13f 100644 --- a/src/bindings/PlayLayer.h +++ b/src/bindings/PlayLayer.h @@ -41,6 +41,9 @@ namespace robtop inline const char *PlayLayer_startPosCheckpoint_pat = "74208B8B^??0000"; inline uintptr_t PlayLayer_startPosCheckpoint_offset; + inline const char *PlayLayer_practiceMode_pat = "E9????8A81^??000084C0"; + inline uintptr_t PlayLayer_practiceMode_offset; + class PlayLayer : public cocos2d::CCLayer { public: @@ -87,5 +90,13 @@ namespace robtop uintptr_t addr = (uintptr_t)this + PlayLayer_startPosCheckpoint_offset; *(uintptr_t *)addr = checkpoint; } + inline bool isPracticeMode() + { + if (!PlayLayer_practiceMode_offset) + return false; + + uintptr_t addr = (uintptr_t)this + PlayLayer_practiceMode_offset; + return *(bool *)addr; + } }; } \ No newline at end of file diff --git a/src/bindings/bindings.h b/src/bindings/bindings.h index 7772782..33737a4 100644 --- a/src/bindings/bindings.h +++ b/src/bindings/bindings.h @@ -23,7 +23,7 @@ namespace robtop // used to find the offset of a member variable from code inline void init_member_offset(const char *name, uintptr_t *ptr, const char *pat, size_t size) { - uintptr_t addr = patterns::find_pattern(pat); + uintptr_t addr = patterns::find_pattern(pat); if (!addr) { L_ERROR("Failed to find {}", name); @@ -42,7 +42,7 @@ namespace robtop { result |= bytes[i] << (i * 8); } - + *ptr = result; L_TRACE("Member offset for {} is 0x{:x}", name, *ptr); @@ -71,6 +71,7 @@ namespace robtop init_binding("PlayLayer::removeAllCheckpoints", (void **)&robtop::PlayLayer_removeAllCheckpoints, robtop::PlayLayer_removeAllCheckpoints_pat); init_binding("PlayLayer::~PlayLayer", (void **)&robtop::PlayLayer_destructor, robtop::PlayLayer_destructor_pat); init_member_offset("PlayLayer::startPosCheckpoint", &robtop::PlayLayer_startPosCheckpoint_offset, robtop::PlayLayer_startPosCheckpoint_pat, 4); + init_member_offset("PlayLayer::practiceMode", &robtop::PlayLayer_practiceMode_offset, robtop::PlayLayer_practiceMode_pat, 4); // LevelEditorLayer init_binding("LevelEditorLayer::init", (void **)&robtop::LevelEditorLayer_init, robtop::LevelEditorLayer_init_pat); diff --git a/src/hacks/hacks.cpp b/src/hacks/hacks.cpp index f9d95ff..a49b8d1 100644 --- a/src/hacks/hacks.cpp +++ b/src/hacks/hacks.cpp @@ -7,6 +7,7 @@ #include "discord_rpc.h" #include "display.h" #include "auto_safemode.h" +#include "startpos_switch.h" #include "pickup_coins.h" namespace hacks @@ -351,6 +352,7 @@ namespace hacks hacks.push_back(new DiscordRPC()); hacks.push_back(new DisplayHack()); hacks.push_back(new AutoSafeMode()); + hacks.push_back(new StartposSwitcher()); hacks.push_back(new PickupCoins()); for (auto &hack : hacks) diff --git a/src/hacks/startpos_switch.cpp b/src/hacks/startpos_switch.cpp new file mode 100644 index 0000000..c33f590 --- /dev/null +++ b/src/hacks/startpos_switch.cpp @@ -0,0 +1,172 @@ +#include "startpos_switch.h" +#include "../menu/gui.h" +#include "../menu/keybinds.h" + +namespace hacks +{ + StartposSwitcher *StartposSwitcher::instance = nullptr; + + StartposSwitcher::StartposSwitcher() + { + instance = this; + } + void StartposSwitcher::init() {} + void StartposSwitcher::late_init() {} + + void StartposSwitcher::draw(bool embedded) + { + if (!embedded) + return; + + gui::ImToggleButton("Startpos Switcher", &m_enabled, + [&]() + { + gui::ImKeybind("Previous Key", &m_prev_keybind, WINDOW_WIDTH, 15, false); + gui::ImKeybind("Next Key", &m_next_keybind, WINDOW_WIDTH, 15, false); + gui::ImToggleButton("Show Label", &m_show_label, nullptr, WINDOW_WIDTH); + }); + } + + void StartposSwitcher::choose_start_pos(int32_t index) + { + if (!m_play_layer || m_startpos_objects.empty()) + return; + + size_t startpos_count = m_startpos_objects.size(); + + if (index >= (int32_t)startpos_count) + index = -1; // -1 means no startpos + else if (index < -1) + index = startpos_count - 1; + + m_current_index = index; + + m_play_layer->set_startPosCheckpoint(0); + + if (m_current_index >= 0) + m_play_layer->setStartPosObject(m_startpos_objects[m_current_index]); + else + m_play_layer->setStartPosObject(nullptr); + + if (m_play_layer->isPracticeMode()) + m_play_layer->removeAllCheckpoints(); + + m_play_layer->resetLevel(); + m_play_layer->startMusic(); + + // update label + // std::string text = fmt::format("{} / {}", m_current_index + 1, startpos_count); + // m_label->setString(text.c_str()); + } + + void StartposSwitcher::update() + { + if (!instance->m_enabled) + return; + + if (utils::is_key_pressed(m_prev_keybind)) + choose_start_pos(m_current_index - 1); + else if (utils::is_key_pressed(m_next_keybind)) + choose_start_pos(m_current_index + 1); + + // Temporary, until CCLabelBMFont is fixed + // use ImGui to draw the label instead + if (m_startpos_objects.empty() || !m_show_label) + return; + + size_t startpos_count = m_startpos_objects.size(); + auto screen_size = ImGui::GetIO().DisplaySize; + std::string text = fmt::format("{} / {}", m_current_index + 1, startpos_count); + auto text_size = ImGui::CalcTextSize(text.c_str()); + auto pos = ImVec2(screen_size.x / 2 - text_size.x / 2, screen_size.y - text_size.y - 10); + ImGui::GetForegroundDrawList()->AddText( + globals::title_font, 20, pos, IM_COL32(255, 255, 255, 255), text.c_str()); + } + + void StartposSwitcher::load(nlohmann::json *data) + { + m_enabled = data->value("startpos_switch.enabled", false); + m_prev_keybind = data->value("startpos_switch.prev_keybind", 0); + m_next_keybind = data->value("startpos_switch.next_keybind", 0); + m_show_label = data->value("startpos_switch.show_label", true); + } + + void StartposSwitcher::save(nlohmann::json *data) + { + data->emplace("startpos_switch.enabled", m_enabled); + data->emplace("startpos_switch.prev_keybind", m_prev_keybind); + data->emplace("startpos_switch.next_keybind", m_next_keybind); + data->emplace("startpos_switch.show_label", m_show_label); + } + + bool StartposSwitcher::load_keybind(keybinds::Keybind *keybind) + { + if (keybind->id == "startpos_switch") + { + keybind->callback = [&]() + { + m_enabled = !m_enabled; + }; + return true; + } + return false; + } + + void StartposSwitcher::playLayer_init(robtop::PlayLayer *self, robtop::GJGameLevel *level) + { + if (!instance || !instance->m_enabled) + return; + + instance->m_startpos_objects.clear(); + instance->m_play_layer = self; + } + + void StartposSwitcher::playLayer_lateInit(robtop::PlayLayer *self) + { + if (!instance || !instance->m_enabled) + return; + + // sort startpos objects by x position + std::sort( + instance->m_startpos_objects.begin(), + instance->m_startpos_objects.end(), + [](robtop::GameObject *a, robtop::GameObject *b) + { + return a->get_x() < b->get_x(); + }); + + size_t startpos_count = instance->m_startpos_objects.size(); + if (startpos_count > 0) + instance->m_current_index = startpos_count - 1; + + // create label + // std::string text = fmt::format("{} / {}", instance->m_current_index + 1, startpos_count); + // instance->m_label = cocos2d::CCLabelBMFont::create(text.c_str(), "bigFont.fnt"); + // instance->m_label->setAnchorPoint({0.5f, 0.95f}); + // instance->m_label->setZOrder(1000); + // instance->m_label->setScale(0.5f); + // instance->m_label->setOpacity(180); + // instance->m_play_layer->addChild(instance->m_label); + } + + void StartposSwitcher::playLayer_destructor(robtop::PlayLayer *self) + { + if (!instance) + return; + + instance->m_play_layer = nullptr; + instance->m_startpos_objects.clear(); + } + + void StartposSwitcher::playLayer_addObject(robtop::PlayLayer *self, robtop::GameObject *object) + { + if (!instance || !instance->m_enabled) + return; + + if (object->get_id() == 31) + { + instance->m_startpos_objects.push_back(object); + robtop::StartPosObject *startpos = (robtop::StartPosObject *)object; + } + } +} \ No newline at end of file diff --git a/src/hacks/startpos_switch.h b/src/hacks/startpos_switch.h new file mode 100644 index 0000000..9e9843e --- /dev/null +++ b/src/hacks/startpos_switch.h @@ -0,0 +1,46 @@ +#pragma once +#include "../pch.h" +#include "hacks.h" + +#include "../bindings/PlayLayer.h" + +namespace hacks +{ + class StartposSwitcher : public Hack + { + public: + StartposSwitcher(); + virtual void init() override; + virtual void late_init() override; + virtual void draw(bool embedded = false) override; + virtual void update() override; + virtual void load(nlohmann::json *data) override; + virtual void save(nlohmann::json *data) override; + virtual std::string get_id() override { return "startpos_switch"; } + virtual bool load_keybind(keybinds::Keybind *keybind) override; + + void choose_start_pos(int32_t index); + + // hooks: + static void playLayer_init(robtop::PlayLayer *self, robtop::GJGameLevel *level); + static void playLayer_lateInit(robtop::PlayLayer *self); + static void playLayer_destructor(robtop::PlayLayer *self); + static void playLayer_addObject(robtop::PlayLayer *self, robtop::GameObject *object); + + private: + static StartposSwitcher *instance; + + robtop::PlayLayer *m_play_layer = nullptr; + std::vector m_startpos_objects; + + cocos2d::CCLabelBMFont *m_label = nullptr; + + bool m_enabled = false; + bool m_show_label = true; + + uint32_t m_prev_keybind = 0; + uint32_t m_next_keybind = 0; + + int32_t m_current_index = 0; + }; +} \ No newline at end of file diff --git a/src/hooks/PlayLayer.cpp b/src/hooks/PlayLayer.cpp index ba43798..c797e27 100644 --- a/src/hooks/PlayLayer.cpp +++ b/src/hooks/PlayLayer.cpp @@ -2,6 +2,7 @@ #include "PlayLayer.h" #include "../hacks/discord_rpc.h" +#include "../hacks/startpos_switch.h" #include "../hacks/pickup_coins.h" namespace hooks::PlayLayer @@ -10,10 +11,12 @@ namespace hooks::PlayLayer bool __fastcall init_hook(robtop::PlayLayer *self, int edx, robtop::GJGameLevel *level, bool v1, bool v2) { hacks::PickupCoins::playLayer_init(self, level); + hacks::StartposSwitcher::playLayer_init(self, level); const bool ret = PlayLayer_init(self, level, v1, v2); hacks::DiscordRPC::change_state(hacks::DiscordRPC::State::GAME, level); + hacks::StartposSwitcher::playLayer_lateInit(self); return ret; } @@ -42,12 +45,15 @@ namespace hooks::PlayLayer { PlayLayer_addObject(self, object); + hacks::StartposSwitcher::playLayer_addObject(self, object); hacks::PickupCoins::playLayer_addObject(self, object); } void(__thiscall *PlayLayer_destructor)(robtop::PlayLayer *); void __fastcall destructor_hook(robtop::PlayLayer *self) { + hacks::StartposSwitcher::playLayer_destructor(self); + PlayLayer_destructor(self); }