├── support.md ├── logo.png ├── pathfinder.png ├── gd-sim ├── Info.md ├── include │ ├── Pad.hpp │ ├── Hazard.hpp │ ├── Orb.hpp │ ├── Block.hpp │ ├── EffectObject.hpp │ ├── Vehicle.hpp │ ├── Portals.hpp │ ├── Slope.hpp │ ├── Level.hpp │ ├── Object.hpp │ ├── Player.hpp │ └── util.hpp ├── src │ ├── Objects │ │ ├── SizePortal.cpp │ │ ├── GravityPortal.cpp │ │ ├── BreakableBlock.cpp │ │ ├── SpeedPortal.cpp │ │ ├── Hazard.cpp │ │ ├── VehiclePortal.cpp │ │ ├── Pad.cpp │ │ ├── Orb.cpp │ │ ├── Block.cpp │ │ ├── Object.cpp │ │ └── Slope.cpp │ ├── EffectObject.cpp │ ├── Player.cpp │ ├── Level.cpp │ ├── util.cpp │ └── Vehicle.cpp ├── CMakeLists.txt └── test │ └── test.cpp ├── src ├── pathfinder.hpp ├── checker.cpp ├── pathfinder.cpp ├── main.cpp ├── debug.cpp └── subprocess.hpp ├── README.md ├── mod.json ├── about.md ├── .gitignore ├── .github └── workflows │ ├── multi-platform.yml │ └── multi-platform-debug.yml ├── CMakeLists.txt └── changelog.md /support.md: -------------------------------------------------------------------------------- 1 | Buy iCreate Pro to support me: https://icreate.pro/ -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camila314/pathfinder/HEAD/logo.png -------------------------------------------------------------------------------- /pathfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camila314/pathfinder/HEAD/pathfinder.png -------------------------------------------------------------------------------- /gd-sim/Info.md: -------------------------------------------------------------------------------- 1 | # GD-Sim 2 | 3 | This is a physics simulator for Geometry Dash, optimized for speed. View `Level.hpp` to start your journey of understanding how it works. -------------------------------------------------------------------------------- /src/pathfinder.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::vector pathfind(std::string const& lvlString, std::atomic_bool& stop, std::function callback); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pathfinder 2 | # Geometry Dash Physics Simulator 3 | 4 | Full support up to 1.7, partial up to 1.9. This code is not currently licensed for redistribution. 5 | 6 | ## Notes 7 | 8 | 1. Y positions are slightly off because in the real gd, they are offest by 105 -------------------------------------------------------------------------------- /gd-sim/include/Pad.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | enum class PadType { 5 | Yellow, 6 | Blue, 7 | Pink 8 | }; 9 | 10 | struct Pad : public EffectObject { 11 | PadType type; 12 | 13 | Pad(Vec2D size, std::unordered_map&& fields); 14 | void collide(Player&) const override; 15 | }; -------------------------------------------------------------------------------- /gd-sim/include/Hazard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct Hazard : public Object { 5 | Hazard(Vec2D size, std::unordered_map&& fields); 6 | void collide(Player&) const override; 7 | }; 8 | 9 | struct Sawblade : public Hazard { 10 | using Hazard::Hazard; 11 | bool touching(Player const&) const override; 12 | }; 13 | -------------------------------------------------------------------------------- /gd-sim/include/Orb.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | enum class OrbType { 5 | Yellow, 6 | Blue, 7 | Pink, 8 | }; 9 | 10 | struct Orb : public EffectObject { 11 | OrbType type; 12 | 13 | Orb(Vec2D size, std::unordered_map&& fields); 14 | bool touching(Player const&) const override; 15 | void collide(Player&) const override; 16 | }; -------------------------------------------------------------------------------- /gd-sim/src/Objects/SizePortal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | SizePortal::SizePortal(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)), small(std::stoi(fields[1]) == 101) {} 5 | 6 | /// Easiest portal! 7 | void SizePortal::collide(Player& p) const { 8 | EffectObject::collide(p); 9 | 10 | p.small = small; 11 | } 12 | -------------------------------------------------------------------------------- /gd-sim/include/Block.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct Block : public Object { 5 | Block(Vec2D size, std::unordered_map&& fields); 6 | void collide(Player&) const override; 7 | }; 8 | 9 | struct BreakableBlock : public Block { 10 | using Block::Block; 11 | 12 | void collide(Player&) const override; 13 | bool touching(Player const& p) const override; 14 | }; 15 | -------------------------------------------------------------------------------- /gd-sim/include/EffectObject.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /** 5 | * Anything that subclasses EffectObject will be single-activate 6 | * unless otherwise specified. This matches how GD handles orbs, 7 | * pads, and portals. 8 | */ 9 | struct EffectObject : public Object { 10 | using Object::Object; 11 | bool touching(Player const&) const override; 12 | void collide(Player&) const override; 13 | }; 14 | -------------------------------------------------------------------------------- /gd-sim/src/EffectObject.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /// usedEffects is used to store information abuout objects that have already been collided with 5 | 6 | bool EffectObject::touching(Player const& player) const { 7 | if (Object::touching(player)) { 8 | return !player.usedEffects.contains(id); 9 | } 10 | 11 | return false; 12 | } 13 | 14 | void EffectObject::collide(Player& player) const { 15 | player.usedEffects.insert(id); 16 | } -------------------------------------------------------------------------------- /gd-sim/src/Objects/GravityPortal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | GravityPortal::GravityPortal(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)), upsideDown(std::stoi(fields[1]) == 11) {} 5 | 6 | 7 | void GravityPortal::collide(Player& p) const { 8 | EffectObject::collide(p); 9 | 10 | if (upsideDown != p.upsideDown) { 11 | // Gravity portals halve the velocity 12 | p.velocity = -p.velocity / 2; 13 | p.upsideDown = upsideDown; 14 | p.gravityPortal = true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/BreakableBlock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | bool BreakableBlock::touching(Player const& p) const { 5 | if (Block::touching(p)) { 6 | return !p.usedEffects.contains(id); 7 | } 8 | 9 | return false; 10 | } 11 | 12 | void BreakableBlock::collide(Player& p) const { 13 | // To handle snapping accordingly, hand it off to Block 14 | Block::collide(p); 15 | 16 | if (p.dead) 17 | p.usedEffects.insert(id); 18 | 19 | // Reset fields that block sets 20 | p.dead = false; 21 | if (p.grounded) 22 | p.snapData.playerFrame = 0; 23 | } 24 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/SpeedPortal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | SpeedPortal::SpeedPortal(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)) { 5 | switch (atoi(fields[1].c_str())) { 6 | case 200: 7 | speed = 0; 8 | break; 9 | case 201: 10 | speed = 1; 11 | break; 12 | case 202: 13 | speed = 2; 14 | break; 15 | case 203: 16 | speed = 3; 17 | break; 18 | case 1334: 19 | speed = 4; 20 | break; 21 | } 22 | } 23 | 24 | 25 | void SpeedPortal::collide(Player& p) const { 26 | EffectObject::collide(p); 27 | 28 | p.speed = speed; 29 | } 30 | -------------------------------------------------------------------------------- /mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "camila314.pathfinder", 3 | "gd": { 4 | "win": "2.2074", 5 | "android": "2.2074", 6 | "mac": "2.2074", 7 | "ios": "2.2074" 8 | }, 9 | "geode": "4.8.0", 10 | "name": "Pathfinder", 11 | "developer": "camila314", 12 | "version": "v1.0.0-beta.230", 13 | "dependencies": [ 14 | { 15 | "id": "geode.node-ids", 16 | "version": ">=v1.18.0", 17 | "importance": "required" 18 | } 19 | ], 20 | "resources": { 21 | "sprites": [ 22 | "pathfinder.png" 23 | ] 24 | }, 25 | "description": "Beat levels automatically!", 26 | "tags": ["offline", "gameplay", "utility"], 27 | "links": { 28 | "community": "https://discord.com/invite/u9m7kqyqxu", 29 | "source": "https://github.com/camila314/pathfinder" 30 | } 31 | } -------------------------------------------------------------------------------- /gd-sim/include/Vehicle.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | enum class VehicleType { 6 | Cube, 7 | Ship, 8 | Ball, 9 | Ufo, 10 | Wave 11 | }; 12 | 13 | struct Player; 14 | struct Object; 15 | 16 | /// NOT the same as vehicle portal. This class exists to hold vehicle-specific logic. 17 | struct Vehicle { 18 | VehicleType type; 19 | 20 | /// When the vehicle is changed into this one. 21 | std::function enter; 22 | 23 | /// Ran after everything else, used mainly for vehicles that have ceilings 24 | std::function clamp; 25 | 26 | /// Vehicle-specific movement, done after collisions 27 | std::function update; 28 | 29 | /// How far away the floor and ceiling are from each other, relative to portal 30 | float bounds; 31 | 32 | static Vehicle from(VehicleType v); 33 | }; 34 | -------------------------------------------------------------------------------- /gd-sim/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24.0 FATAL_ERROR) 2 | set(CMAKE_CXX_STANDARD 20) 3 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 4 | 5 | project(gd-sim VERSION 0.1.0 LANGUAGES CXX) 6 | 7 | file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.cpp src/**/*.cpp) 8 | add_library(gd-sim STATIC ${SOURCES}) 9 | 10 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) 11 | target_compile_options(gd-sim PRIVATE $<$:-g> -O3 -fno-math-errno -fno-trapping-math -fomit-frame-pointer $<$:-fno-rtti -fvisibility=hidden>) 12 | 13 | 14 | target_include_directories(gd-sim PUBLIC include) 15 | 16 | target_compile_definitions(gd-sim PRIVATE -D_HAS_ITERATOR_DEBUGGING=0) 17 | 18 | add_executable(gd-sim-test ${SOURCES} test/test.cpp) 19 | 20 | set_target_properties(gd-sim-test PROPERTIES INTERPROCEDURAL_OPTIMIZATION 0) 21 | target_include_directories(gd-sim-test PRIVATE include) -------------------------------------------------------------------------------- /src/checker.cpp: -------------------------------------------------------------------------------- 1 | /*#include 2 | #include 3 | #include 4 | 5 | bool calcFlip(bool p) { 6 | return GameManager::sharedState()->getGameVariable("0010") ? !p : p; 7 | } 8 | 9 | bool pathfinderTest(gdr::Replay<>& macro, GJGameLevel* lvl) { 10 | auto pl = PlayLayer::create(lvl, false, false); 11 | pl->resetLevel(); 12 | 13 | std::deque inputs(macro.inputs.begin(), macro.inputs.end()); 14 | bool flipP2 = GameManager::sharedState()->getGameVariable("0010"); 15 | 16 | while (!pl->m_hasCompletedLevel && !pl->m_playerDied) { 17 | if (inputs.size() > 0) { 18 | auto& input = inputs.front(); 19 | if (input.frame <= macro.frameForTime(pl->m_timePlayed)) { 20 | pl->handleButton(input.down, input.button, input.player2 ^ flipP2); 21 | inputs.pop_front(); 22 | } 23 | } 24 | 25 | pl->update(1/240.); 26 | } 27 | 28 | return pl->m_playerDied; 29 | }*/ -------------------------------------------------------------------------------- /gd-sim/include/Portals.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | 6 | struct VehiclePortal : public EffectObject { 7 | VehicleType type; 8 | VehiclePortal(Vec2D size, std::unordered_map&& fields); 9 | void collide(Player&) const override; 10 | }; 11 | 12 | struct GravityPortal : public EffectObject { 13 | bool upsideDown; 14 | GravityPortal(Vec2D size, std::unordered_map&& fields); 15 | void collide(Player&) const override; 16 | }; 17 | 18 | struct SizePortal : public EffectObject { 19 | bool small; 20 | SizePortal(Vec2D size, std::unordered_map&& fields); 21 | void collide(Player&) const override; 22 | }; 23 | 24 | struct SpeedPortal : public EffectObject { 25 | int speed; 26 | SpeedPortal(Vec2D size, std::unordered_map&& fields); 27 | void collide(Player&) const override; 28 | }; 29 | -------------------------------------------------------------------------------- /gd-sim/test/test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | 6 | std::string readFromFile(const std::string& path) { 7 | std::ifstream file(path); 8 | std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); 9 | return str; 10 | } 11 | 12 | int main(int argc, char** argv) { 13 | if (argc < 3) { 14 | std::cerr << "Must be used by mod!" << std::endl; 15 | return 1; 16 | } 17 | 18 | std::string levelString = readFromFile(argv[1]); 19 | std::string inputs = readFromFile(argv[2]); 20 | 21 | Level lvl(levelString); 22 | 23 | lvl.debug = true; 24 | for (size_t i = 2; i < inputs.size(); ++i) { 25 | auto state = lvl.runFrame(inputs[i] == '1'); 26 | 27 | if (state.dead) { 28 | std::cerr << "Macro failed at frame " << lvl.currentFrame() << std::endl; 29 | return 0; 30 | } 31 | } 32 | 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | # Pathfinder 2 | 3 | Auto-generate macros for levels using simulation! This mod uses a physics simulator under the hood to solve levels in seconds! It does not come with a bot, you will need to install one for this. 4 | 5 | # ONLY SUPPORTS LEVELS WITH 1.9 OBJECTS 6 | ### None of these are supported 7 | - Duals 8 | - Upside-down Slopes 9 | - Partially Rotated Objects for Cube, UFO, Ball 10 | - Robot Mode 11 | - Spider Mode 12 | - Swing Mode 13 | - Any Non-Visual Triggers 14 | - Red Pads / Red Orbs 15 | - Dash Orbs 16 | - Teleport Portals 17 | - Anything from 2.2 18 | 19 | # How To Use 20 | 21 | 1. Go to a level you want to pathfind, either in your saved or an online level. 22 | 2. Click the blue Pathfinder button to start the pathfinder. 23 | 3. Export the macro into the correct folder of whichever bot you are using. 24 | 4. Import the macro and play it back! 25 | 26 | # Report Bugs 27 | 28 | Any simulation bugs need to be reported in the [Discord](https://discord.gg/u9m7kqyqxu) 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Macos be like 35 | **/.DS_Store 36 | 37 | # Cache files for Sublime Text 38 | *.tmlanguage.cache 39 | *.tmPreferences.cache 40 | *.stTheme.cache 41 | 42 | # Ignore build folders 43 | **/build 44 | # Ignore platform specific build folders 45 | build-*/ 46 | 47 | # Workspace files are user-specific 48 | *.sublime-workspace 49 | *.sublime-project 50 | 51 | # ILY vscode 52 | **/.vscode 53 | 54 | # Local History for Visual Studio Code 55 | .history/ 56 | 57 | # clangd 58 | .cache/ 59 | 60 | # Visual Studio 61 | .vs/ 62 | 63 | # CLion 64 | .idea/ 65 | /cmake-build-*/ -------------------------------------------------------------------------------- /gd-sim/src/Objects/Hazard.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | Hazard::Hazard(Vec2D size, std::unordered_map&& fields) : Object(size, std::move(fields)) { 6 | // Hazards are processed last, after everything else 7 | prio = 2; 8 | } 9 | 10 | bool Sawblade::touching(Player const& p) const { 11 | // Hitbox detection for circle 12 | 13 | Vec2D corners[4] = { 14 | Vec2D(p.getLeft(), p.getBottom()), 15 | Vec2D(p.getRight(), p.getBottom()), 16 | Vec2D(p.getRight(), p.getTop()), 17 | Vec2D(p.getLeft(), p.getTop()) 18 | }; 19 | 20 | float radius = size.x; 21 | 22 | for (auto& c : corners) { 23 | auto diff = c - pos; 24 | if (std::sqrt(diff.x * diff.x + diff.y * diff.y) <= radius) 25 | return true; 26 | c = diff; 27 | } 28 | 29 | // Check if sawblade is physically inside the hitbox 30 | if (std::signbit(corners[2].x) != std::signbit(corners[0].x) && std::signbit(corners[2].y) != std::signbit(corners[0].y)) { 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | void Hazard::collide(Player& p) const { 38 | p.dead = true; 39 | } 40 | -------------------------------------------------------------------------------- /gd-sim/include/Slope.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct Slope : public Block { 5 | int orientation; 6 | 7 | Slope(Vec2D size, std::unordered_map&& fields); 8 | void collide(Player&) const override; 9 | 10 | /** 11 | * On certain slopes, the player can still be "on" them without physically 12 | * touching them. This function is called by Player separately and accounts 13 | * for this. 14 | */ 15 | void calc(Player& p) const; 16 | 17 | /// Orientation of the slope relative to the player's gravity. 18 | int gravOrient(Player const& p) const; 19 | 20 | double angle() const; 21 | bool isFacingUp() const; 22 | 23 | /** 24 | * The Y position that a player should be snapped to if on the slope. 25 | * Also used for collision detection 26 | */ 27 | virtual double expectedY(Player const& p) const; 28 | }; 29 | 30 | 31 | struct SlopeHazard : public Slope { 32 | using Slope::Slope; 33 | void collide(Player&) const override; 34 | bool touching(Player const&) const override; 35 | double expectedY(Player const& p) const override; 36 | }; -------------------------------------------------------------------------------- /gd-sim/src/Objects/VehiclePortal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | VehiclePortal::VehiclePortal(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)) { 5 | switch (atoi(fields[1].c_str())) { 6 | case 12: 7 | type = VehicleType::Cube; 8 | break; 9 | case 13: 10 | type = VehicleType::Ship; 11 | break; 12 | case 47: 13 | type = VehicleType::Ball; 14 | break; 15 | case 111: 16 | type = VehicleType::Ufo; 17 | break; 18 | case 660: 19 | type = VehicleType::Wave; 20 | break; 21 | default: 22 | type = VehicleType::Cube; 23 | break; 24 | } 25 | } 26 | 27 | void VehiclePortal::collide(Player& player) const { 28 | EffectObject::collide(player); 29 | 30 | if (player.vehicle.type != type) { 31 | player.size = Vec2D(30, 30) * (player.small ? 0.6 : 1); 32 | 33 | // Going from wave to any other vehicle changes the velocity 34 | if (player.vehicle.type == VehicleType::Wave) 35 | player.velocity *= 0.9; 36 | 37 | player.vehicle = Vehicle::from(type); 38 | player.vehicle.enter(player); 39 | } 40 | 41 | player.floor = std::max(0., 30 * std::ceil((pos.y - (player.vehicle.bounds / 2. + 30)) / 30.)); 42 | player.ceiling = player.floor + player.vehicle.bounds; 43 | } -------------------------------------------------------------------------------- /.github/workflows/multi-platform.yml: -------------------------------------------------------------------------------- 1 | name: Build Geode Mod 2 | 3 | on: 4 | workflow_dispatch: 5 | #push: 6 | # branches: 7 | # - "**" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | config: 15 | - name: Windows 16 | os: windows-latest 17 | 18 | - name: macOS 19 | os: macos-latest 20 | 21 | - name: Android32 22 | os: ubuntu-latest 23 | target: Android32 24 | 25 | - name: Android64 26 | os: ubuntu-latest 27 | target: Android64 28 | 29 | name: ${{ matrix.config.name }} 30 | runs-on: ${{ matrix.config.os }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Build the mod 36 | uses: geode-sdk/build-geode-mod@main 37 | with: 38 | bindings: geode-sdk/bindings 39 | bindings-ref: main 40 | combine: true 41 | target: ${{ matrix.config.target }} 42 | 43 | package: 44 | name: Package builds 45 | runs-on: ubuntu-latest 46 | needs: ['build'] 47 | 48 | steps: 49 | - uses: geode-sdk/build-geode-mod/combine@main 50 | id: build 51 | 52 | - uses: actions/upload-artifact@v4 53 | with: 54 | name: Build Output 55 | path: ${{ steps.build.outputs.build-output }} 56 | -------------------------------------------------------------------------------- /.github/workflows/multi-platform-debug.yml: -------------------------------------------------------------------------------- 1 | name: Build Geode Mod (Debug) 2 | 3 | on: 4 | workflow_dispatch: 5 | #push: 6 | # branches: 7 | # - "**" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | config: 15 | - name: Windows 16 | os: windows-latest 17 | 18 | - name: macOS 19 | os: macos-latest 20 | 21 | - name: Android32 22 | os: ubuntu-latest 23 | target: Android32 24 | 25 | - name: Android64 26 | os: ubuntu-latest 27 | target: Android64 28 | 29 | name: ${{ matrix.config.name }} 30 | runs-on: ${{ matrix.config.os }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Build the mod 36 | uses: geode-sdk/build-geode-mod@main 37 | with: 38 | build-config: Debug 39 | bindings: geode-sdk/bindings 40 | bindings-ref: main 41 | combine: true 42 | target: ${{ matrix.config.target }} 43 | 44 | package: 45 | name: Package builds 46 | runs-on: ubuntu-latest 47 | needs: ['build'] 48 | 49 | steps: 50 | - uses: geode-sdk/build-geode-mod/combine@main 51 | id: build 52 | 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: Build Output 56 | path: ${{ steps.build.outputs.build-output }} 57 | -------------------------------------------------------------------------------- /gd-sim/include/Level.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | /** 8 | * In order to use the simulator, you must create a Level. The Level class is 9 | * the root class of everything else, containing both objects and player states, 10 | * as well as the main update function. See Level.cpp for implementation info. 11 | */ 12 | class Level { 13 | /// Called by constructor, applies level settings to the initial player state 14 | void initLevelSettings(std::string const& lvlSettings, Player& player); 15 | public: 16 | /** 17 | * All player states are stored, including previous states. This way, Pathfinder 18 | * is able to seamlessly rewind when searching for solutions. 19 | */ 20 | std::vector gameStates; 21 | 22 | size_t objectCount = 0; 23 | 24 | /// Sections are used just like real GD. See Object.hpp for more info on ObjectContainer. 25 | std::vector> sections; 26 | 27 | float length = 0.0; 28 | 29 | static constexpr uint32_t sectionSize = 100; 30 | bool debug = false; 31 | 32 | Level(std::string const& lvlString); 33 | 34 | /// The main update function. Every frame is associated with a press/release state. 35 | Player& runFrame(bool pressed, float dt = 1/240.); 36 | 37 | /// Go back to a certain frame. Used in Pathfinder. 38 | void rollback(int frame); 39 | 40 | int currentFrame() const; 41 | Player const& getState(int frame) const; 42 | Player& latestState(); 43 | }; 44 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | set(CMAKE_CXX_STANDARD 20) 3 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 4 | set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") 5 | set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreadedDLL) 6 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 7 | 8 | project(pathfinder VERSION 1.0.0) 9 | 10 | add_library(${PROJECT_NAME} SHARED 11 | src/main.cpp 12 | src/pathfinder.cpp 13 | src/checker.cpp 14 | src/debug.cpp 15 | # Add any extra C++ source files here 16 | ) 17 | 18 | if (NOT DEFINED ENV{GEODE_SDK}) 19 | message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") 20 | else() 21 | message(STATUS "Found Geode: $ENV{GEODE_SDK}") 22 | endif() 23 | 24 | add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) 25 | add_subdirectory(gd-sim) 26 | 27 | CPMAddPackage("gh:camila314/UIBuilder#main") 28 | CPMAddPackage("gh:maxnut/GDReplayFormat#gdr2") 29 | 30 | # add custom target to strip binary 31 | if (APPLE) 32 | add_custom_target(strip ALL 33 | COMMAND $<$:echo> strip -x $ 34 | DEPENDS ${PROJECT_NAME} 35 | ) 36 | endif() 37 | 38 | 39 | setup_geode_mod(${PROJECT_NAME}) 40 | target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:-DDEBUG_MODE=1>) 41 | target_compile_options(${PROJECT_NAME} PRIVATE $<$:-g> -O3 -fno-math-errno -fno-trapping-math -fvisibility=hidden -fomit-frame-pointer) 42 | target_link_libraries(${PROJECT_NAME} gd-sim UIBuilder libGDR) -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # beta 23 2 | 3 | - Fixed object snapping forever 4 | - Partial upside-down slope support 5 | - Start Pos support 6 | - More bug fixes 7 | 8 | # beta 22.1 (bugfix) 9 | - Fixed bug where player's hitbox was larger than intended 10 | 11 | # beta 22 12 | 13 | - Rotated object support for ship and wave 14 | - Slope fixes 15 | - 4x speed 16 | - Wave fixes 17 | - Debug button added 18 | - Fixed bounds for vehicles in level settings 19 | 20 | # beta 21.1 (minor fix) 21 | - Fixed bug where hitting pads while wave caused an error 22 | 23 | # beta 21 24 | 25 | - Full (hopefully) wave support 26 | - More slope fixes 27 | - Fixed bug where ship was inaccurate in beta 20 28 | - Other minor fixes 29 | 30 | # beta 20 31 | 32 | - Lots of slope fixes 33 | - Ship hitbox fixes 34 | 35 | # beta 19 36 | 37 | - Fix ship accelerations 38 | - Wave is now slightly more functional 39 | - Slope ejections are accurate 40 | - Everything is debug mode now 41 | 42 | # beta 18 43 | ## Fix tons of bugs: 44 | - Blue portal on floor would kill 45 | - Landing on right edge of blocks did wrong x-snapping 46 | - Small cube didn't snap on 90-60 stairs 47 | - Priority issue when hitting block and orb on the same frame 48 | 49 | # beta 17 50 | - Fix bug where export doesn't show up 51 | 52 | # beta 1-16 53 | - Initial Release 54 | - geode index does not allow for link transfers without updating the entire mod (and therefore pushing a new update to everyone) 55 | - this is not a new version. it is the same version. the geode index people do not like flexibility (?) -------------------------------------------------------------------------------- /gd-sim/include/Object.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | struct ObjectContainer; 8 | struct Player; 9 | 10 | struct Object : public Entity { 11 | /// NOT object id. A unique ID associated with each Object for direct comparisons 12 | int id; 13 | 14 | /** 15 | * In GD, some objects have their collision checks later than others (blocks, hazards). 16 | * A higher prio numbers means collisions are processed later. 17 | */ 18 | int prio; 19 | 20 | Object() = default; 21 | Object(Vec2D size, std::unordered_map&& fields); 22 | 23 | /// Determines if the object should be counted as colliding with the player. 24 | virtual bool touching(Player const&) const; 25 | 26 | /// Where all of the collision magic happens. 27 | virtual void collide(Player&) const; 28 | 29 | /// Create an object from a given level string mapping. 30 | static std::optional create(std::unordered_map&& ob); 31 | }; 32 | 33 | /** 34 | * In the simulator, there are many classes that subclass Object. Some of these add member variables 35 | * and all of them have virtual overrides. Under normal circumstances, this would require every Object 36 | * class to be dynamically allocated, which is inefficient. 37 | * 38 | * ObjectContainer provides a solution to this by storing every created Object subclass in a padded 39 | * container class. Doing this instead of dynamic allocation not only saves time from heap allocation, 40 | * it also allows for better CPU caching since all Object classes are stored contiguously in memory. 41 | */ 42 | struct ObjectContainer { 43 | char buffer[sizeof(Object) + 0x8] = {0}; 44 | 45 | ObjectContainer(ObjectContainer& cont) { memcpy(buffer, cont.buffer, sizeof(buffer)); } 46 | ObjectContainer(ObjectContainer const& cont) { memcpy(buffer, cont.buffer, sizeof(buffer)); } 47 | 48 | template 49 | ObjectContainer(T&& obj) { 50 | static_assert(sizeof(T) <= sizeof(buffer)); 51 | memcpy(buffer, (void*)&obj, sizeof(T)); 52 | } 53 | Object const* operator->() const { 54 | return reinterpret_cast(buffer); 55 | } 56 | Object* operator->() { 57 | return reinterpret_cast(buffer); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/pathfinder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "pathfinder.hpp" 6 | 7 | class Replay2 : public gdr::Replay> { 8 | public: 9 | Replay2() : Replay("Pathfinder", 1){} 10 | }; 11 | 12 | struct Level2 : public Level { 13 | bool press = false; 14 | float highestY = 0; 15 | using Level::Level; 16 | 17 | Level2(std::string const& lvlString) : Level(lvlString) { 18 | // Find highest y 19 | for (auto& i : sections) { 20 | for (auto& j : i) { 21 | highestY = std::max(highestY, j->pos.y); 22 | } 23 | } 24 | } 25 | }; 26 | 27 | bool isLevelEnd(Level2& lvl) { 28 | return lvl.latestState().pos.x >= lvl.length; 29 | } 30 | 31 | int tryInputs(Level2& lvl, std::set inputs) { 32 | auto frame = lvl.currentFrame(); 33 | auto press_before = lvl.press; 34 | 35 | while (!inputs.empty() && !lvl.gameStates.back().dead) { 36 | if (inputs.contains(lvl.currentFrame())) { 37 | lvl.press = !lvl.press; 38 | inputs.erase(lvl.currentFrame()); 39 | } 40 | 41 | lvl.runFrame(lvl.press); 42 | } 43 | int final = lvl.currentFrame(); 44 | float lastX = lvl.latestState().pos.y; 45 | float lastY = lvl.latestState().pos.y; 46 | 47 | lvl.rollback(frame); 48 | lvl.press = press_before; 49 | 50 | if (lastX < lvl.length && (lastY > 1300 || lastY < 0)) 51 | return 0; 52 | 53 | return final; 54 | } 55 | 56 | std::vector pathfind(std::string const& lvlString, std::atomic_bool& stop, std::function callback) { 57 | Level2 lvl(lvlString); 58 | 59 | std::random_device rd; 60 | std::mt19937 rng(rd()); 61 | std::uniform_int_distribution dist(0, 999); 62 | 63 | int trueBest = 0; 64 | int fail = 1; 65 | int numAway = 1000; 66 | 67 | Level2 lvlBest = lvl; 68 | 69 | while (lvl.gameStates.back().pos.x < lvl.length) { 70 | auto frame = lvl.currentFrame(); 71 | 72 | std::set bestInputs; 73 | int bestFrame = frame; 74 | 75 | constexpr int iterations = 300; //30 76 | for (int i = 0; i < iterations; i++) { 77 | 78 | std::set inputs; 79 | for (int i = 0; i < 30; i++) { 80 | inputs.insert(frame + dist(rng)); 81 | } 82 | 83 | int nf = tryInputs(lvl, inputs); 84 | 85 | if (nf > bestFrame) { 86 | bestFrame = nf; 87 | bestInputs = inputs; 88 | if (bestFrame - frame > 500 && fail < 1000) 89 | break; 90 | } 91 | } 92 | 93 | if (bestFrame == frame) { 94 | lvl.rollback(std::max(std::max(frame - fail, trueBest - numAway), 1)); 95 | 96 | fail += 5; 97 | 98 | if (fail > numAway + 1000) { 99 | numAway += 1000; 100 | fail = 1; 101 | 102 | if (numAway > 10000) { 103 | numAway = 1000; 104 | trueBest = 0; 105 | lvl.rollback(1); 106 | } 107 | } else if (fail > 100) { 108 | fail += 50; 109 | } 110 | } else { 111 | for (int i = frame; i < bestFrame - (bestFrame - frame) / 1.5; ++i) { 112 | if (bestInputs.contains(i)) { 113 | lvl.press = !lvl.press; 114 | } 115 | lvl.runFrame(lvl.press); 116 | } 117 | } 118 | 119 | if (lvl.currentFrame() > trueBest) { 120 | trueBest = lvl.currentFrame(); 121 | fail = 0; 122 | numAway = 1000; 123 | } 124 | if (lvl.currentFrame() > lvlBest.currentFrame()) { 125 | lvlBest = lvl; 126 | } 127 | 128 | if (callback) 129 | callback(std::min((lvl.latestState().pos.x / lvl.length) * 100, 100.0f)); 130 | //std::cout << "\rStatus: " << std::min((lvl.latestFrame().pos.x / lvl.length) * 100, 100.0f) << "% " << std::flush; 131 | 132 | if (stop) 133 | break; 134 | } 135 | 136 | Replay2 output; 137 | for (auto& i : lvlBest.gameStates) { 138 | if (i.frame > 1 && i.button != i.prevPlayer().button) 139 | output.inputs.push_back(gdr::Input(i.frame, 1, false, i.button)); 140 | } 141 | 142 | return output.exportData().unwrapOr({}); 143 | } 144 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/Pad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | Pad::Pad(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)) { 5 | switch (atoi(fields[1].c_str())) { 6 | case 35: 7 | type = PadType::Yellow; 8 | break; 9 | case 67: 10 | type = PadType::Blue; 11 | break; 12 | case 140: 13 | type = PadType::Pink; 14 | break; 15 | /*case 1332: 16 | type = PadType::Red; 17 | break;*/ 18 | default: 19 | type = PadType::Yellow; 20 | break; 21 | } 22 | 23 | if (atoi(fields[5].c_str()) == 1) { 24 | rotation += deg2rad(180); 25 | } 26 | } 27 | 28 | const velocity_map pad_velocities = { 29 | // slow speed 1x speed 2x speed 3x speed 30 | {{PadType::Yellow, VehicleType::Cube, false}, {864, 864, 864, 864}}, 31 | {{PadType::Yellow, VehicleType::Cube, true}, {691.2, 691.2, 691.2, 691.2}}, 32 | 33 | {{PadType::Yellow, VehicleType::Ship, false}, {864, 864, 864, 864}}, 34 | {{PadType::Yellow, VehicleType::Ship, true}, {691.2, 691.2, 691.2, 691.2}}, 35 | 36 | {{PadType::Yellow, VehicleType::Ball, false}, {518.4000206, 518.4000206, 518.4000206, 518.4000206}}, 37 | {{PadType::Yellow, VehicleType::Ball, true}, {414.7200165, 414.7200165, 414.7200165, 414.7200165}}, 38 | 39 | {{PadType::Yellow, VehicleType::Ufo, false}, {573.48, 432, 432, 432}}, 40 | {{PadType::Yellow, VehicleType::Ufo, true}, {458.784, 691.2, 691.2, 691.2}}, 41 | 42 | 43 | {{PadType::Blue, VehicleType::Cube, false}, {-345.6, -345.6, -345.6, -345.6}}, 44 | {{PadType::Blue, VehicleType::Cube, true}, {-276.48, -276.48, -276.48, -276.48}}, 45 | 46 | {{PadType::Blue, VehicleType::Ship, false}, {-229.392, -345.6, -345.6, -345.6}}, 47 | {{PadType::Blue, VehicleType::Ship, true}, {-183.519, -276.48, -276.48, -165.888}}, 48 | 49 | {{PadType::Blue, VehicleType::Ball, false}, {-160.574397, -207.360008, -207.360008, -207.360008}}, 50 | {{PadType::Blue, VehicleType::Ball, true}, {-128.463298, -165.888007, -165.888007, -165.888007}}, 51 | 52 | {{PadType::Blue, VehicleType::Ufo, false}, {-229.392, -345.6, -345.6, -345.6}}, 53 | {{PadType::Blue, VehicleType::Ufo, true}, {-183.519, -276.48, -276.48, -276.48}}, 54 | 55 | 56 | {{PadType::Pink, VehicleType::Cube, false}, {561.6, 561.6, 561.6, 561.6}}, 57 | {{PadType::Pink, VehicleType::Cube, true}, {449.28, 449.28, 449.28, 449.28}}, 58 | 59 | {{PadType::Pink, VehicleType::Ship, false}, {302.4, 302.4, 302.4, 302.4}}, 60 | {{PadType::Pink, VehicleType::Ship, true}, {241.92, 241.92, 241.92, 241.92}}, 61 | 62 | {{PadType::Pink, VehicleType::Ball, false}, {362.880014, 362.880014, 362.880014, 362.880014}}, 63 | {{PadType::Pink, VehicleType::Ball, true}, {290.304012, 290.304012, 290.304012, 290.304012}}, 64 | 65 | {{PadType::Pink, VehicleType::Ufo, false}, {345.6, 345.6, 345.6, 345.6}}, 66 | {{PadType::Pink, VehicleType::Ufo, true}, {276.48, 276.48, 276.48, 276.48}} 67 | }; 68 | 69 | void Pad::collide(Player& p) const { 70 | if (type == PadType::Blue) { 71 | auto rot = rad2deg(std::abs(rotation)); 72 | if ((rot > 90 && !p.upsideDown) || (rot < 90 && p.upsideDown)) 73 | return; 74 | 75 | // Can't flip twice in a frame with blue pad 76 | if (p.upsideDown != p.prevPlayer().upsideDown) 77 | return; 78 | 79 | p.upsideDown = !p.upsideDown; 80 | 81 | if (p.vehicle.type == VehicleType::Wave) 82 | p.velocity = -p.velocity; 83 | } 84 | 85 | if (p.vehicle.type != VehicleType::Wave) 86 | p.velocity = pad_velocities.get(type, p.vehicle.type, p.small, std::min(3, p.speed)); 87 | 88 | p.grounded = false; 89 | p.gravityPortal = false; 90 | 91 | EffectObject::collide(p); 92 | 93 | } 94 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/Orb.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | Orb::Orb(Vec2D size, std::unordered_map&& fields) : EffectObject(size, std::move(fields)) { 5 | switch (std::stoi(fields[1])) { 6 | case 36: 7 | type = OrbType::Yellow; 8 | break; 9 | case 84: 10 | type = OrbType::Blue; 11 | break; 12 | case 141: 13 | type = OrbType::Pink; 14 | break; 15 | default: 16 | type = OrbType::Yellow; 17 | break; 18 | }; 19 | } 20 | 21 | bool Orb::touching(Player const& p) const { 22 | // Coyote frame for orbs 23 | return EffectObject::touching(p) || EffectObject::touching(p.prevPlayer()); 24 | } 25 | 26 | const velocity_map orb_velocities = { 27 | // slow speed 1x speed 2x speed 3x speed 28 | {{OrbType::Yellow, VehicleType::Cube, false}, {573.48, 603.72, 616.68, 606.42}}, 29 | {{OrbType::Yellow, VehicleType::Cube, true}, {458.784, 482.976, 481.734, 485.136}}, 30 | 31 | {{OrbType::Yellow, VehicleType::Ship, false}, {573.48, 603.72, 616.68, 606.42}}, 32 | {{OrbType::Yellow, VehicleType::Ship, true}, {458.784, 482.976, 481.734, 485.136}}, 33 | 34 | {{OrbType::Yellow, VehicleType::Ball, false}, {401.435993, 422.60399, 431.67599, 424.493993}}, 35 | {{OrbType::Yellow, VehicleType::Ball, true}, {321.148795, 338.08319, 345.34079, 339.59519}}, 36 | 37 | {{OrbType::Yellow, VehicleType::Ufo, false}, {573.48, 603.72, 616.68, 606.42}}, 38 | {{OrbType::Yellow, VehicleType::Ufo, true}, {458.784, 482.976, 481.734, 485.136}}, 39 | 40 | 41 | {{OrbType::Blue, VehicleType::Cube, false}, {-229.392, -241.488, -246.672, -242.568}}, 42 | {{OrbType::Blue, VehicleType::Cube, true}, {-183.519, -193.185, -197.343, -194.049}}, 43 | 44 | {{OrbType::Blue, VehicleType::Ship, false}, {-229.392, -241.488, -246.672, -242.568}}, 45 | {{OrbType::Blue, VehicleType::Ship, true}, {-183.519, -193.185, -197.343, -194.049}}, 46 | 47 | {{OrbType::Blue, VehicleType::Ball, false}, {-160.574397, -169.04160, -172.6704, -169.7976}}, 48 | {{OrbType::Blue, VehicleType::Ball, true}, {-128.463298, -135.2295, -138.1401, -135.8343}}, 49 | 50 | {{OrbType::Blue, VehicleType::Ufo, false}, {-229.392, -241.48, -246.672, -242.568}}, 51 | {{OrbType::Blue, VehicleType::Ufo, true}, {-183.519, -193.185, -197.343, -194.049}}, 52 | 53 | 54 | {{OrbType::Pink, VehicleType::Cube, false}, {412.884, 434.7, 443.988, 436.644}}, 55 | {{OrbType::Pink, VehicleType::Cube, true}, {330.318, 347.76, 355.212, 349.272}}, 56 | 57 | {{OrbType::Pink, VehicleType::Ship, false}, {212.166, 223.398, 228.15, 224.37}}, 58 | {{OrbType::Pink, VehicleType::Ship, true}, {169.776, 178.686, 182.52, 179.496}}, 59 | 60 | {{OrbType::Pink, VehicleType::Ball, false}, {309.090595, 325.42019, 332.37539, 326.85659}}, 61 | {{OrbType::Pink, VehicleType::Ball, true}, {247.287596, 260.3286, 265.923, 261.5004}}, 62 | 63 | {{OrbType::Pink, VehicleType::Ufo, false}, {240.84, 253.584, 258.984, 254.718}}, 64 | {{OrbType::Pink, VehicleType::Ufo, true}, {192.672, 202.824, 207.198, 203.742}} 65 | }; 66 | 67 | void Orb::collide(Player& p) const { 68 | // Orbs are often buffered, but p.buffer will still be true if its not a real buffer 69 | if (p.buffer || (p.prevPlayer().buffer && !p.button) || (p.vehicle.type == VehicleType::Ball && p.vehicleBuffer)) { 70 | p.buffer = false; 71 | p.vehicleBuffer = false; 72 | 73 | EffectObject::collide(p); 74 | 75 | // Wave can't use non-gravity orbs 76 | if (p.vehicle.type != VehicleType::Wave) { 77 | p.velocity = orb_velocities.get(type, p.vehicle.type, p.small , std::min(3, p.speed)); 78 | p.grounded = false; 79 | } 80 | 81 | if (type == OrbType::Blue) { 82 | p.upsideDown = !p.upsideDown; 83 | } 84 | 85 | // Clicking on an orb as ball essentially removes the input 86 | if (p.vehicle.type == VehicleType::Ball) 87 | p.input = false; 88 | } 89 | } -------------------------------------------------------------------------------- /gd-sim/include/Player.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #undef small 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | /// Player X velocity per speed 13 | inline double player_speeds[5] = { 14 | 251.16007972276924, 15 | 311.580093712804, 16 | 387.42014039710523, 17 | 468.0001388338566, 18 | 576.00020058307177 19 | }; 20 | 21 | /// Used in player rotation. Similar to m_playerSpeed member variable 22 | inline float player_speedmults[5] = { 23 | 0.7, 24 | 0.9, 25 | 1.1, 26 | 1.3, 27 | 1.6 28 | }; 29 | 30 | double roundVel(double velocity, bool upsideDown); 31 | 32 | struct Object; 33 | class Level; 34 | struct Slope; 35 | 36 | /** 37 | * The main player. This contains the entire player state and is the only thing that changes each frame. 38 | */ 39 | struct Player : public Entity { 40 | Vehicle vehicle; 41 | Level* level; 42 | 43 | double timeElapsed; 44 | double acceleration; 45 | double velocity; 46 | 47 | /// See util.hpp for what cow_set is 48 | cow_set usedEffects; 49 | 50 | // Potential slopes are important for block collisions. Reset per frame. 51 | std::vector potentialSlopes; 52 | 53 | /// Actions will be ran at the beginning of every frame. 54 | std::vector> actions; 55 | 56 | /// Slopes have special collision rules. See Slope.cpp for more information. 57 | struct { 58 | std::optional slope; 59 | 60 | /// Time on slope 61 | double elapsed; 62 | 63 | /// When colliding with a downhill slope with a positive velocity. 64 | bool snapDown; 65 | } slopeData; 66 | 67 | /// X-snapping. See Block.cpp for more information 68 | struct { 69 | Entity object; 70 | int playerFrame = 0; 71 | } snapData; 72 | 73 | float ceiling; 74 | float floor; 75 | float dt; 76 | 77 | /// Some vehicles have coyote frames for valid inputs 78 | unsigned int coyoteFrames; 79 | 80 | int speed; 81 | int frame; 82 | 83 | bool dead; 84 | bool grounded; 85 | 86 | /** 87 | * Under normal circumstances, acceleration is applied to the velocity 88 | * at the end of a frame. When this field is set, acceleration will not 89 | * be applied at the end of the frame. Mainly set via `setVelocity` 90 | */ 91 | bool velocityOverride; 92 | 93 | /** 94 | * `button` vs `input`: Button always refers to whether a click was applied; input can be disabled 95 | * for niche circumstances where no other normal operations are allowed despite a click. 96 | */ 97 | bool button, input; 98 | 99 | /// If a click is being buffered. Used for things like orb clicks. 100 | bool buffer; 101 | /** 102 | * In the ball vehicle, holding a click while transitioning into another vehicle will cause a 103 | * buffered input, despite things like orbs not buffering in the same way. 104 | */ 105 | bool vehicleBuffer; 106 | 107 | bool upsideDown; 108 | bool small; 109 | 110 | /// Entering a gravity portal can cause the next frame to have certain edge cases 111 | bool gravityPortal; 112 | 113 | /// Whether velocity at the end of the frame is to be rounded (typically used by ball) 114 | bool roundVelocity; 115 | 116 | Player(); 117 | 118 | void preCollision(bool input); 119 | void postCollision(); 120 | 121 | 122 | Entity unrotatedHitbox() const; 123 | 124 | /// Inner hitbox of {9, 9} that is used mainly for blocks 125 | Entity innerHitbox() const; 126 | 127 | Player const& prevPlayer() const; 128 | Player const* nextPlayer() const; 129 | 130 | /// Values relative to player gravity. 131 | template 132 | T grav(T value) const { return upsideDown ? -value : value; } 133 | inline float gravBottom(Entity const& e) const { return upsideDown ? -e.getTop() : e.getBottom(); } 134 | inline float gravTop(Entity const& e) const { return upsideDown ? -e.getBottom() : e.getTop(); } 135 | inline float gravFloor() const { return upsideDown ? -ceiling : floor; } 136 | inline float gravCeiling() const { return upsideDown ? -floor : ceiling; } 137 | 138 | void setVelocity(double v, bool override=false); 139 | }; -------------------------------------------------------------------------------- /gd-sim/src/Player.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | Entity Player::innerHitbox() const { 8 | return {pos, Vec2D{9, 9}, 0}; 9 | } 10 | 11 | Entity Player::unrotatedHitbox() const { 12 | return {pos, size, 0}; 13 | } 14 | 15 | void Player::setVelocity(double v, bool override) { 16 | velocityOverride = override; 17 | 18 | // Being small commonly means velocity is 4/5 the original velocity. 19 | velocity = v * (small ? 0.8 : 1); 20 | 21 | if (v != 0) 22 | grounded = false; 23 | } 24 | 25 | Player const& Player::prevPlayer() const { 26 | return level->getState(frame - 1); 27 | } 28 | 29 | Player const* Player::nextPlayer() const { 30 | return level->currentFrame() <= frame ? nullptr : &level->getState(frame + 1); 31 | } 32 | 33 | /** 34 | * In Geometry Dash, velocity is stored as 1/54 of distance per second. 35 | * It is also rounded to the nearest hundredth after (almost) every operation. 36 | * This function accounts for that rounding 37 | */ 38 | double roundVel(double velocity, bool upsideDown) { 39 | double nVel = velocity / 54.0 * (upsideDown * 2 - 1); 40 | double floored = (int)nVel; 41 | if (nVel != floored) { 42 | nVel = (double)std::round((nVel - floored) * 1000.0) / 1000.0 + floored; 43 | } 44 | return nVel * 54.0 * (upsideDown * 2 - 1); 45 | } 46 | 47 | void Player::preCollision(bool pressed) { 48 | pos.x += player_speeds[(int)speed] * dt; 49 | pos.y += grav(velocity) * dt; 50 | 51 | frame++; 52 | timeElapsed += dt; 53 | grounded = false; 54 | velocityOverride = false; 55 | gravityPortal = false; 56 | roundVelocity = true; 57 | 58 | if (button != pressed) { 59 | button = pressed; 60 | input = button; 61 | buffer = button; 62 | } 63 | 64 | for (auto& i : actions) 65 | i(*this); 66 | actions.clear(); 67 | 68 | potentialSlopes.clear(); 69 | 70 | // Downhill slopes snap you automatically 71 | if (slopeData.slope && slopeData.slope->gravOrient(*this) == 1) { 72 | grounded = true; 73 | } 74 | } 75 | 76 | void Player::postCollision() { 77 | // Size portal only affects hitbox size at the end of frame 78 | if (small != prevPlayer().small) { 79 | size = small ? (size * 0.6) : (size / 0.6); 80 | } 81 | 82 | if (gravBottom(*this) <= gravFloor() && !velocityOverride && velocity <= 0 ) { 83 | pos.y = grav(gravFloor()) + grav(size.y / 2); 84 | grounded = true; 85 | snapData.playerFrame = 0; 86 | } 87 | 88 | // Fell through ceiling, or hit floor 89 | if (pos.y > 1476.3 || (upsideDown && getBottom() < floor)) { 90 | dead = true; 91 | return; 92 | } 93 | 94 | // Coyote frames 95 | if (prevPlayer().gravBottom(*this) > prevPlayer().gravFloor() && upsideDown == prevPlayer().upsideDown && !grounded && velocity <= 0) { 96 | if (prevPlayer().grounded && !prevPlayer().input) 97 | coyoteFrames = 0; 98 | coyoteFrames++; 99 | } else { 100 | // Nothing will check for coyote frames this high 101 | coyoteFrames = INT_MAX; 102 | } 103 | 104 | vehicle.update(*this); 105 | 106 | if (!velocityOverride) { 107 | double newVel = velocity + acceleration * dt; 108 | 109 | // Player will fall off blocks a frame faster than expected. 110 | if (!grounded && prevPlayer().grounded && ((!input && (prevPlayer().button || !button)) || buffer) && prevPlayer().gravBottom(*this) > prevPlayer().gravFloor() && size == prevPlayer().size) { 111 | pos.y += roundVel(prevPlayer().grav(prevPlayer().acceleration) * dt, prevPlayer().upsideDown) * dt; 112 | 113 | if (gravityPortal && vehicle.type != VehicleType::Ship) 114 | newVel = -newVel; 115 | 116 | if (velocity == 0) 117 | newVel += roundVel(prevPlayer().acceleration * dt, upsideDown); 118 | } 119 | velocity = newVel; 120 | } 121 | 122 | // Ball movements are not rounded in GD. Probably a bug! 123 | if (roundVelocity) 124 | velocity = roundVel(velocity, upsideDown); 125 | 126 | if (slopeData.slope) 127 | slopeData.slope->calc(*this); 128 | 129 | // Ensure the player hasn't gone beyond the bounds of the vehicle 130 | vehicle.clamp(*this); 131 | } 132 | 133 | Player::Player() : 134 | Entity({{0, 15}, {30, 30}, 0}), frame(1), timeElapsed(0), dead(false), 135 | vehicle(Vehicle::from(VehicleType::Cube)), 136 | ceiling(999999), floor(0), grounded(true), 137 | coyoteFrames(0), acceleration(0), velocity(0), 138 | velocityOverride(false), button(false), input(false), 139 | vehicleBuffer(false), upsideDown(false), small(false), 140 | speed(1), slopeData({{}, 0, false}), roundVelocity(true) {} 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/Block.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | Block::Block(Vec2D s, std::unordered_map&& fields) : Object(s, std::move(fields)) { 10 | // Blocks have a prio of 1, so they are processed later than most other objects. 11 | prio = 1; 12 | 13 | // No rotation allowed 14 | if ((int)fabs(rotation) % 180 != 0) 15 | size = {size.y, size.x}; 16 | rotation = 0; 17 | 18 | // Edge case for this specific block. 19 | if (fields[1] == "468" && size.y == 5) { 20 | size.y -= 3.5; 21 | } 22 | } 23 | 24 | 25 | /** 26 | * "Snapping" here refers to what happens when the cube player is 27 | * holding a button on given block staircases. In order to prevent 28 | * the player from falling off a block staircase, the X position 29 | * is manually adjusted by a set amount. The amount depends on 30 | * the type of staircase, speed, and cube size. 31 | */ 32 | enum class SnapType { 33 | None, 34 | BigStair, 35 | LittleStair, 36 | DownStair 37 | }; 38 | 39 | 40 | float snapThreshold(Vec2D const& diff, Player const& p) { 41 | std::array stairs; 42 | float threshold; 43 | 44 | switch (p.speed) { 45 | case 0: 46 | stairs = { Vec2D(120, -30), Vec2D(90, 30), Vec2D(60, 60) }; 47 | threshold = 1; 48 | break; 49 | case 1: 50 | stairs = { Vec2D(150, -30), Vec2D(p.small ? 90 : 120, 30), Vec2D(90, 60) }; 51 | threshold = 1; 52 | break; 53 | case 2: 54 | stairs = { Vec2D(180, -30), Vec2D(p.small ? 90 : 150, 30), Vec2D(120, 60) }; 55 | threshold = 2; 56 | break; 57 | case 3: 58 | stairs = { Vec2D(225, -30), Vec2D(p.small ? 90 : 180, 30), Vec2D(135, 60) }; 59 | threshold = 2; 60 | break; 61 | default: 62 | stairs = { Vec2D(150, -30), Vec2D(120, 30), Vec2D(90, 60) }; 63 | threshold = 1; 64 | break; 65 | } 66 | 67 | for (auto& stair : stairs) { 68 | if (std::abs(diff.x - stair.x) <= threshold && std::abs(diff.y - stair.y) <= threshold) 69 | return threshold; 70 | } 71 | 72 | return 0; 73 | } 74 | 75 | void trySnap(Block const& b, Player& p) { 76 | auto snapData = p.snapData; 77 | auto diff = b.pos - snapData.object.pos; 78 | diff.y = p.grav(diff.y); 79 | 80 | if (float threshold = snapThreshold(diff, p); threshold > 0) { 81 | p.pos.x = std::clamp( 82 | p.level->getState(snapData.playerFrame).nextPlayer()->pos.x + diff.x, 83 | p.pos.x - threshold, 84 | p.pos.x + threshold 85 | ); 86 | } 87 | } 88 | 89 | void Block::collide(Player& p) const { 90 | // The maximum amount the player can dip below block while still being snapped up 91 | int clip = (p.vehicle.type == VehicleType::Ufo || p.vehicle.type == VehicleType::Ship) ? 7 : 10; 92 | 93 | // When hitting blue orbs/pads, there is a single frame where block collisions arent done. 94 | // TODO might be able to be merged with the other pad check 95 | if (p.upsideDown != p.prevPlayer().upsideDown && !p.gravityPortal) 96 | return; 97 | 98 | /* 99 | Going from slope to block means you have to check the bottom of the player 100 | adjusted for the angle of the slope 101 | */ 102 | double bottom = p.gravBottom(p); 103 | if (p.slopeData.slope) { 104 | if (p.slopeData.slope->angle() > 0) { 105 | bottom = bottom + sin(p.slopeData.slope->angle()) * p.size.y / 2; 106 | clip = 7; 107 | 108 | // Prevent block from catching on slope when it shouldn't 109 | // TODO this should probably go somewhere else... 110 | if (p.gravTop(*this) - bottom < 2) { 111 | return; 112 | } 113 | } 114 | } 115 | 116 | for (auto& entity : p.potentialSlopes) { 117 | auto block_comp = entity->orientation < 2 ? getTop() : getBottom(); 118 | auto slope_comp = entity->orientation < 2 ? entity->getBottom() : entity->getTop(); 119 | 120 | if (block_comp - slope_comp < 2) { 121 | return; 122 | } 123 | } 124 | 125 | bool padHitBefore = (!p.prevPlayer().grounded && p.prevPlayer().velocity <= 0 && p.velocity > 0); 126 | 127 | if (p.innerHitbox().intersects(*this)) { 128 | // Hitting block head-on 129 | p.dead = true; 130 | } else if (p.vehicle.type != VehicleType::Wave && p.gravTop(*this) - bottom <= clip && (padHitBefore || p.velocity <= 0 || p.gravityPortal)) { 131 | p.pos.y = p.grav(p.gravTop(*this)) + p.grav(p.size.y / 2); 132 | 133 | // When hitting pads, the next frame will cause the player ot slightly dip into the block 134 | if (!padHitBefore) 135 | p.grounded = true; 136 | 137 | // If on a downhill slope and hits a block, the player is no longer on that slope. 138 | if (p.slopeData.slope && p.slopeData.slope->angle() < 0) { 139 | p.slopeData.slope = {}; 140 | } 141 | 142 | // X-snapping 143 | if (p.vehicle.type == VehicleType::Cube) { 144 | if (!p.prevPlayer().grounded) { 145 | if (p.snapData.playerFrame > 0 && p.snapData.playerFrame + 1 < p.frame) 146 | trySnap(*this, p); 147 | } 148 | 149 | p.snapData.playerFrame = p.level->currentFrame(); 150 | p.snapData.object = *this; 151 | } 152 | } else { 153 | // Ship and ufo can hit the ceiling of a block without dying 154 | if (p.vehicle.type == VehicleType::Ship || p.vehicle.type == VehicleType::Ufo || p.vehicle.type == VehicleType::Ball) { 155 | if (p.gravTop(p) - p.gravBottom(*this) <= clip - 1 && p.velocity > 0) { 156 | p.pos.y = p.grav(p.gravBottom(*this)) - p.grav(p.size.y / 2); 157 | p.velocity = 0; 158 | } 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /gd-sim/src/Level.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void Level::initLevelSettings(std::string const& lvlSettings, Player& player) { 6 | std::unordered_map obj; 7 | 8 | std::stringstream ss2(lvlSettings); 9 | std::string k, v; 10 | while (std::getline(ss2, k, ',')) { 11 | std::getline(ss2, v, ','); 12 | obj[k] = v; 13 | } 14 | 15 | // Helper to make a default value for nonexistant keys 16 | auto get_or = [&obj](std::string const& key, std::string const& def) { 17 | if (auto it = obj.find(key); it != obj.end()) 18 | return it->second.c_str(); 19 | return def.c_str(); 20 | }; 21 | 22 | player.speed = atoi(get_or("kA4", "0")); 23 | 24 | // Robtop stores 1x speed as 0 and slow speed as 1. Very silly 25 | if (player.speed == 0) 26 | player.speed = 1; 27 | else if (player.speed == 1) 28 | player.speed = 0; 29 | 30 | if ((player.small = atoi(get_or("kA3", "0")))) 31 | player.size = player.size * 0.6; 32 | 33 | player.upsideDown = atoi(get_or("kA11", "0")); 34 | player.vehicle = Vehicle::from(static_cast(atoi(get_or("kA2", "0")))); 35 | 36 | player.floor = 0; 37 | player.ceiling = player.vehicle.bounds; 38 | } 39 | 40 | Level::Level(std::string const& lvlString) { 41 | // Split by ';' to parse level string 42 | std::stringstream ss(lvlString); 43 | std::string objstr; 44 | bool first = true; 45 | 46 | // First player state 47 | auto player = Player(); 48 | 49 | while (std::getline(ss, objstr, ';')) { 50 | // First entry is level settings object 51 | if (first) { 52 | initLevelSettings(objstr, player); 53 | first = false; 54 | continue; 55 | } 56 | 57 | std::unordered_map obj; 58 | 59 | std::stringstream ss2(objstr); 60 | std::string k, v; 61 | while (std::getline(ss2, k, ',')) { 62 | std::getline(ss2, v, ','); 63 | if (atoi(k.c_str()) > 0) 64 | obj[atoi(k.c_str())] = v; 65 | } 66 | 67 | if (obj[1] == "31") { 68 | initLevelSettings(objstr, player); 69 | player.pos.x = stod_def(obj[2], 0); 70 | player.pos.y = stod_def(obj[3], 0); 71 | } 72 | 73 | if (auto ob_o = Object::create(std::move(obj))) { 74 | auto ob = ob_o.value(); 75 | 76 | // Unique ID 77 | ob->id = objectCount++; 78 | 79 | // Sections are divided by x position in increments of 100 80 | size_t sectionPos = std::max(.0f, ob->pos.x / sectionSize); 81 | if (sectionPos >= sections.size()) 82 | sections.resize(sectionPos + 1); 83 | sections[sectionPos].push_back(ob); 84 | 85 | if (ob->pos.x > length) 86 | length = ob->pos.x + 100; 87 | } 88 | } 89 | 90 | player.level = this; 91 | gameStates.push_back(player); 92 | } 93 | 94 | Player& Level::runFrame(bool pressed, float dt) { 95 | Player p = gameStates.back(); 96 | 97 | // Can't play if you're dead 98 | if (p.dead) 99 | return gameStates.back(); 100 | 101 | p.dt = dt; 102 | p.preCollision(pressed); 103 | 104 | // Objects from previous, current, and next section are all collision tested 105 | size_t sectionIdx = std::min(std::max(0, (int)(p.pos.x / sectionSize)), (int)sections.size() - 1); 106 | auto prevSection = §ions[sectionIdx == 0 ? 0 : sectionIdx - 1]; 107 | auto currSection = §ions[sectionIdx]; 108 | auto nextSection = §ions[sectionIdx + 1 >= sections.size() - 1 ? sections.size() - 1 : sectionIdx + 1]; 109 | 110 | // If at start or end of level, previous/next section is invalid so don't use it 111 | std::vector* sections[3] = { prevSection, nullptr, nullptr }; 112 | if (&currSection != &prevSection) 113 | sections[1] = currSection; 114 | if (&nextSection != &currSection) 115 | sections[2] = nextSection; 116 | 117 | // Blocks are hazards processed separately 118 | std::vector blocks; 119 | std::vector hazards; 120 | blocks.reserve(100); 121 | hazards.reserve(100); 122 | 123 | size_t numCollisions = 0; 124 | 125 | for (auto section : sections) { 126 | if (section == nullptr) continue; 127 | for (auto& o : *section) { 128 | if (p.dead) break; 129 | if (o->prio == 1) 130 | blocks.push_back(o); 131 | else if (o->prio == 2) 132 | hazards.push_back(o); 133 | else if (o->touching(p)) { 134 | ++numCollisions; 135 | o->collide(p); 136 | } 137 | } 138 | } 139 | 140 | // Blocks are processed in descending order 141 | for (int i = blocks.size() - 1; i >= 0; --i) { 142 | if (p.dead) break; 143 | auto& b = blocks[i]; 144 | if (b->touching(p)) { 145 | ++numCollisions; 146 | b->collide(p); 147 | } 148 | } 149 | 150 | for (auto& h : hazards) { 151 | if (p.dead) break; 152 | if (h->touching(p)) { 153 | ++numCollisions; 154 | h->collide(p); 155 | 156 | } 157 | } 158 | 159 | if (!p.dead) 160 | p.postCollision(); 161 | 162 | if (debug) { 163 | std::cout << "Frame " << gameStates.size() << std::fixed << std::setprecision(8) 164 | << " X " << p.pos.x << " Y " << p.pos.y - 15 << " Vel " << p.velocity 165 | << " Accel " << p.acceleration << " Rot " << p.rotation << " Coll " << numCollisions 166 | << std::endl; 167 | 168 | if (p.button != gameStates.back().button) { 169 | std::cout << "Input X " << p.pos.x << " Y " << p.pos.y - 15 << std::endl; 170 | } 171 | } 172 | 173 | gameStates.push_back(p); 174 | return gameStates.back(); 175 | } 176 | 177 | 178 | void Level::rollback(int frame) { 179 | gameStates.resize(frame > 0 ? frame : 1); 180 | } 181 | 182 | int Level::currentFrame() const { 183 | return gameStates.size(); 184 | } 185 | 186 | Player const& Level::getState(int frame) const { 187 | if (frame == 0) 188 | return gameStates[0]; 189 | if (gameStates.size() < frame) 190 | return gameStates.back(); 191 | return gameStates[frame - 1]; 192 | } 193 | 194 | Player& Level::latestState() { 195 | return gameStates.back(); 196 | } 197 | -------------------------------------------------------------------------------- /gd-sim/include/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class hash_tuple { 14 | template 15 | struct component { 16 | const T& value; 17 | component(const T& value) : value(value) {} 18 | uintmax_t operator,(uintmax_t n) const { 19 | n ^= std::hash()(value); 20 | n ^= n << (sizeof(uintmax_t) * 4 - 1); 21 | return n ^ std::hash()(n); 22 | } 23 | }; 24 | 25 | public: 26 | template 27 | size_t operator()(const Tuple& tuple) const { 28 | return std::hash()( 29 | std::apply([](const auto& ... xs) { return (component(xs), ..., 0); }, tuple)); 30 | } 31 | }; 32 | 33 | template 34 | struct velocity_map : public std::unordered_map, std::vector, hash_tuple> { 35 | using std::unordered_map, std::vector, hash_tuple>::unordered_map; 36 | double get(Args... args, int sec) const { 37 | return *(this->at(std::tuple(args...)).begin() + sec); 38 | } 39 | }; 40 | 41 | /** 42 | * Using normal sets is extremely bad for performance since we would need to make 43 | * a copy for every frame, due it saving all previous states. I wrote this to ensure 44 | * that unnecessary copies do not happen if nothing is changed between frames. 45 | */ 46 | template 47 | class cow_set { 48 | std::shared_ptr> data; 49 | bool written; 50 | public: 51 | cow_set() : data(std::make_shared>()), written(false) {} 52 | cow_set(cow_set const& other) : data(other.data), written(false) {} 53 | cow_set(cow_set&& other) : data(std::move(other.data)), written(false) {} 54 | cow_set(std::unordered_set const& other) : data(std::make_shared>(other)), written(false) {} 55 | 56 | cow_set& operator=(cow_set const& other) { 57 | data = other.data; 58 | written = false; 59 | return *this; 60 | } 61 | cow_set& operator=(cow_set&& other) { 62 | data = std::move(other.data); 63 | written = false; 64 | return *this; 65 | } 66 | void insert(T const& value) { 67 | if (!written) { 68 | data = std::make_shared>(*data); 69 | written = true; 70 | } 71 | data->insert(value); 72 | } 73 | void erase(T const& value) { 74 | if (!written) { 75 | data = std::make_shared>(*data); 76 | written = true; 77 | } 78 | data->erase(value); 79 | } 80 | bool contains(T const& value) const { 81 | return data->contains(value); 82 | } 83 | bool empty() const { 84 | return data->empty(); 85 | } 86 | size_t size() const { 87 | return data->size(); 88 | } 89 | void clear() { 90 | if (!written) { 91 | data = std::make_shared>(); 92 | written = true; 93 | } 94 | data->clear(); 95 | } 96 | std::unordered_set const& get() const { 97 | return *data; 98 | } 99 | }; 100 | 101 | float slerp(float fromAngle, float toAngle, float t); 102 | 103 | /// Combine two values into one for the purposes of switches 104 | constexpr int case_and(auto a, auto b) { 105 | return static_cast(a) | (static_cast(b) << 4); 106 | } 107 | 108 | inline constexpr float deg2rad(float deg) { 109 | return deg * 3.141592653 / 180; 110 | } 111 | inline constexpr float rad2deg(float rad) { 112 | return rad * 180 / 3.141592653; 113 | } 114 | 115 | inline float stod_def(std::string const& str, float def = 0) { 116 | char const* begin = str.c_str(); 117 | char* end = nullptr; 118 | 119 | errno = 0; 120 | double out = std::strtod(begin, &end); 121 | if (begin == end || *end != '\0' || errno == ERANGE) { 122 | return def; 123 | } 124 | 125 | return out; 126 | } 127 | 128 | struct Vec2D { 129 | float x; 130 | float y; 131 | 132 | inline Vec2D() : x(0), y(0) {} 133 | inline Vec2D(float x, float y) : x(x), y(y) {} 134 | 135 | inline bool operator==(Vec2D const& other) const { 136 | return x == other.x && y == other.y; 137 | } 138 | 139 | inline Vec2D operator-(Vec2D const& other) const { 140 | return {x - other.x, y - other.y}; 141 | } 142 | inline Vec2D operator+(Vec2D const& other) const { 143 | return {x + other.x, y + other.y}; 144 | } 145 | inline Vec2D operator*(float scalar) const { 146 | return {x * scalar, y * scalar}; 147 | } 148 | inline Vec2D operator/(float scalar) const { 149 | return {x / scalar, y / scalar}; 150 | } 151 | inline Vec2D& operator+=(Vec2D const& other) { 152 | x += other.x; 153 | y += other.y; 154 | return *this; 155 | } 156 | inline Vec2D& operator-=(Vec2D const& other) { 157 | x -= other.x; 158 | y -= other.y; 159 | return *this; 160 | } 161 | inline Vec2D& operator*=(float scalar) { 162 | x *= scalar; 163 | y *= scalar; 164 | return *this; 165 | } 166 | inline Vec2D& operator/=(float scalar) { 167 | x /= scalar; 168 | y /= scalar; 169 | return *this; 170 | } 171 | 172 | Vec2D rotate(float angle, Vec2D const& pivot = {0, 0}) const; 173 | }; 174 | 175 | struct Entity { 176 | Vec2D pos; 177 | Vec2D size; 178 | float rotation; 179 | bool intersects(Entity const& other) const; 180 | 181 | inline float getLeft() const { return pos.x - size.x / 2; } 182 | inline float getRight() const { return pos.x + size.x / 2; } 183 | inline float getTop() const { return pos.y + size.y / 2; } 184 | inline float getBottom() const { return pos.y - size.y / 2; } 185 | }; 186 | 187 | -------------------------------------------------------------------------------- /gd-sim/src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | float slerp(float fromAngle, float toAngle, float t) { 6 | std::complex fromVec = std::polar(1.0f, fromAngle * 0.5f); 7 | std::complex toVec = std::polar(1.0f, toAngle * 0.5f); 8 | 9 | 10 | float dot = std::imag(fromVec) * std::imag(toVec) + std::real(fromVec) * std::real(toVec); 11 | if (dot < 0.0f) { 12 | dot *= -1; 13 | toVec *= -1; 14 | } 15 | 16 | std::complex weight = std::complex(1.0f - t, t); 17 | if (dot < 0.9999) { 18 | float between = std::acos(dot); 19 | 20 | weight *= between; 21 | weight = std::complex(std::sin(weight.real()), std::sin(weight.imag())); 22 | weight /= std::sin(between); 23 | } 24 | 25 | std::complex interpVec = (weight.imag() * toVec) + (weight.real() * fromVec); 26 | return std::atan2(std::imag(interpVec), std::real(interpVec)) * 2; 27 | } 28 | 29 | 30 | Vec2D Vec2D::rotate(float angle, Vec2D const& pivot) const { 31 | if (angle == 0) return *this; 32 | 33 | Vec2D tmp = *this - pivot; 34 | 35 | float rad = deg2rad(angle); 36 | float s = std::sin(rad); 37 | float c = std::cos(rad); 38 | 39 | tmp = {tmp.x * c - tmp.y * s, tmp.x * s + tmp.y * c}; 40 | tmp += pivot; 41 | 42 | return tmp; 43 | } 44 | 45 | // Very complicated (but very fast) way to check intersection 46 | bool intersectOneWay(Entity const& a, Entity const& b) { 47 | float big = std::max(a.size.x, a.size.y) + std::max(b.size.x, b.size.y); 48 | if (std::abs(a.pos.x - b.pos.x) > big || std::abs(a.pos.y - b.pos.y) > big) { 49 | return false; 50 | } 51 | 52 | Entity tmp = b; 53 | 54 | tmp.rotation -= a.rotation; 55 | tmp.pos = tmp.pos.rotate(-a.rotation, a.pos); 56 | 57 | Vec2D corners[4] = { 58 | Vec2D(tmp.getLeft(), tmp.getBottom()).rotate(tmp.rotation, tmp.pos), 59 | Vec2D(tmp.getRight(), tmp.getBottom()).rotate(tmp.rotation, tmp.pos), 60 | Vec2D(tmp.getRight(), tmp.getTop()).rotate(tmp.rotation, tmp.pos), 61 | Vec2D(tmp.getLeft(), tmp.getTop()).rotate(tmp.rotation, tmp.pos) 62 | }; 63 | 64 | float lastDiffX = 0; 65 | bool overlapX = false; 66 | 67 | float lastDiffY = 0; 68 | bool overlapY = false; 69 | 70 | for (auto vert : corners) { 71 | if (!overlapX) { 72 | float diffX = vert.x - a.pos.x; 73 | if ((vert.x >= a.getLeft() && vert.x <= a.getRight()) || (lastDiffX != 0 && std::signbit(lastDiffX) != std::signbit(diffX))) { 74 | overlapX = true; 75 | } 76 | lastDiffX = diffX; 77 | } 78 | if (!overlapY) { 79 | float diffY = vert.y - a.pos.y; 80 | if ((vert.y >= a.getBottom() && vert.y <= a.getTop()) || (lastDiffY != 0 && std::signbit(lastDiffY) != std::signbit(diffY))) { 81 | overlapY = true; 82 | } 83 | lastDiffY = diffY; 84 | } 85 | } 86 | 87 | return overlapX && overlapY; 88 | } 89 | 90 | bool Entity::intersects(Entity const& b) const { 91 | return intersectOneWay(*this, b) && intersectOneWay(b, *this); 92 | } 93 | 94 | 95 | /*using Vec2 = Vec2D; 96 | // Return normalized perpendicular axis from two points 97 | static Vec2 edgeToAxis(const Vec2D& p1, const Vec2& p2) { 98 | Vec2 edge = {p2.x - p1.x, p2.y - p1.y}; 99 | Vec2 axis = {-edge.y, edge.x}; // perpendicular 100 | 101 | float len = std::sqrt(axis.x * axis.x + axis.y * axis.y); 102 | if (len != 0.0f) { 103 | axis.x /= len; 104 | axis.y /= len; 105 | } 106 | return axis; 107 | } 108 | 109 | // Compute the four corners of a rectangle 110 | static void getCorners(const Entity& e, Vec2 outCorners[4]) { 111 | float hw = e.size.x * 0.5f; 112 | float hh = e.size.y * 0.5f; 113 | float rad = deg2rad(e.rotation); 114 | 115 | float cosA = std::cos(rad); 116 | float sinA = std::sin(rad); 117 | 118 | // Local unrotated corners 119 | Vec2 local[4] = { 120 | {-hw, -hh}, 121 | { hw, -hh}, 122 | { hw, hh}, 123 | {-hw, hh} 124 | }; 125 | 126 | // Rotate + translate 127 | for (int i = 0; i < 4; ++i) { 128 | outCorners[i].x = e.pos.x + (local[i].x * cosA - local[i].y * sinA); 129 | outCorners[i].y = e.pos.y + (local[i].x * sinA + local[i].y * cosA); 130 | } 131 | } 132 | 133 | // Project rectangle corners onto an axis 134 | static void projectOntoAxis(const Vec2 corners[4], const Vec2& axis, 135 | float& min, float& max) { 136 | min = max = corners[0].x * axis.x + corners[0].y * axis.y; 137 | for (int i = 1; i < 4; ++i) { 138 | float projection = corners[i].x * axis.x + corners[i].y * axis.y; 139 | if (projection < min) min = projection; 140 | if (projection > max) max = projection; 141 | } 142 | } 143 | 144 | // ------------------------------------------------------------ 145 | // Entity::collidesWith 146 | // ------------------------------------------------------------ 147 | bool Entity::intersects(const Entity& other) const { 148 | Vec2 cornersA[4]; 149 | Vec2 cornersB[4]; 150 | getCorners(*this, cornersA); 151 | getCorners(other, cornersB); 152 | 153 | // Axes to test: 2 unique from each rectangle 154 | Vec2 axes[4] = { 155 | edgeToAxis(cornersA[0], cornersA[1]), 156 | edgeToAxis(cornersA[1], cornersA[2]), 157 | edgeToAxis(cornersB[0], cornersB[1]), 158 | edgeToAxis(cornersB[1], cornersB[2]), 159 | }; 160 | 161 | // SAT check: if projections do NOT overlap on any axis -> no collision 162 | for (int i = 0; i < 4; ++i) { 163 | float minA, maxA, minB, maxB; 164 | projectOntoAxis(cornersA, axes[i], minA, maxA); 165 | projectOntoAxis(cornersB, axes[i], minB, maxB); 166 | 167 | if (maxA < minB || maxB < minA) 168 | return false; // Found separating axis 169 | } 170 | 171 | return true; // No separating axis found -> collision 172 | }*/ 173 | 174 | 175 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/Object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | /// Helper classes and functions to make assigning object IDs easier 16 | struct range : public std::pair { 17 | range(int i) : std::pair({i, i}) {} 18 | range(int i, int j) : std::pair({i, j}) {} 19 | }; 20 | 21 | std::vector unroll(std::vector ranges) { 22 | std::vector res; 23 | for (auto r : ranges) 24 | for (int i = r.first; i <= r.second; i++) 25 | res.push_back(i); 26 | return res; 27 | }; 28 | 29 | #define objs(x, type, w, h) \ 30 | for (auto& i : unroll x) \ 31 | if (id == i) \ 32 | return ObjectContainer(type({w, h}, std::move(ob))); 33 | 34 | std::optional Object::create(std::unordered_map&& ob) { 35 | auto id = std::stoi(ob[1]); 36 | 37 | objs(({ 38 | {1, 4}, {6, 7}, 63, {69, 72}, 39 | {74, 78}, {81, 83}, {90, 96}, 40 | {116, 119}, {121, 122}, 146, 41 | {160, 163}, {165, 169}, 173, 42 | 175, {207, 210}, {212, 213}, 43 | {247, 250}, {252, 258}, 44 | {260, 261}, {263, 265}, 45 | {267, 272}, {274, 275}, 46 | 467, {469, 471}, {1203, 1204}, 47 | {1209, 1210}, {1221, 1222}, 1226 48 | }), Block, 30, 30) 49 | objs(( 50 | { 64, 195, 206, 220, 661, 51 | {1155, 1157}, 1208, 1910 52 | }), Block, 15, 15) 53 | objs(({ 40, 147, 215, {369, 370}, {1903, 1905} }), Block, 30, 14) 54 | objs(({ { 170, 172 }, 174, 192 }), Block, 30, 21) 55 | objs(({ 468, 475, 1260 }), Block, 30, 1.5) 56 | objs(({ 62, 65, 66, 68 }), Block, 30, 16) 57 | objs(({ 1202, 1262 }), Block, 30, 3) 58 | objs(({ 1220, 1264 }), Block, 30, 6) 59 | objs(({ 196, 219, 1911 }), Block, 15, 8) 60 | objs(({ 204 }), Block, 8, 15) 61 | objs(({ 662, 663, 664 }), Block, 30, 15) 62 | objs(({ 1561 }), Block, 30, 10) 63 | objs(({ 1567 }), Block, 15, 10) 64 | objs(({ 1566 }), Block, 12, 12) 65 | objs(({ 1565 }), Block, 17, 17) 66 | objs(({ 1227 }), Block, 30, 7) 67 | objs(({ 328 }), Block, 22, 22) 68 | objs(({ 197 }), Block, 22, 21) 69 | objs(({ 194 }), Block, 21, 21) 70 | objs(({ 176 }), Block, 14, 21) 71 | objs(({ 1562 }), Block, 30, 2) 72 | objs(({ 1343 }), Block, 25, 3) 73 | objs(({ 1340 }), Block, 27, 2) 74 | objs(({ 34 }), Block, 37, 23) 75 | 76 | objs(({ 720, 991, 1731, 1733 }), Hazard, 2.40039063, 3.20001221) 77 | objs(({ 61, 446, 1719, 1728 }), Hazard, 9, 7.2) 78 | objs(({ 365, 667, 1716, 1730 }), Hazard, 9, 6) 79 | objs(({ 392, {458, 459} }), Hazard, 2.6, 4.8) 80 | objs(({ 8, 144, 177, 216 }), Hazard, 6, 12) 81 | objs(({ 103, 145, 218 }), Hazard, 4, 7.6) 82 | objs(({ 39, 205, 217 }), Hazard, 6, 5.6) 83 | objs(({ 768, 1727 }), Hazard, 4.5, 5.2) 84 | objs(({ 447, 1729 }), Hazard, 5.2, 7.2) 85 | objs(({ 135, 1711 }), Hazard, 14.1, 20) 86 | objs(({ 422, 1726 }), Hazard, 6, 4.4) 87 | objs(({ 244, 1721 }), Hazard, 6, 6.8) 88 | objs(({ 243, 1720 }), Hazard, 6, 7.2) 89 | objs(({ 421, 1725 }), Hazard, 9, 5.2) 90 | objs(({ 9, 1715 }), Hazard, 9, 10.8) 91 | objs(({ 989, 1732 }), Hazard, 9, 12) 92 | objs(({ 1714 }), Hazard, 11.4, 16.4) 93 | objs(({ 1712 }), Hazard, 13.5, 22.4) 94 | objs(({ 368, 1722 }), Hazard, 9, 4) 95 | objs(({ 1713 }), Hazard, 11.7, 20) 96 | objs(({ 178 }), Hazard, 6, 6.4) 97 | objs(({ 919 }), Hazard, 25, 6) 98 | objs(({ 179 }), Hazard, 4, 8) 99 | 100 | objs(({ 88, 186, 740, 1705 }), Sawblade, 32.3, 32.3) 101 | objs(({ 89, 1706 }), Sawblade, 21.6, 21.6) 102 | objs(({ 98 }), Sawblade, 12, 12) 103 | objs(({ 183 }), Sawblade, 15.660001, 15.660001) 104 | objs(({ 184 }), Sawblade, 20.4, 20.4) 105 | objs(({ 185 }), Sawblade, 2.8500001, 2.8500001) 106 | objs(({ 187, 741 }), Sawblade, 21.960001, 21.960001) 107 | objs(({ 188, 742 }), Sawblade, 12.6000004, 12.6000004) 108 | objs(({ 397, 1708 }), Sawblade, 28.9, 28.9) 109 | objs(({ 398, 1709 }), Sawblade, 17.44, 17.44) 110 | objs(({ 399, 1710 }), Sawblade, 12.900001, 12.900001) 111 | objs(({ 675, 1734 }), Sawblade, 32, 32) 112 | objs(({ 676, 1735 }), Sawblade, 17.5100002, 17.5100002) 113 | objs(({ 677, 1736 }), Sawblade, 12.479999, 12.479999) 114 | objs(({ 678 }), Sawblade, 30.4, 30.4) 115 | objs(({ 679 }), Sawblade, 18.54, 18.54) 116 | objs(({ 680 }), Sawblade, 10.8, 10.8) 117 | objs(({ 918 }), Sawblade, 24, 24) 118 | objs(({ 1582, 1583 }), Sawblade, 4, 4) 119 | objs(({ 1619 }), Sawblade, 25, 25) 120 | objs(({ 1620 }), Sawblade, 15, 15) 121 | objs(({ 1707 }), Sawblade, 12, 12) 122 | objs(({ {1701, 1703} }), Sawblade, 6, 6) 123 | 124 | objs(({ 35 }), Pad, 25, 4) 125 | objs(({ 140 }), Pad, 25, 5) 126 | objs(({ 67 }), Pad, 25, 6) 127 | objs(({ 36, 84, 141 }), Orb, 36, 36) 128 | 129 | objs(({ 12, 13, 47, 111 , 660 }), VehiclePortal, 34, 86) 130 | objs(({ 10, 11 }), GravityPortal, 25, 75) 131 | 132 | objs(({ 99, 101 }), SizePortal, 31, 90) 133 | 134 | objs(({ 143 }), BreakableBlock, 30, 30) 135 | 136 | objs(({ 200 }), SpeedPortal, 35, 44) 137 | objs(({ 201 }), SpeedPortal, 33, 56) 138 | objs(({ 202 }), SpeedPortal, 51, 56) 139 | objs(({ 203 }), SpeedPortal, 65, 56) 140 | objs(({ 1334 }), SpeedPortal, 69, 56) 141 | 142 | objs(({ 143 | 289, 294, 299, 305, 309, 315, 321, 326, 331, 337, 144 | 343, 349, 353, 371, 483, 492, 651, 665, 145 | 673, 709, 711, 726, 728, 886, 1338, 1341, 1344, 146 | 1723, 1743, 1745, 1747, 1749, 1906 147 | }), Slope, 30, 30) 148 | objs(({ 363, 1717 }), SlopeHazard, 30, 30); 149 | 150 | objs(({ 151 | 291, 295, 301, 307, 311, 317, 323, 327, 333, 339, 152 | 345, 351, 355, 367, 372, 484, 493, 652, 666, 153 | 674, 710, 712, 727, 729, 887, 1339, 1342, 1345, 154 | 1724, 1744, 1746, 1748, 1750, 1907 155 | }), Slope, 60, 30) 156 | objs(({ 364, 366, 1718 }), SlopeHazard, 60, 30); 157 | 158 | // Any block that isnt' defined is ignored 159 | return {}; 160 | } 161 | 162 | 163 | Object::Object(Vec2D s, std::unordered_map&& fields) { 164 | size = s; 165 | 166 | pos.x = stod_def(fields[2]); 167 | pos.y = stod_def(fields[3]); 168 | rotation = -stod_def(fields[6]); 169 | prio = 0; 170 | 171 | float scale = stod_def(fields[32], 1); 172 | float scalex = stod_def(fields[128], 1); 173 | float scaley = stod_def(fields[129], 1); 174 | 175 | size.x *= scale * scalex; 176 | size.y *= scale * scaley; 177 | } 178 | 179 | bool Object::touching(Player const& player) const { 180 | int r = std::abs(rotation); 181 | return intersects((r == 0 || r == 90 || r == 180 || r == 270) ? player.unrotatedHitbox() : (Entity&)player); 182 | } 183 | 184 | void Object::collide(Player&) const { 185 | // Unreachable 186 | abort(); 187 | } 188 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "pathfinder.hpp" 7 | #include 8 | 9 | using namespace geode::prelude; 10 | using namespace geode::utils::file; 11 | 12 | class PathfinderNode : public CCLayerColor { 13 | std::atomic_bool m_stop = false; 14 | std::atomic m_progress = 0; 15 | std::future> m_result; 16 | std::string m_levelName; 17 | public: 18 | static PathfinderNode* create(std::string const& levelName, std::string const& lvlString) { 19 | auto node = new PathfinderNode(); 20 | if (node && node->init(levelName, lvlString)) { 21 | node->autorelease(); 22 | return node; 23 | } 24 | CC_SAFE_DELETE(node); 25 | return nullptr; 26 | } 27 | 28 | ~PathfinderNode() { 29 | m_stop = true; 30 | if (m_result.valid()) 31 | m_result.get(); 32 | } 33 | 34 | void finalize(std::vector macro) { 35 | getChildByIDRecursive("stop")->setVisible(false); 36 | 37 | auto callback = [this, macro] { 38 | auto saveDir = Mod::get()->getSaveDir(); 39 | if (Loader::get()->isModLoaded("eclipse.eclipse-menu")) { 40 | saveDir = Loader::get()->getLoadedMod("eclipse.eclipse-menu")->getSaveDir() / "replays"; 41 | } 42 | 43 | if (!exists(saveDir)) { 44 | create_directories(saveDir); 45 | } 46 | 47 | pick(PickMode::SaveFile, { 48 | saveDir / fmt::format("{}.gdr2", m_levelName), 49 | {{ 50 | std::string("Macro File"), 51 | std::unordered_set {std::string("gdr2")} 52 | }} 53 | }).listen([this, macro](auto path) { 54 | if (path->isOk()) { 55 | (void)writeBinary(path->unwrap(), macro); 56 | removeFromParentAndCleanup(true); 57 | } 58 | }, [](auto) {}, []() {}); 59 | }; 60 | 61 | Build::create("Export", "bigFont.fnt", "GJ_button_01.png") 62 | .intoMenuItem(callback) 63 | .scale(0.8) 64 | .move(0, -40) 65 | .parent(getChildByID("menu")); 66 | } 67 | 68 | void keyBackClicked() override { 69 | m_stop = true; 70 | CCLayer::keyBackClicked(); 71 | removeFromParentAndCleanup(true); 72 | } 73 | 74 | bool init(std::string const& levelName, std::string const& lvlString) { 75 | CCLayerColor::initWithColor({0, 0, 0, 100}); 76 | setCascadeOpacityEnabled(true); 77 | 78 | m_levelName = levelName; 79 | 80 | m_result = std::async(std::launch::async, [lvlString, this]() { 81 | try { 82 | return pathfind(lvlString, m_stop, [this](double progress) { 83 | if (m_progress < progress) 84 | m_progress = progress; 85 | }); 86 | } catch (std::exception& e) { 87 | log::error("{}", e.what()); 88 | return std::vector(); 89 | } 90 | }); 91 | 92 | setKeypadEnabled(true); 93 | 94 | Build(this).initTouch().schedule([this](float) { 95 | Build(this).intoChildRecurseID("percent") 96 | .string(fmt::format("{:.2f}%", m_progress).c_str()); 97 | 98 | if (m_result.valid() && m_result.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { 99 | finalize(m_result.get()); 100 | } 101 | }); 102 | 103 | auto handle = [this](CCMenuItemSpriteExtra* it) { 104 | m_stop = true; 105 | 106 | if (it->getID() == "stop") 107 | finalize(m_result.get()); 108 | else 109 | removeFromParentAndCleanup(true); 110 | }; 111 | 112 | auto menu = Build::create().parent(this).id("menu").children( 113 | Build::create("GJ_square02.png") 114 | .contentSize(250, 140), 115 | Build::create("Pathfinding...", "bigFont.fnt") 116 | .move(0, 50) 117 | .scale(0.8), 118 | Build::create("0.00", "chatFont.fnt") 119 | .id("percent") 120 | .move(0, 10), 121 | Build::create("Stop", "bigFont.fnt", "GJ_button_04.png") 122 | .scale(0.8) 123 | .intoMenuItem(handle) 124 | .id("stop") 125 | .move(0, -40), 126 | Build::createSpriteName("GJ_closeBtn_001.png") 127 | .intoMenuItem(handle) 128 | .id("close") 129 | .move(-125, 70) 130 | .scale(0.8) 131 | ); 132 | 133 | /* .intoNewChild(CCMenu::create()) 134 | .id("menu") 135 | .intoNewChild(CCScale9Sprite::create("GJ_square04.png")) 136 | .contentSize(250, 140) 137 | .intoNewSibling(CCLabelBMFont::create("Pathfinding...", "bigFont.fnt")) 138 | .move(0, 50) 139 | .scale(0.8) 140 | .intoNewSibling(CCLabelBMFont::create("0.00", "chatFont.fnt")) 141 | .id("percent") 142 | .move(0, 10) 143 | .intoNewSibling(ButtonSprite::create("Stop", "bigFont.fnt", "GJ_button_04.png")) 144 | .intoMenuItem([this]() { 145 | m_stop = true; 146 | finalize(m_result.get()); 147 | }) 148 | .scale(0.8) 149 | .id("cancel") 150 | .move(0, -40) 151 | .intoNewSibling(CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png")) 152 | .intoMenuItem([this]() { 153 | m_stop = true; 154 | this->removeFromParentAndCleanup(true); 155 | }) 156 | .move(-125, 70) 157 | .scale(0.8);*/ 158 | ; 159 | 160 | return true; 161 | } 162 | 163 | }; 164 | 165 | class $modify(EditLevelLayer) { 166 | bool init(GJGameLevel* p0) { 167 | EditLevelLayer::init(p0); 168 | 169 | auto btn = Build::create( 170 | CCSprite::create("pathfinder.png"_spr), 171 | BaseType::Circle, 172 | 4, 173 | 3 174 | ).scale(0.8); 175 | 176 | btn->setTopRelativeScale(1.4); 177 | 178 | btn.intoMenuItem([this]() { 179 | auto lvlString = ZipUtils::decompressString(m_level->m_levelString, true, 0); 180 | Build::create(m_level->m_levelName, lvlString).parent(this).zOrder(100); 181 | }).id("pathfinder-button") 182 | .intoNewParent(CCMenu::create()) 183 | .parent(this) 184 | .id("pathfinder-menu") 185 | .matchPos(getChildByIDRecursive("delete-button")) 186 | .move(-45, 0); 187 | 188 | 189 | return true; 190 | } 191 | }; 192 | 193 | class $modify(LevelInfoLayer) { 194 | bool init(GJGameLevel* level, bool challenge) { 195 | LevelInfoLayer::init(level, challenge); 196 | 197 | auto btn = Build::create( 198 | CCSprite::create("pathfinder.png"_spr), 199 | BaseType::Circle, 200 | 4, 201 | 3 202 | ).scale(0.8); 203 | 204 | btn->setTopRelativeScale(1.4); 205 | 206 | btn.intoMenuItem([this]() { 207 | auto lvlString = ZipUtils::decompressString(m_level->m_levelString, true, 0); 208 | Build::create(m_level->m_levelName, lvlString).parent(this).zOrder(100); 209 | }).id("pathfinder-button") 210 | .parent(getChildByID("other-menu")) 211 | .matchPos(getChildByIDRecursive("list-button")) 212 | .move(0, 45); 213 | 214 | return true; 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /src/debug.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace geode::prelude; 5 | using namespace geode::utils::file; 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "subprocess.hpp" 14 | 15 | #ifndef DEBUG_MODE 16 | #include 17 | #include 18 | #endif 19 | 20 | std::vector s_realPoints; 21 | std::vector s_simPoints; 22 | std::vector s_inputPoints; 23 | std::string realTxt = ""; 24 | static int frames = 0; 25 | 26 | class Replay2 : public gdr::Replay> { 27 | public: 28 | Replay2() : Replay("Pathfinder", 1){} 29 | }; 30 | 31 | 32 | #ifdef _WIN32 33 | #include 34 | #include 35 | #include 36 | 37 | std::string pathToUtf8(std::filesystem::path const& path) { 38 | std::wstring wstr = path.wstring(); 39 | int count = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), wstr.length(), NULL, 0, NULL, NULL); 40 | std::string str(count, 0); 41 | WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &str[0], count, NULL, NULL); 42 | return str; 43 | } 44 | #endif 45 | 46 | 47 | void runTestSim(std::string const& level, std::filesystem::path const& path) { 48 | std::ifstream macro(path, std::ios::binary); 49 | std::vector data((std::istreambuf_iterator(macro)), std::istreambuf_iterator()); 50 | 51 | auto replay = Replay2::importData(data); 52 | 53 | if (replay.isErr()) 54 | return; 55 | auto inputs = replay.unwrap().inputs; 56 | 57 | std::string encoded = "0"; 58 | bool currentHold = false; 59 | 60 | int maxFrame = inputs.back().frame; 61 | for (int i = 1; i < maxFrame; ++i) { 62 | if (i == inputs.front().frame) { 63 | currentHold = inputs.front().down; 64 | inputs.erase(inputs.begin()); 65 | } 66 | 67 | encoded += currentHold ? '1' : '0'; 68 | } 69 | 70 | auto lvlFile = Mod::get()->getSaveDir() / ".lvl"; 71 | auto inpFile = Mod::get()->getSaveDir() / ".inp"; 72 | 73 | 74 | writeString(lvlFile, level).unwrap(); 75 | writeString(inpFile, encoded).unwrap(); 76 | 77 | std::string outbuf; 78 | 79 | #if DEBUG_MODE 80 | try { 81 | #if _WIN32 82 | auto dir = std::filesystem::path(__FILE__).parent_path().parent_path() / "build" / "gd-sim" / "Debug" / "gd-sim-test.exe"; 83 | const auto out = subprocess::check_output({ 84 | pathToUtf8(dir), 85 | pathToUtf8(lvlFile), 86 | pathToUtf8(inpFile) 87 | }); 88 | #else 89 | auto dir = std::filesystem::path(__FILE__).parent_path().parent_path() / "build" / "gd-sim" / "gd-sim-test"; 90 | const auto out = subprocess::check_output({dir, lvlFile, inpFile}); 91 | #endif 92 | 93 | outbuf = std::string(&out.buf[0]); 94 | } catch (const subprocess::CalledProcessError& e) { 95 | log::error("{}", e.what()); 96 | return; 97 | } 98 | #else 99 | { 100 | std::stringstream stream; 101 | std::streambuf* cout_orig = std::cout.rdbuf(); 102 | std::cout.rdbuf(stream.rdbuf()); 103 | 104 | Level lvl(level); 105 | lvl.debug = true; 106 | for (size_t i = 2; i < encoded.size(); ++i) { 107 | auto state = lvl.runFrame(encoded[i] == '1'); 108 | 109 | if (state.dead) { 110 | std::cout << "Macro failed at frame " << lvl.currentFrame() << std::endl; 111 | break; 112 | } 113 | } 114 | 115 | std::cout.rdbuf(cout_orig); 116 | 117 | outbuf = stream.str(); 118 | } 119 | #endif 120 | 121 | if (!writeString(Mod::get()->getSaveDir() / "sim.txt", outbuf).isOk()) { 122 | log::error("Failed to write to {}", Mod::get()->getSaveDir() / "sim.txt"); 123 | return; 124 | } 125 | 126 | std::stringstream ss; 127 | s_simPoints.clear(); 128 | s_inputPoints.clear(); 129 | 130 | for (auto& i : outbuf) { 131 | ss << i; 132 | if (i == '\n') { 133 | std::string line = ss.str(); 134 | ss.str(""); 135 | 136 | if (line.find("Frame") == 0) { 137 | int frame; 138 | float x, y; 139 | if (sscanf(line.c_str(), "Frame %d X %f Y %f", &frame, &x, &y) == 3) { 140 | s_simPoints.push_back(ccp(x, y + 105)); 141 | } 142 | } else if (line.find("Input") == 0) { 143 | float x, y; 144 | if (sscanf(line.c_str(), "Input X %f Y %f", &x, &y) == 2) { 145 | s_inputPoints.push_back(ccp(x, y + 105)); 146 | } 147 | } 148 | } 149 | } 150 | /*} catch (const subprocess::CalledProcessError& e) { 151 | log::error("{}", e.what()); 152 | }*/ 153 | } 154 | 155 | class $modify(EditorPauseLayer) { 156 | void customSetup() { 157 | EditorPauseLayer::customSetup(); 158 | 159 | if (true) 160 | Loader::get()->queueInMainThread([this]() { 161 | auto guide = Build(this).intoChildByID("guidelines-menu").collect(); 162 | if (!guide) 163 | return; 164 | Build::createSpriteName("GJ_createLinesBtn_001.png") 165 | .scale(0.7) 166 | .intoMenuItem([]() { 167 | CCDrawNode* node = (CCDrawNode*)LevelEditorLayer::get()->m_objectLayer->getChildByID("pathfind-node"); 168 | node->clear(); 169 | CCPoint start = ccp(0, 105); 170 | for (auto& i : s_simPoints) { 171 | node->drawSegment(start, i, 1, ccc4f(0, 1, 0, 1)); 172 | start = i; 173 | } 174 | 175 | start = ccp(0, 105); 176 | for (auto& i : s_realPoints) { 177 | node->drawSegment(start, i, 1, ccc4f(1, 0, 0, 1)); 178 | start = i; 179 | } 180 | 181 | bool k = false; 182 | for (auto& i : s_inputPoints) { 183 | node->drawDot(i, 2, ccc4f(k ? 1 : 0, 0.5, k ? 0 : 1, 1)); 184 | k = !k; 185 | } 186 | }) 187 | .parent(guide); 188 | 189 | guide->updateLayout(); 190 | }); 191 | } 192 | }; 193 | 194 | class $modify(EditLevelLayer) { 195 | bool init(GJGameLevel* p0) { 196 | EditLevelLayer::init(p0); 197 | 198 | auto btn = Build::create( 199 | CCSprite::create("pathfinder.png"_spr), 200 | BaseType::Circle, 201 | 4, 202 | 2 203 | ).scale(0.8); 204 | 205 | btn->setTopRelativeScale(1.4); 206 | 207 | btn.intoMenuItem([this]() { 208 | auto lvlString = ZipUtils::decompressString(m_level->m_levelString, true, 0); 209 | pick(PickMode::OpenFile, {}).listen([lvlString](auto path) { 210 | if (*path) 211 | runTestSim(lvlString, path->unwrap()); 212 | }, [](auto) {}, []() {}); 213 | }).id("pathfinder-debug-button") 214 | .intoNewParent(CCMenu::create()) 215 | .parent(this) 216 | .id("pathfinder-debug-menu") 217 | .matchPos(getChildByIDRecursive("delete-button")) 218 | .move(-45, -50); 219 | 220 | 221 | return true; 222 | } 223 | }; 224 | 225 | class $modify(LevelEditorLayer) { 226 | bool init(GJGameLevel* lvl, bool p1) { 227 | LevelEditorLayer::init(lvl, p1); 228 | (void)file::writeString(Mod::get()->getSaveDir() / "real.txt", realTxt); 229 | 230 | auto b = CCDrawNode::create(); 231 | b->setID("pathfind-node"); 232 | m_objectLayer->addChild(b); 233 | 234 | #if DEBUG_MODE 235 | CCPoint start = ccp(0, 105); 236 | for (auto& i : s_simPoints) { 237 | b->drawSegment(start, i, 1, ccc4f(0, 1, 0, 1)); 238 | start = i; 239 | } 240 | 241 | start = ccp(0, 105); 242 | for (auto& i : s_realPoints) { 243 | b->drawSegment(start, i, 1, ccc4f(1, 0, 0, 1)); 244 | start = i; 245 | } 246 | 247 | bool k = false; 248 | for (auto& i : s_inputPoints) { 249 | b->drawDot(i, 2, ccc4f(k ? 1 : 0, 0.5, k ? 0 : 1, 1)); 250 | k = !k; 251 | } 252 | #endif 253 | 254 | return true; 255 | } 256 | }; 257 | 258 | class $modify(GJBaseGameLayer) { 259 | void updateCamera(float dt) { 260 | GJBaseGameLayer::updateCamera(1 / 4.); 261 | 262 | static double prevVel = 0; 263 | static double prevX = 0; 264 | static double prevXVel = 0; 265 | 266 | double vel = m_player1->m_yVelocity * 60 * 0.9 * (m_player1->m_isUpsideDown ? -1 : 1); 267 | //log::info("{}", reference_cast(m_player1->m_yVelocity)); 268 | double xvel = (m_player1->getPositionX() - prevX); 269 | 270 | auto dat = fmt::format("Frame {} X {:.8f} Y {:.8f} Vel {:.8f} Accel {:.8f} Rot {:.8f}", frames, m_player1->getPositionX(), m_player1->getPositionY() - 105, vel, (vel - prevVel) * 240, m_player1->getRotation()); 271 | 272 | #if DEBUG_MODE 273 | log::info("{}", dat); 274 | #endif 275 | 276 | frames++; 277 | prevVel = vel; 278 | prevXVel = xvel; 279 | prevX = m_player1->getPositionX(); 280 | 281 | if (PlayLayer::get()) { 282 | realTxt += dat + "\n"; 283 | s_realPoints.push_back(m_player1->getPosition()); 284 | } 285 | } 286 | }; 287 | class $modify(PlayLayer) { 288 | void resetLevel() { 289 | PlayLayer::resetLevel(); 290 | realTxt = ""; 291 | s_realPoints.clear(); 292 | frames = 0; 293 | } 294 | }; 295 | -------------------------------------------------------------------------------- /gd-sim/src/Objects/Slope.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | /// Slopes are possibly the most complicated object. This is still very unfinished! 7 | 8 | Slope::Slope(Vec2D size, std::unordered_map&& fields) : Block(size, std::move(fields)) { 9 | auto rot = stod_def(fields[6].c_str()); 10 | 11 | bool flipX = atoi(fields[4].c_str()) == 1; 12 | bool flipY = atoi(fields[5].c_str()) == 1; 13 | 14 | orientation = rot / 90; 15 | 16 | // Slope orientation matters a lot lot 17 | if (flipX && flipY) 18 | orientation += 2; 19 | else if (flipX) 20 | orientation += 1; 21 | else if (flipY) 22 | orientation += 3; 23 | 24 | orientation = orientation % 4; 25 | if (orientation < 0) orientation += 4; 26 | 27 | rotation = 0; 28 | } 29 | 30 | bool Slope::isFacingUp() const { 31 | return orientation < 2; 32 | } 33 | 34 | int Slope::gravOrient(Player const& p) const { 35 | int orient = orientation; 36 | 37 | if (p.upsideDown) { 38 | if (orient == 3) 39 | orient = 0; 40 | else if (orient == 2) 41 | orient = 1; 42 | else if (orient == 0) 43 | orient = 3; 44 | else if (orient == 1) 45 | orient = 2; 46 | } 47 | 48 | return orient; 49 | } 50 | 51 | double Slope::angle() const { 52 | auto ang = std::atan(size.y / size.x); 53 | // Downhill slopes 54 | if (orientation == 1 || orientation == 3) 55 | ang = -ang; 56 | return ang; 57 | } 58 | 59 | double Slope::expectedY(Player const& p) const { 60 | double ydist = (isFacingUp() ? 1 : -1) * (p.size.y / 2.f) / cosf(angle()); 61 | float posRelative = (size.y / size.x) * (p.pos.x - getLeft()); 62 | 63 | // Uphill vs downhill 64 | if (angle() > 0) 65 | return getBottom() + std::min(posRelative + ydist, size.y + p.size.y / 2.0); 66 | else 67 | return getTop() - std::min(posRelative - ydist, size.y + p.size.y / 2.0); 68 | } 69 | 70 | /// See Slope.hpp for why this is a separate function 71 | void Slope::calc(Player& p) const { 72 | if (gravOrient(p.prevPlayer()) == 0) { 73 | // Regular uphill slope 74 | 75 | // Coyote frame for slopes must be taken into account 76 | if (!touching(p)) { 77 | p.actions.push_back(+[](Player& p) { 78 | p.slopeData.slope = {}; 79 | p.slopeData.elapsed = 0.0; 80 | p.slopeData.snapDown = false; 81 | }); 82 | } 83 | 84 | // If player isn't on top already, use expectedY to snap player 85 | if (p.gravBottom(p.prevPlayer()) != getTop()) { 86 | if (p.prevPlayer().upsideDown) { 87 | p.pos.y = std::min((double)p.pos.y, expectedY(p)); 88 | } else { 89 | p.pos.y = std::max((double)p.pos.y, expectedY(p)); 90 | } 91 | } 92 | 93 | // When you're on top of the slope you will be ejected 94 | if (p.grounded && (p.gravBottom(p) == p.gravTop(*this) || (p.gravBottom(p) > p.gravTop(*this) && p.snapData.playerFrame > 0))) { 95 | 96 | // Rob's algorithm for slope ejection velocity. So goofy! 97 | double vel = 0.9 * std::min(1.12 / p.grav(angle()), 1.54) * (size.y * player_speeds[p.speed] / size.x); 98 | double time = std::clamp(10 * (p.timeElapsed - p.slopeData.elapsed), 0.4, 1.0); 99 | 100 | if (p.vehicle.type == VehicleType::Ball || p.vehicle.type == VehicleType::Ship) 101 | vel *= 0.75; 102 | if (p.vehicle.type == VehicleType::Ufo) 103 | vel *= 0.7499; // I have no justification for this. It just works 104 | 105 | vel *= time; 106 | 107 | // Gotta eject on the next frame 108 | p.actions.push_back([vel](Player& p) { 109 | p.velocity = roundVel(vel, p.upsideDown); 110 | 111 | p.slopeData.slope = {}; 112 | p.slopeData.elapsed = 0; 113 | p.slopeData.snapDown = false; 114 | }); 115 | } 116 | } else if (gravOrient(p.prevPlayer()) == 1) { 117 | // Downhill regular slope 118 | 119 | // Velocity up means you're not on slope anymore 120 | if (p.velocity > 0) { 121 | p.actions.push_back(+[](Player& p) { 122 | p.slopeData.slope = {}; 123 | p.slopeData.elapsed = 0; 124 | p.slopeData.snapDown = false; 125 | }); 126 | } 127 | 128 | // Snap to expected Y just like uphill 129 | if (p.gravBottom(p.prevPlayer()) != getTop() || p.slopeData.snapDown) { 130 | // just in case 131 | p.pos.y = std::max(p.pos.y, expectedY(p.prevPlayer())); 132 | 133 | p.pos.y = std::max(std::min((double)p.pos.y, expectedY(p)), pos.y - p.size.y / 2.); 134 | } 135 | 136 | // Ejections, but downwards! 137 | if (p.getTop() <= pos.y) { 138 | // TODO use the actual algorithm because there are probably wrong 139 | static double falls[4] = { 140 | 226.044054, 141 | 280.422108, 142 | 348.678108, 143 | 421.200108 144 | }; 145 | 146 | double vel = -falls[p.speed] * (size.y / size.x); 147 | p.velocity = 0; 148 | p.actions.push_back([vel](Player& p) { 149 | p.velocity = vel; 150 | p.slopeData.slope = {}; 151 | p.slopeData.elapsed = 0; 152 | p.slopeData.snapDown = false; 153 | }); 154 | } 155 | } else if (gravOrient(p.prevPlayer()) == 2) { 156 | if (p.velocity < 0) { 157 | p.actions.push_back(+[](Player& p) { 158 | p.slopeData.slope = {}; 159 | p.slopeData.elapsed = 0; 160 | p.slopeData.snapDown = false; 161 | }); 162 | return; 163 | } 164 | 165 | p.velocity = 0; 166 | 167 | if (p.grav(p.pos.y) < p.gravTop(*this)) { 168 | p.pos.y = p.grav(std::max(p.grav(p.pos.y), p.grav(expectedY(p)))); 169 | } 170 | 171 | if (p.grav(p.pos.y) >= p.gravTop(*this)) { 172 | p.pos.y = p.grav(p.gravTop(*this)); 173 | p.velocity = roundVel(p.prevPlayer().acceleration * p.dt, p.prevPlayer().upsideDown); 174 | 175 | p.actions.push_back(+[](Player& p) { 176 | p.slopeData.slope = {}; 177 | p.slopeData.elapsed = 0; 178 | p.slopeData.snapDown = false; 179 | }); 180 | } 181 | } 182 | } 183 | 184 | void Slope::collide(Player& p) const { 185 | p.potentialSlopes.push_back(this); 186 | 187 | if (orientation < 2 && expectedY(p) <= p.pos.y) 188 | return; 189 | else if (orientation >= 2 && expectedY(p) >= p.pos.y) 190 | return; 191 | else if (p.vehicle.type == VehicleType::Cube && p.gravTop(p) - p.gravBottom(*this) < 16) 192 | return; 193 | 194 | // No slope calculations for you! 195 | if (p.vehicle.type == VehicleType::Wave) { 196 | p.dead = true; 197 | return; 198 | } 199 | 200 | // When you hit a downhill slope before your center hits the leftmost side, it's treated like a block 201 | if (!p.prevPlayer().slopeData.slope && gravOrient(p) == 1 && p.velocity <= 0 && p.pos.x - getLeft() < 0) { 202 | p.pos.y = p.grav(p.gravTop(*this) + p.size.y / 2.); 203 | p.grounded = true; 204 | return; 205 | } 206 | 207 | if (!p.prevPlayer().slopeData.slope && gravOrient(p) == 2 && p.velocity >= 0 && p.pos.x - getLeft() < 0) { 208 | p.pos.y = p.grav(p.gravBottom(*this) - p.size.y / 2.); 209 | p.velocity = 0; 210 | return; 211 | } 212 | 213 | // Current (or previous) slope 214 | auto pSlope = p.slopeData.slope; 215 | 216 | /* 217 | If stored slope data is current slope, or there is no stored, 218 | or you're no longer touching the previous slope. 219 | */ 220 | if (!pSlope || !pSlope->touching(p) || (pSlope->gravOrient(p) == gravOrient(p) && p.grav(expectedY(p)) > p.grav(pSlope->expectedY(p))) || pSlope->id == id) { 221 | bool hasSlope = p.prevPlayer().slopeData.slope.has_value(); 222 | 223 | // Is player traveling at the right angle to contact the slope 224 | double pAngle = atan((p.prevPlayer().velocity * p.dt) / (player_speeds[p.speed] * p.dt)); 225 | if (gravOrient(p.prevPlayer()) > 1) 226 | pAngle = -pAngle; 227 | 228 | 229 | bool projectedHit = orientation == 1 ? (pAngle * 5.0 <= angle()) : (pAngle <= angle()); 230 | 231 | // Downhill slopes attach you to the slope faster uphill 232 | //float expAdjust = expectedY(p) + (angle() > 0 ? 0 : 2); 233 | bool clip = true;//isFacingUp() ? (p.grav(expAdjust) >= p.grav(p.pos.y)) : (p.grav(expAdjust) <= p.grav(p.pos.y)); 234 | 235 | // Downhill slopes snap you down 236 | bool snapDown = orientation == 1 && p.velocity > 0 && p.pos.x - getLeft() > 0; 237 | 238 | if (hasSlope ? p.velocity <= 0 : projectedHit & clip || snapDown) { 239 | p.grounded = true; 240 | p.slopeData.slope = *this; 241 | 242 | if (snapDown && !hasSlope) { 243 | p.velocity = 0; 244 | p.pos.y = getTop() + p.size.y / 2; 245 | p.slopeData.snapDown = true; 246 | } 247 | 248 | if (!p.slopeData.elapsed) 249 | p.slopeData.elapsed = p.prevPlayer().timeElapsed; 250 | } 251 | } 252 | } 253 | 254 | void SlopeHazard::collide(Player& p) const { 255 | if (orientation < 2 && expectedY(p) <= p.pos.y) 256 | return; 257 | else if (orientation >= 2 && expectedY(p) >= p.pos.y) 258 | return; 259 | 260 | p.dead = true; 261 | } 262 | double SlopeHazard::expectedY(Player const& p) const { 263 | // Hazardous slopes have slightly larger hitboxes 264 | return Slope::expectedY(p) + (orientation > 1 ? -4 : 4); 265 | } 266 | 267 | bool SlopeHazard::touching(Player const& p) const { 268 | Entity hitbox = p.unrotatedHitbox(); 269 | hitbox.size.y += 8; 270 | if (!intersects(hitbox)) 271 | return false; 272 | 273 | switch (orientation) { 274 | case 0: 275 | return expectedY(p) > p.pos.y; 276 | case 1: 277 | return expectedY(p) > p.pos.y; 278 | case 2: 279 | return expectedY(p) < p.pos.y; 280 | case 3: 281 | return expectedY(p) < p.pos.y; 282 | default: 283 | return false; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /gd-sim/src/Vehicle.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | /* 10 | For ship and ufo, there are two sets of acceleration values depending on the current 11 | velocity. If the current velocity is higher than one of these thresholds 12 | (indexed by speed) then a lighter acceleration would be used. 13 | */ 14 | constexpr double velocity_thresholds[] = { 15 | 101.541492, 16 | 103.485494592, 17 | 103.377492, 18 | 103.809492, 19 | 103.809492 20 | }; 21 | 22 | /// When the cube lands, it's rotation is snapped to whichever 90 degree angle is closest 23 | float normalizeRotation(Player const& p, float angle) { 24 | float playerRotation = (int)p.rotation % 360; 25 | 26 | float diff = std::fmod(playerRotation - angle, 90.0f); 27 | if (diff < 0) 28 | diff += 90.0f; 29 | 30 | if (std::abs(playerRotation - angle) < std::abs(diff)) 31 | return angle; 32 | else 33 | return playerRotation - diff; 34 | } 35 | 36 | /// Ship, UFO, Wave all have the same basic mechanism for rotation 37 | void rotateFly(Player& p, float mult) { 38 | auto diff = p.pos - p.prevPlayer().pos; 39 | 40 | // Unknown why this happens 41 | if (p.dt * 72 <= std::pow(diff.x, 2) + std::pow(diff.y, 2)) { 42 | p.rotation = slerp(p.rotation * 0.017453292f, atan2(diff.y, diff.x), (p.dt * 60) * mult) * 57.29578f; 43 | } 44 | } 45 | 46 | Vehicle cube() { 47 | Vehicle v; 48 | v.type = VehicleType::Cube; 49 | 50 | v.enter = +[](Player& p) { 51 | if (p.prevPlayer().vehicle.type != VehicleType::Ball) 52 | p.velocity = p.velocity / 2; 53 | 54 | if (p.prevPlayer().vehicle.type == VehicleType::Wave) 55 | p.velocity = p.velocity / 2; 56 | 57 | if (p.prevPlayer().vehicle.type == VehicleType::Ship && p.input) 58 | p.buffer = true; 59 | }; 60 | 61 | v.clamp = +[](Player& p) { 62 | if (p.velocity < -810) 63 | p.velocity = -810; 64 | 65 | if (p.gravTop(p.innerHitbox()) >= p.gravCeiling()) 66 | p.dead = true; 67 | }; 68 | 69 | v.update = +[](Player& p) { 70 | // Fall speeds 71 | static double accelerations[] = { 72 | -2747.52, 73 | -2794.1082, 74 | -2786.4, 75 | -2799.36, 76 | -2799.36 77 | }; 78 | p.acceleration = accelerations[p.speed]; 79 | 80 | // Strange hardcode 81 | if (p.gravityPortal && p.grav(p.velocity) > 350 && p.speed > 1) { 82 | p.acceleration -= 6.48; 83 | } 84 | 85 | //TODO add rotation 86 | p.rotation = 0; 87 | 88 | bool jump = false; 89 | 90 | if (p.grounded) { 91 | if (p.input) { 92 | jump = true; 93 | } else { 94 | p.setVelocity(0, true); 95 | } 96 | p.buffer = false; 97 | } 98 | 99 | if (p.upsideDown && p.input && p.coyoteFrames < 10) { 100 | jump = true; 101 | p.buffer = false; 102 | } 103 | 104 | if (jump) { 105 | static double jumpHeights[] = { 106 | 573.481728, 107 | 603.7217172, 108 | 616.681728, 109 | 606.421728, 110 | 606.421728 111 | }; 112 | 113 | // On slopes, you jump higher depending on how long you've been on the slope 114 | if (p.slopeData.slope && p.slopeData.slope->orientation == 0) { 115 | auto time = std::clamp(10 * (p.timeElapsed - p.slopeData.elapsed), 0.4, 1.0); 116 | double vel = 0.9 * std::min(1.12 / p.slopeData.slope->angle(), 1.54) * (p.slopeData.slope->size.y * player_speeds[p.speed] / p.slopeData.slope->size.x); 117 | p.setVelocity(0.25 * time * vel + jumpHeights[p.speed], p.prevPlayer().input); 118 | 119 | //p.velocity = std::floor(1000 * p.velocity / 54.) * 54 / 1000.; 120 | p.grounded = false; 121 | } else { 122 | p.setVelocity(jumpHeights[p.speed], p.prevPlayer().input); 123 | p.grounded = false; 124 | } 125 | } 126 | }; 127 | 128 | v.bounds = FLT_MAX; 129 | 130 | return v; 131 | } 132 | 133 | Vehicle ship() { 134 | Vehicle v; 135 | v.type = VehicleType::Ship; 136 | 137 | v.enter = +[](Player& p) { 138 | if (p.prevPlayer().vehicle.type == VehicleType::Ufo || p.prevPlayer().vehicle.type == VehicleType::Wave) 139 | p.velocity = p.velocity / 4.0; 140 | else 141 | p.velocity = p.velocity / 2.0; 142 | }; 143 | 144 | v.clamp = +[](Player& p) { 145 | // Can't buffer clicks on ship 146 | p.buffer = false; 147 | 148 | // Max velocity 149 | p.velocity = std::clamp(p.velocity, 150 | p.small ? -406.566 : -345.6, 151 | p.small ? 508.248 : 432.0 152 | ); 153 | 154 | //TODO deal with slope velocity more consistently 155 | 156 | // Slopes complicate things like "maximum velocity" 157 | /*if (p.input) 158 | p.velocity = std::min(p.velocity, p.small ? 508.248 : 432.0); 159 | else 160 | p.velocity = std::max(p.velocity, p.small ? -406.566 : -345.6);*/ 161 | 162 | 163 | if (p.gravTop(p) > p.gravCeiling()) { 164 | if (p.velocity > 0) { 165 | p.setVelocity(0, false); 166 | } 167 | p.pos.y = p.grav(p.gravCeiling()) - p.grav(p.size.y / 2); 168 | } 169 | }; 170 | 171 | v.update = +[](Player& p) { 172 | p.buffer = false; 173 | 174 | if (p.grounded) 175 | p.setVelocity(0, !p.input); 176 | 177 | if (p.input) { 178 | if (p.velocity <= p.grav(velocity_thresholds[p.speed])) 179 | p.acceleration = p.small ? 1643.5872 : 1397.0491; 180 | else 181 | p.acceleration = p.small ? 1314.86976 : 1117.64328; 182 | } else { 183 | if (p.velocity >= p.grav(velocity_thresholds[p.speed])) 184 | p.acceleration = p.small ? -1577.85408 : -1341.1719; 185 | else 186 | p.acceleration = p.small ? -1051.8984 : -894.11464; 187 | } 188 | 189 | if (p.grav(p.pos.y) >= p.gravCeiling()) { 190 | p.setVelocity(0, false); 191 | } 192 | 193 | rotateFly(p, 0.15f); 194 | }; 195 | 196 | v.bounds = 300; 197 | 198 | return v; 199 | } 200 | 201 | Vehicle ball() { 202 | Vehicle v; 203 | 204 | v.type = VehicleType::Ball; 205 | v.clamp = +[](Player& p) { 206 | if (p.velocity >= 810) 207 | p.velocity = 810; 208 | if (p.velocity <= -810) 209 | p.velocity = -810; 210 | 211 | if (p.grav(p.pos.y) >= p.gravCeiling() && p.velocity > 0) { 212 | p.setVelocity(0, true); 213 | 214 | if (p.input) 215 | p.upsideDown = !p.upsideDown; 216 | } 217 | }; 218 | 219 | v.enter = +[](Player& p) { 220 | if (p.input) 221 | p.vehicleBuffer = true; 222 | 223 | switch (p.prevPlayer().vehicle.type) { 224 | case VehicleType::Ship: 225 | case VehicleType::Ufo: 226 | p.velocity = p.velocity / 2; 227 | break; 228 | default: break; 229 | } 230 | }; 231 | 232 | v.update = +[](Player& p) { 233 | if (!p.prevPlayer().velocityOverride || p.prevPlayer().slopeData.slope) 234 | p.acceleration = -1676.46672; 235 | 236 | if (!p.input) 237 | p.vehicleBuffer = false; 238 | 239 | bool jump = false; 240 | 241 | if (p.grounded) { 242 | //p.rotVelocity = p.grav(600); 243 | if (p.input && (p.prevPlayer().buffer || !p.prevPlayer().input || p.vehicleBuffer)) { 244 | jump = true; 245 | } else { 246 | p.setVelocity(0, true); 247 | } 248 | p.buffer = false; 249 | } else if (p.buffer && p.coyoteFrames < (p.upsideDown ? 16 : 1)) { 250 | jump = true; 251 | } 252 | 253 | if (jump) { 254 | static double jumpHeights[] = { 255 | -172.044007, 256 | -181.11601, 257 | -185.00401, 258 | -181.92601, 259 | -181.92601 260 | }; 261 | 262 | double newVel = jumpHeights[p.speed]; 263 | 264 | if (p.slopeData.slope && p.slopeData.slope->orientation == 0) { 265 | auto slope = p.slopeData.slope; 266 | 267 | // Ball doesn't round, leading to very silly results 268 | newVel -= p.grav(0.300000001 * roundVel(0.16875 * std::min(1.12 / slope->angle(), 1.54) * (slope->size.y * player_speeds[p.speed] / slope->size.x), p.upsideDown)); 269 | } 270 | 271 | p.upsideDown = !p.upsideDown; 272 | p.setVelocity(newVel, p.prevPlayer().buffer || p.vehicleBuffer); 273 | p.vehicleBuffer = false; 274 | p.buffer = false; 275 | p.input = false; 276 | p.roundVelocity = false; 277 | } 278 | }; 279 | 280 | v.bounds = 240; 281 | 282 | return v; 283 | } 284 | 285 | Vehicle ufo() { 286 | Vehicle v; 287 | 288 | v.type = VehicleType::Ufo; 289 | v.enter = +[](Player& p) { 290 | VehicleType pv = p.prevPlayer().vehicle.type; 291 | if ((pv == VehicleType::Ship || pv == VehicleType::Wave) && p.input) 292 | p.buffer = true; 293 | 294 | p.velocity = p.velocity / (p.prevPlayer().vehicle.type == VehicleType::Ship ? 4 : 2); 295 | }; 296 | 297 | v.clamp = +[](Player& p) { 298 | p.velocity = std::clamp(p.velocity, 299 | p.small ? -406.56 : -345.6, 300 | p.small ? 508.24 : 432.0 301 | ); 302 | 303 | p.input = p.button; 304 | 305 | if (p.gravTop(p) > p.gravCeiling()) { 306 | if (p.velocity > 0) { 307 | p.setVelocity(0, false); 308 | } 309 | p.pos.y = p.grav(p.gravCeiling()) - p.grav(p.size.y / 2); 310 | } 311 | 312 | }; 313 | 314 | v.update = +[](Player& p) { 315 | if (p.buffer) { 316 | p.velocity = std::max(p.velocity, p.small ? 358.992 : 371.034); 317 | p.velocityOverride = true; 318 | p.buffer = false; 319 | p.grounded = false; 320 | } else { 321 | if (p.velocity > p.grav(velocity_thresholds[p.speed])) { 322 | p.acceleration = p.small ? -1969.92 : -1671.84; 323 | } else { 324 | p.acceleration = p.small ? -1308.96 : -1114.56; 325 | } 326 | 327 | if (p.grounded) { 328 | p.setVelocity(0, true); 329 | } 330 | 331 | if (p.button) 332 | p.input = false; 333 | } 334 | }; 335 | 336 | v.bounds = 300; 337 | 338 | return v; 339 | } 340 | 341 | Vehicle wave() { 342 | Vehicle v; 343 | v.type = VehicleType::Wave; 344 | v.enter = +[](Player& p) { 345 | p.actions.push_back(+[](Player& p) { 346 | p.size = p.small ? Vec2D(6, 6) : Vec2D(10, 10); 347 | }); 348 | }; 349 | 350 | v.clamp = +[](Player& p) { 351 | float waveTop = p.grav(p.pos.y + p.grav(p.size.y)) ; 352 | float waveBottom = p.grav(p.pos.y - p.grav(p.size.y)); 353 | 354 | p.velocity = (p.input * 2 - 1) * player_speeds[p.speed] * (p.small ? 2 : 1); 355 | 356 | if (waveBottom <= p.gravFloor()) { 357 | p.pos.y = p.grav(p.gravFloor() + p.size.y); 358 | if (waveBottom == p.gravFloor() && !p.input) 359 | p.velocity = 0; 360 | } else if (waveTop >= p.gravCeiling()) { 361 | p.pos.y = p.grav(p.gravCeiling() - p.size.y); 362 | if (waveTop == p.gravCeiling() && p.input) 363 | p.velocity = 0; 364 | } 365 | 366 | //p.rotation = (p.input ? 1 : -1) * (p.small ? 63.4258423 : 45); 367 | }; 368 | v.update = +[](Player& p) { 369 | p.acceleration = 0; 370 | p.buffer = false; 371 | 372 | rotateFly(p, p.small ? 0.4 : 0.25); 373 | }; 374 | 375 | v.bounds = 300; 376 | 377 | return v; 378 | } 379 | 380 | Vehicle Vehicle::from(VehicleType v) { 381 | switch (v) { 382 | case VehicleType::Cube: 383 | return cube(); 384 | case VehicleType::Ship: 385 | return ship(); 386 | case VehicleType::Ball: 387 | return ball(); 388 | case VehicleType::Ufo: 389 | return ufo(); 390 | case VehicleType::Wave: 391 | return wave(); 392 | } 393 | } -------------------------------------------------------------------------------- /src/subprocess.hpp: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Documentation for C++ subprocessing libraray. 4 | 5 | @copyright The code is licensed under the [MIT 6 | License](http://opensource.org/licenses/MIT): 7 |
8 | Copyright © 2016-2018 Arun Muralidharan. 9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 |
17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | @author [Arun Muralidharan] 29 | @see https://github.com/arun11299/cpp-subprocess to download the source code 30 | 31 | @version 1.0.0 32 | */ 33 | 34 | #ifndef SUBPROCESS_HPP 35 | #define SUBPROCESS_HPP 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | 54 | #if (defined _MSC_VER) || (defined __MINGW32__) 55 | #define __USING_WINDOWS__ 56 | #endif 57 | 58 | #ifdef __USING_WINDOWS__ 59 | #include 60 | #endif 61 | 62 | extern "C" { 63 | #ifdef __USING_WINDOWS__ 64 | #include 65 | #include 66 | #include 67 | 68 | #define close _close 69 | #define open _open 70 | #define fileno _fileno 71 | #else 72 | #include 73 | #include 74 | #endif 75 | #include 76 | #include 77 | #include 78 | } 79 | 80 | /*! 81 | * Getting started with reading this source code. 82 | * The source is mainly divided into four parts: 83 | * 1. Exception Classes: 84 | * These are very basic exception classes derived from 85 | * runtime_error exception. 86 | * There are two types of exception thrown from subprocess 87 | * library: OSError and CalledProcessError 88 | * 89 | * 2. Popen Class 90 | * This is the main class the users will deal with. It 91 | * provides with all the API's to deal with processes. 92 | * 93 | * 3. Util namespace 94 | * It includes some helper functions to split/join a string, 95 | * reading from file descriptors, waiting on a process, fcntl 96 | * options on file descriptors etc. 97 | * 98 | * 4. Detail namespace 99 | * This includes some metaprogramming and helper classes. 100 | */ 101 | 102 | 103 | namespace subprocess { 104 | 105 | // Max buffer size allocated on stack for read error 106 | // from pipe 107 | static const size_t SP_MAX_ERR_BUF_SIZ = 1024; 108 | 109 | // Default buffer capcity for OutBuffer and ErrBuffer. 110 | // If the data exceeds this capacity, the buffer size is grown 111 | // by 1.5 times its previous capacity 112 | static const size_t DEFAULT_BUF_CAP_BYTES = 8192; 113 | 114 | 115 | /*----------------------------------------------- 116 | * EXCEPTION CLASSES 117 | *----------------------------------------------- 118 | */ 119 | 120 | /*! 121 | * class: CalledProcessError 122 | * Thrown when there was error executing the command. 123 | * Check Popen class API's to know when this exception 124 | * can be thrown. 125 | * 126 | */ 127 | class CalledProcessError: public std::runtime_error 128 | { 129 | public: 130 | int retcode; 131 | CalledProcessError(const std::string& error_msg, int retcode): 132 | std::runtime_error(error_msg), retcode(retcode) 133 | {} 134 | }; 135 | 136 | 137 | /*! 138 | * class: OSError 139 | * Thrown when some system call fails to execute or give result. 140 | * The exception message contains the name of the failed system call 141 | * with the stringisized errno code. 142 | * Check Popen class API's to know when this exception would be 143 | * thrown. 144 | * Its usual that the API exception specification would have 145 | * this exception together with CalledProcessError. 146 | */ 147 | class OSError: public std::runtime_error 148 | { 149 | public: 150 | OSError(const std::string& err_msg, int err_code): 151 | std::runtime_error( err_msg + ": " + std::strerror(err_code) ) 152 | {} 153 | }; 154 | 155 | //-------------------------------------------------------------------- 156 | 157 | //Environment Variable types 158 | #ifndef _MSC_VER 159 | using env_string_t = std::string; 160 | using env_char_t = char; 161 | #else 162 | using env_string_t = std::wstring; 163 | using env_char_t = wchar_t; 164 | #endif 165 | using env_map_t = std::map; 166 | using env_vector_t = std::vector; 167 | 168 | //-------------------------------------------------------------------- 169 | namespace util 170 | { 171 | template 172 | inline bool is_ready(std::shared_future const &f) 173 | { 174 | return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; 175 | } 176 | 177 | inline void quote_argument(const std::wstring &argument, std::wstring &command_line, 178 | bool force) 179 | { 180 | // 181 | // Unless we're told otherwise, don't quote unless we actually 182 | // need to do so --- hopefully avoid problems if programs won't 183 | // parse quotes properly 184 | // 185 | 186 | if (force == false && argument.empty() == false && 187 | argument.find_first_of(L" \t\n\v\"") == argument.npos) { 188 | command_line.append(argument); 189 | } 190 | else { 191 | command_line.push_back(L'"'); 192 | 193 | for (auto it = argument.begin();; ++it) { 194 | unsigned number_backslashes = 0; 195 | 196 | while (it != argument.end() && *it == L'\\') { 197 | ++it; 198 | ++number_backslashes; 199 | } 200 | 201 | if (it == argument.end()) { 202 | 203 | // 204 | // Escape all backslashes, but let the terminating 205 | // double quotation mark we add below be interpreted 206 | // as a metacharacter. 207 | // 208 | 209 | command_line.append(number_backslashes * 2, L'\\'); 210 | break; 211 | } 212 | else if (*it == L'"') { 213 | 214 | // 215 | // Escape all backslashes and the following 216 | // double quotation mark. 217 | // 218 | 219 | command_line.append(number_backslashes * 2 + 1, L'\\'); 220 | command_line.push_back(*it); 221 | } 222 | else { 223 | 224 | // 225 | // Backslashes aren't special here. 226 | // 227 | 228 | command_line.append(number_backslashes, L'\\'); 229 | command_line.push_back(*it); 230 | } 231 | } 232 | 233 | command_line.push_back(L'"'); 234 | } 235 | } 236 | 237 | #ifdef __USING_WINDOWS__ 238 | inline std::string get_last_error(DWORD errorMessageID) 239 | { 240 | if (errorMessageID == 0) 241 | return std::string(); 242 | 243 | LPSTR messageBuffer = nullptr; 244 | size_t size = FormatMessageA( 245 | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | 246 | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK, 247 | NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 248 | (LPSTR)&messageBuffer, 0, NULL); 249 | 250 | std::string message(messageBuffer, size); 251 | 252 | LocalFree(messageBuffer); 253 | 254 | return message; 255 | } 256 | 257 | inline FILE *file_from_handle(HANDLE h, const char *mode) 258 | { 259 | int md; 260 | if (!mode) { 261 | throw OSError("invalid_mode", 0); 262 | } 263 | 264 | if (mode[0] == 'w') { 265 | md = _O_WRONLY; 266 | } 267 | else if (mode[0] == 'r') { 268 | md = _O_RDONLY; 269 | } 270 | else { 271 | throw OSError("file_from_handle", 0); 272 | } 273 | 274 | int os_fhandle = _open_osfhandle((intptr_t)h, md); 275 | if (os_fhandle == -1) { 276 | CloseHandle(h); 277 | throw OSError("_open_osfhandle", 0); 278 | } 279 | 280 | FILE *fp = _fdopen(os_fhandle, mode); 281 | if (fp == 0) { 282 | _close(os_fhandle); 283 | throw OSError("_fdopen", 0); 284 | } 285 | 286 | return fp; 287 | } 288 | 289 | inline void configure_pipe(HANDLE* read_handle, HANDLE* write_handle, HANDLE* child_handle) 290 | { 291 | SECURITY_ATTRIBUTES saAttr; 292 | 293 | // Set the bInheritHandle flag so pipe handles are inherited. 294 | saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 295 | saAttr.bInheritHandle = TRUE; 296 | saAttr.lpSecurityDescriptor = NULL; 297 | 298 | // Create a pipe for the child process's STDIN. 299 | if (!CreatePipe(read_handle, write_handle, &saAttr,0)) 300 | throw OSError("CreatePipe", 0); 301 | 302 | // Ensure the write handle to the pipe for STDIN is not inherited. 303 | if (!SetHandleInformation(*child_handle, HANDLE_FLAG_INHERIT, 0)) 304 | throw OSError("SetHandleInformation", 0); 305 | } 306 | 307 | // env_map_t MapFromWindowsEnvironment() 308 | // * Imports current Environment in a C-string table 309 | // * Parses the strings by splitting on the first "=" per line 310 | // * Creates a map of the variables 311 | // * Returns the map 312 | inline env_map_t MapFromWindowsEnvironment(){ 313 | wchar_t *variable_strings_ptr; 314 | wchar_t *environment_strings_ptr; 315 | std::wstring delimeter(L"="); 316 | int del_len = delimeter.length(); 317 | env_map_t mapped_environment; 318 | 319 | // Get a pointer to the environment block. 320 | environment_strings_ptr = GetEnvironmentStringsW(); 321 | // If the returned pointer is NULL, exit. 322 | if (environment_strings_ptr == NULL) 323 | { 324 | throw OSError("GetEnvironmentStringsW", 0); 325 | } 326 | 327 | // Variable strings are separated by NULL byte, and the block is 328 | // terminated by a NULL byte. 329 | 330 | variable_strings_ptr = (wchar_t *) environment_strings_ptr; 331 | 332 | //Since the environment map ends with a null, we can loop until we find it. 333 | while (*variable_strings_ptr) 334 | { 335 | // Create a string from Variable String 336 | env_string_t current_line(variable_strings_ptr); 337 | // Find the first "equals" sign. 338 | auto pos = current_line.find(delimeter); 339 | // Assuming it's not missing ... 340 | if(pos!=std::wstring::npos){ 341 | // ... parse the key and value. 342 | env_string_t key = current_line.substr(0, pos); 343 | env_string_t value = current_line.substr(pos + del_len); 344 | // Map the entry. 345 | mapped_environment[key] = value; 346 | } 347 | // Jump to next line in the environment map. 348 | variable_strings_ptr += std::wcslen(variable_strings_ptr) + 1; 349 | } 350 | // We're done with the old environment map buffer. 351 | FreeEnvironmentStringsW(environment_strings_ptr); 352 | 353 | // Return the map. 354 | return mapped_environment; 355 | } 356 | 357 | // env_vector_t WindowsEnvironmentVectorFromMap(const env_map_t &source_map) 358 | // * Creates a vector buffer for the new environment string table 359 | // * Copies in the mapped variables 360 | // * Returns the vector 361 | inline env_vector_t WindowsEnvironmentVectorFromMap(const env_map_t &source_map) 362 | { 363 | // Make a new environment map buffer. 364 | env_vector_t environment_map_buffer; 365 | // Give it some space. 366 | environment_map_buffer.reserve(4096); 367 | 368 | // And fill'er up. 369 | for(auto kv: source_map){ 370 | // Create the line 371 | env_string_t current_line(kv.first); current_line += L"="; current_line += kv.second; 372 | // Add the line to the buffer. 373 | std::copy(current_line.begin(), current_line.end(), std::back_inserter(environment_map_buffer)); 374 | // Append a null 375 | environment_map_buffer.push_back(0); 376 | } 377 | // Append one last null because of how Windows does it's environment maps. 378 | environment_map_buffer.push_back(0); 379 | 380 | return environment_map_buffer; 381 | } 382 | 383 | // env_vector_t CreateUpdatedWindowsEnvironmentVector(const env_map_t &changes_map) 384 | // * Merges host environment with new mapped variables 385 | // * Creates and returns string vector based on map 386 | inline env_vector_t CreateUpdatedWindowsEnvironmentVector(const env_map_t &changes_map){ 387 | // Import the environment map 388 | env_map_t environment_map = MapFromWindowsEnvironment(); 389 | // Merge in the changes with overwrite 390 | for(auto& it: changes_map) 391 | { 392 | environment_map[it.first] = it.second; 393 | } 394 | // Create a Windows-usable Environment Map Buffer 395 | env_vector_t environment_map_strings_vector = WindowsEnvironmentVectorFromMap(environment_map); 396 | 397 | return environment_map_strings_vector; 398 | } 399 | 400 | #endif 401 | 402 | /*! 403 | * Function: split 404 | * Parameters: 405 | * [in] str : Input string which needs to be split based upon the 406 | * delimiters provided. 407 | * [in] deleims : Delimiter characters based upon which the string needs 408 | * to be split. Default constructed to ' '(space) and '\t'(tab) 409 | * [out] vector : Vector of strings split at deleimiter. 410 | */ 411 | static inline std::vector 412 | split(const std::string& str, const std::string& delims=" \t") 413 | { 414 | std::vector res; 415 | size_t init = 0; 416 | 417 | while (true) { 418 | auto pos = str.find_first_of(delims, init); 419 | if (pos == std::string::npos) { 420 | res.emplace_back(str.substr(init, str.length())); 421 | break; 422 | } 423 | res.emplace_back(str.substr(init, pos - init)); 424 | pos++; 425 | init = pos; 426 | } 427 | 428 | return res; 429 | } 430 | 431 | 432 | /*! 433 | * Function: join 434 | * Parameters: 435 | * [in] vec : Vector of strings which needs to be joined to form 436 | * a single string with words seperated by a seperator char. 437 | * [in] sep : String used to seperate 2 words in the joined string. 438 | * Default constructed to ' ' (space). 439 | * [out] string: Joined string. 440 | */ 441 | static inline 442 | std::string join(const std::vector& vec, 443 | const std::string& sep = " ") 444 | { 445 | std::string res; 446 | for (auto& elem : vec) res.append(elem + sep); 447 | res.erase(--res.end()); 448 | return res; 449 | } 450 | 451 | 452 | #ifndef __USING_WINDOWS__ 453 | /*! 454 | * Function: set_clo_on_exec 455 | * Sets/Resets the FD_CLOEXEC flag on the provided file descriptor 456 | * based upon the `set` parameter. 457 | * Parameters: 458 | * [in] fd : The descriptor on which FD_CLOEXEC needs to be set/reset. 459 | * [in] set : If 'true', set FD_CLOEXEC. 460 | * If 'false' unset FD_CLOEXEC. 461 | */ 462 | static inline 463 | void set_clo_on_exec(int fd, bool set = true) 464 | { 465 | int flags = fcntl(fd, F_GETFD, 0); 466 | if (set) flags |= FD_CLOEXEC; 467 | else flags &= ~FD_CLOEXEC; 468 | //TODO: should check for errors 469 | fcntl(fd, F_SETFD, flags); 470 | } 471 | 472 | 473 | /*! 474 | * Function: pipe_cloexec 475 | * Creates a pipe and sets FD_CLOEXEC flag on both 476 | * read and write descriptors of the pipe. 477 | * Parameters: 478 | * [out] : A pair of file descriptors. 479 | * First element of pair is the read descriptor of pipe. 480 | * Second element is the write descriptor of pipe. 481 | */ 482 | static inline 483 | std::pair pipe_cloexec() noexcept(false) 484 | { 485 | int pipe_fds[2]; 486 | int res = pipe(pipe_fds); 487 | if (res) { 488 | throw OSError("pipe failure", errno); 489 | } 490 | 491 | set_clo_on_exec(pipe_fds[0]); 492 | set_clo_on_exec(pipe_fds[1]); 493 | 494 | return std::make_pair(pipe_fds[0], pipe_fds[1]); 495 | } 496 | #endif 497 | 498 | 499 | /*! 500 | * Function: write_n 501 | * Writes `length` bytes to the file descriptor `fd` 502 | * from the buffer `buf`. 503 | * Parameters: 504 | * [in] fd : The file descriptotr to write to. 505 | * [in] buf: Buffer from which data needs to be written to fd. 506 | * [in] length: The number of bytes that needs to be written from 507 | * `buf` to `fd`. 508 | * [out] int : Number of bytes written or -1 in case of failure. 509 | */ 510 | static inline 511 | int write_n(int fd, const char* buf, size_t length) 512 | { 513 | size_t nwritten = 0; 514 | while (nwritten < length) { 515 | int written = write(fd, buf + nwritten, length - nwritten); 516 | if (written == -1) return -1; 517 | nwritten += written; 518 | } 519 | return nwritten; 520 | } 521 | 522 | 523 | /*! 524 | * Function: read_atmost_n 525 | * Reads at the most `read_upto` bytes from the 526 | * file object `fp` before returning. 527 | * Parameters: 528 | * [in] fp : The file object from which it needs to read. 529 | * [in] buf : The buffer into which it needs to write the data. 530 | * [in] read_upto: Max number of bytes which must be read from `fd`. 531 | * [out] int : Number of bytes written to `buf` or read from `fd` 532 | * OR -1 in case of error. 533 | * NOTE: In case of EINTR while reading from socket, this API 534 | * will retry to read from `fd`, but only till the EINTR counter 535 | * reaches 50 after which it will return with whatever data it read. 536 | */ 537 | static inline 538 | int read_atmost_n(FILE* fp, char* buf, size_t read_upto) 539 | { 540 | #ifdef __USING_WINDOWS__ 541 | return (int)fread(buf, 1, read_upto, fp); 542 | #else 543 | int fd = fileno(fp); 544 | int rbytes = 0; 545 | int eintr_cnter = 0; 546 | 547 | while (1) { 548 | int read_bytes = read(fd, buf + rbytes, read_upto - rbytes); 549 | if (read_bytes == -1) { 550 | if (errno == EINTR) { 551 | if (eintr_cnter >= 50) return -1; 552 | eintr_cnter++; 553 | continue; 554 | } 555 | return -1; 556 | } 557 | if (read_bytes == 0) return rbytes; 558 | 559 | rbytes += read_bytes; 560 | } 561 | return rbytes; 562 | #endif 563 | } 564 | 565 | 566 | /*! 567 | * Function: read_all 568 | * Reads all the available data from `fp` into 569 | * `buf`. Internally calls read_atmost_n. 570 | * Parameters: 571 | * [in] fp : The file object from which to read from. 572 | * [in] buf : The buffer of type `class Buffer` into which 573 | * the read data is written to. 574 | * [out] int: Number of bytes read OR -1 in case of failure. 575 | * 576 | * NOTE: `class Buffer` is a exposed public class. See below. 577 | */ 578 | 579 | static inline int read_all(FILE* fp, std::vector& buf) 580 | { 581 | auto buffer = buf.data(); 582 | int total_bytes_read = 0; 583 | int fill_sz = buf.size(); 584 | 585 | while (1) { 586 | const int rd_bytes = read_atmost_n(fp, buffer, fill_sz); 587 | 588 | if (rd_bytes == -1) { // Read finished 589 | if (total_bytes_read == 0) return -1; 590 | break; 591 | 592 | } else if (rd_bytes == fill_sz) { // Buffer full 593 | const auto orig_sz = buf.size(); 594 | const auto new_sz = orig_sz * 2; 595 | buf.resize(new_sz); 596 | fill_sz = new_sz - orig_sz; 597 | 598 | //update the buffer pointer 599 | buffer = buf.data(); 600 | total_bytes_read += rd_bytes; 601 | buffer += total_bytes_read; 602 | 603 | } else { // Partial data ? Continue reading 604 | total_bytes_read += rd_bytes; 605 | fill_sz -= rd_bytes; 606 | break; 607 | } 608 | } 609 | buf.erase(buf.begin()+total_bytes_read, buf.end()); // remove extra nulls 610 | return total_bytes_read; 611 | } 612 | 613 | #ifndef __USING_WINDOWS__ 614 | /*! 615 | * Function: wait_for_child_exit 616 | * Waits for the process with pid `pid` to exit 617 | * and returns its status. 618 | * Parameters: 619 | * [in] pid : The pid of the process. 620 | * [out] pair: 621 | * pair.first : Return code of the waitpid call. 622 | * pair.second : Exit status of the process. 623 | * 624 | * NOTE: This is a blocking call as in, it will loop 625 | * till the child is exited. 626 | */ 627 | static inline 628 | std::pair wait_for_child_exit(int pid) 629 | { 630 | int status = 0; 631 | int ret = -1; 632 | while (1) { 633 | ret = waitpid(pid, &status, 0); 634 | if (ret == -1) break; 635 | if (ret == 0) continue; 636 | return std::make_pair(ret, status); 637 | } 638 | 639 | return std::make_pair(ret, status); 640 | } 641 | #endif 642 | 643 | } // end namespace util 644 | 645 | 646 | 647 | /* ------------------------------- 648 | * Popen Arguments 649 | * ------------------------------- 650 | */ 651 | 652 | /*! 653 | * The buffer size of the stdin/stdout/stderr 654 | * streams of the child process. 655 | * Default value is 0. 656 | */ 657 | struct bufsize { 658 | explicit bufsize(int siz): bufsiz(siz) {} 659 | int bufsiz = 0; 660 | }; 661 | 662 | /*! 663 | * Option to defer spawning of the child process 664 | * till `Popen::start_process` API is called. 665 | * Default value is false. 666 | */ 667 | struct defer_spawn { 668 | explicit defer_spawn(bool d): defer(d) {} 669 | bool defer = false; 670 | }; 671 | 672 | /*! 673 | * Option to close all file descriptors 674 | * when the child process is spawned. 675 | * The close fd list does not include 676 | * input/output/error if they are explicitly 677 | * set as part of the Popen arguments. 678 | * 679 | * Default value is false. 680 | */ 681 | struct close_fds { 682 | explicit close_fds(bool c): close_all(c) {} 683 | bool close_all = false; 684 | }; 685 | 686 | /*! 687 | * Option to make the child process as the 688 | * session leader and thus the process 689 | * group leader. 690 | * Default value is false. 691 | */ 692 | struct session_leader { 693 | explicit session_leader(bool sl): leader_(sl) {} 694 | bool leader_ = false; 695 | }; 696 | 697 | struct shell { 698 | explicit shell(bool s): shell_(s) {} 699 | bool shell_ = false; 700 | }; 701 | 702 | /*! 703 | * Base class for all arguments involving string value. 704 | */ 705 | struct string_arg 706 | { 707 | string_arg(const char* arg): arg_value(arg) {} 708 | string_arg(std::string&& arg): arg_value(std::move(arg)) {} 709 | string_arg(std::string arg): arg_value(std::move(arg)) {} 710 | std::string arg_value; 711 | }; 712 | 713 | /*! 714 | * Option to specify the executable name seperately 715 | * from the args sequence. 716 | * In this case the cmd args must only contain the 717 | * options required for this executable. 718 | * 719 | * Eg: executable{"ls"} 720 | */ 721 | struct executable: string_arg 722 | { 723 | template 724 | executable(T&& arg): string_arg(std::forward(arg)) {} 725 | }; 726 | 727 | /*! 728 | * Option to set the current working directory 729 | * of the spawned process. 730 | * 731 | * Eg: cwd{"/som/path/x"} 732 | */ 733 | struct cwd: string_arg 734 | { 735 | template 736 | cwd(T&& arg): string_arg(std::forward(arg)) {} 737 | }; 738 | 739 | /*! 740 | * Option to specify environment variables required by 741 | * the spawned process. 742 | * 743 | * Eg: environment{{ {"K1", "V1"}, {"K2", "V2"},... }} 744 | */ 745 | struct environment 746 | { 747 | environment(env_map_t&& env): 748 | env_(std::move(env)) {} 749 | explicit environment(const env_map_t& env): 750 | env_(env) {} 751 | env_map_t env_; 752 | }; 753 | 754 | 755 | /*! 756 | * Used for redirecting input/output/error 757 | */ 758 | enum IOTYPE { 759 | STDOUT = 1, 760 | STDERR, 761 | PIPE, 762 | }; 763 | 764 | //TODO: A common base/interface for below stream structures ?? 765 | 766 | /*! 767 | * Option to specify the input channel for the child 768 | * process. It can be: 769 | * 1. An already open file descriptor. 770 | * 2. A file name. 771 | * 3. IOTYPE. Usual a PIPE 772 | * 773 | * Eg: input{PIPE} 774 | * OR in case of redirection, output of another Popen 775 | * input{popen.output()} 776 | */ 777 | struct input 778 | { 779 | // For an already existing file descriptor. 780 | explicit input(int fd): rd_ch_(fd) {} 781 | 782 | // FILE pointer. 783 | explicit input (FILE* fp):input(fileno(fp)) { assert(fp); } 784 | 785 | explicit input(const char* filename) { 786 | int fd = open(filename, O_RDONLY); 787 | if (fd == -1) throw OSError("File not found: ", errno); 788 | rd_ch_ = fd; 789 | } 790 | explicit input(IOTYPE typ) { 791 | assert (typ == PIPE && "STDOUT/STDERR not allowed"); 792 | #ifndef __USING_WINDOWS__ 793 | std::tie(rd_ch_, wr_ch_) = util::pipe_cloexec(); 794 | #endif 795 | } 796 | 797 | int rd_ch_ = -1; 798 | int wr_ch_ = -1; 799 | }; 800 | 801 | 802 | /*! 803 | * Option to specify the output channel for the child 804 | * process. It can be: 805 | * 1. An already open file descriptor. 806 | * 2. A file name. 807 | * 3. IOTYPE. Usually a PIPE. 808 | * 809 | * Eg: output{PIPE} 810 | * OR output{"output.txt"} 811 | */ 812 | struct output 813 | { 814 | explicit output(int fd): wr_ch_(fd) {} 815 | 816 | explicit output (FILE* fp):output(fileno(fp)) { assert(fp); } 817 | 818 | explicit output(const char* filename) { 819 | int fd = open(filename, O_APPEND | O_CREAT | O_RDWR, 0640); 820 | if (fd == -1) throw OSError("File not found: ", errno); 821 | wr_ch_ = fd; 822 | } 823 | explicit output(IOTYPE typ) { 824 | assert (typ == PIPE && "STDOUT/STDERR not allowed"); 825 | #ifndef __USING_WINDOWS__ 826 | std::tie(rd_ch_, wr_ch_) = util::pipe_cloexec(); 827 | #endif 828 | } 829 | 830 | int rd_ch_ = -1; 831 | int wr_ch_ = -1; 832 | }; 833 | 834 | 835 | /*! 836 | * Option to specify the error channel for the child 837 | * process. It can be: 838 | * 1. An already open file descriptor. 839 | * 2. A file name. 840 | * 3. IOTYPE. Usually a PIPE or STDOUT 841 | * 842 | */ 843 | struct error 844 | { 845 | explicit error(int fd): wr_ch_(fd) {} 846 | 847 | explicit error(FILE* fp):error(fileno(fp)) { assert(fp); } 848 | 849 | explicit error(const char* filename) { 850 | int fd = open(filename, O_APPEND | O_CREAT | O_RDWR, 0640); 851 | if (fd == -1) throw OSError("File not found: ", errno); 852 | wr_ch_ = fd; 853 | } 854 | explicit error(IOTYPE typ) { 855 | assert ((typ == PIPE || typ == STDOUT) && "STDERR not aloowed"); 856 | if (typ == PIPE) { 857 | #ifndef __USING_WINDOWS__ 858 | std::tie(rd_ch_, wr_ch_) = util::pipe_cloexec(); 859 | #endif 860 | } else { 861 | // Need to defer it till we have checked all arguments 862 | deferred_ = true; 863 | } 864 | } 865 | 866 | bool deferred_ = false; 867 | int rd_ch_ = -1; 868 | int wr_ch_ = -1; 869 | }; 870 | 871 | // Impoverished, meager, needy, truly needy 872 | // version of type erasure to store function pointers 873 | // needed to provide the functionality of preexec_func 874 | // ATTN: Can be used only to execute functions with no 875 | // arguments and returning void. 876 | // Could have used more efficient methods, ofcourse, but 877 | // that wont yield me the consistent syntax which I am 878 | // aiming for. If you know, then please do let me know. 879 | 880 | class preexec_func 881 | { 882 | public: 883 | preexec_func() {} 884 | 885 | template 886 | explicit preexec_func(Func f): holder_(new FuncHolder(std::move(f))) 887 | {} 888 | 889 | void operator()() { 890 | (*holder_)(); 891 | } 892 | 893 | private: 894 | struct HolderBase { 895 | virtual void operator()() const = 0; 896 | virtual ~HolderBase(){}; 897 | }; 898 | template 899 | struct FuncHolder: HolderBase { 900 | FuncHolder(T func): func_(std::move(func)) {} 901 | void operator()() const override { func_(); } 902 | // The function pointer/reference 903 | T func_; 904 | }; 905 | 906 | std::unique_ptr holder_ = nullptr; 907 | }; 908 | 909 | // ~~~~ End Popen Args ~~~~ 910 | 911 | 912 | /*! 913 | * class: Buffer 914 | * This class is a very thin wrapper around std::vector 915 | * This is basically used to determine the length of the actual 916 | * data stored inside the dynamically resized vector. 917 | * 918 | * This is what is returned as the output to communicate and check_output 919 | * functions, so, users must know about this class. 920 | * 921 | * OutBuffer and ErrBuffer are just different typedefs to this class. 922 | */ 923 | class Buffer 924 | { 925 | public: 926 | Buffer() {} 927 | explicit Buffer(size_t cap) { buf.resize(cap); } 928 | void add_cap(size_t cap) { buf.resize(cap); } 929 | 930 | #if 0 931 | Buffer(const Buffer& other): 932 | buf(other.buf), 933 | length(other.length) 934 | { 935 | std::cout << "COPY" << std::endl; 936 | } 937 | 938 | Buffer(Buffer&& other): 939 | buf(std::move(other.buf)), 940 | length(other.length) 941 | { 942 | std::cout << "MOVE" << std::endl; 943 | } 944 | #endif 945 | 946 | public: 947 | std::vector buf; 948 | size_t length = 0; 949 | }; 950 | 951 | // Buffer for storing output written to output fd 952 | using OutBuffer = Buffer; 953 | // Buffer for storing output written to error fd 954 | using ErrBuffer = Buffer; 955 | 956 | 957 | // Fwd Decl. 958 | class Popen; 959 | 960 | /*--------------------------------------------------- 961 | * DETAIL NAMESPACE 962 | *--------------------------------------------------- 963 | */ 964 | 965 | namespace detail { 966 | 967 | // Metaprogram for searching a type within 968 | // a variadic parameter pack 969 | // This is particularly required to do a compile time 970 | // checking of the arguments provided to 'check_ouput' function 971 | // wherein the user is not expected to provide an 'ouput' option. 972 | 973 | template struct param_pack{}; 974 | 975 | template struct has_type; 976 | 977 | template 978 | struct has_type> { 979 | static constexpr bool value = false; 980 | }; 981 | 982 | template 983 | struct has_type> { 984 | static constexpr bool value = true; 985 | }; 986 | 987 | template 988 | struct has_type> { 989 | static constexpr bool value = 990 | std::is_same::type>::value ? true : has_type>::value; 991 | }; 992 | 993 | //---- 994 | 995 | /*! 996 | * A helper class to Popen class for setting 997 | * options as provided in the Popen constructor 998 | * or in check_ouput arguments. 999 | * This design allows us to _not_ have any fixed position 1000 | * to any arguments and specify them in a way similar to what 1001 | * can be done in python. 1002 | */ 1003 | struct ArgumentDeducer 1004 | { 1005 | ArgumentDeducer(Popen* p): popen_(p) {} 1006 | 1007 | void set_option(executable&& exe); 1008 | void set_option(cwd&& cwdir); 1009 | void set_option(bufsize&& bsiz); 1010 | void set_option(environment&& env); 1011 | void set_option(defer_spawn&& defer); 1012 | void set_option(shell&& sh); 1013 | void set_option(input&& inp); 1014 | void set_option(output&& out); 1015 | void set_option(error&& err); 1016 | void set_option(close_fds&& cfds); 1017 | void set_option(preexec_func&& prefunc); 1018 | void set_option(session_leader&& sleader); 1019 | 1020 | private: 1021 | Popen* popen_ = nullptr; 1022 | }; 1023 | 1024 | /*! 1025 | * A helper class to Popen. 1026 | * This takes care of all the fork-exec logic 1027 | * in the execute_child API. 1028 | */ 1029 | class Child 1030 | { 1031 | public: 1032 | Child(Popen* p, int err_wr_pipe): 1033 | parent_(p), 1034 | err_wr_pipe_(err_wr_pipe) 1035 | {} 1036 | 1037 | void execute_child(); 1038 | 1039 | private: 1040 | // Lets call it parent even though 1041 | // technically a bit incorrect 1042 | Popen* parent_ = nullptr; 1043 | int err_wr_pipe_ = -1; 1044 | }; 1045 | 1046 | // Fwd Decl. 1047 | class Streams; 1048 | 1049 | /*! 1050 | * A helper class to Streams. 1051 | * This takes care of management of communicating 1052 | * with the child process with the means of the correct 1053 | * file descriptor. 1054 | */ 1055 | class Communication 1056 | { 1057 | public: 1058 | Communication(Streams* stream): stream_(stream) 1059 | {} 1060 | void operator=(const Communication&) = delete; 1061 | public: 1062 | int send(const char* msg, size_t length); 1063 | int send(const std::vector& msg); 1064 | 1065 | std::pair communicate(const char* msg, size_t length); 1066 | std::pair communicate(const std::vector& msg) 1067 | { return communicate(msg.data(), msg.size()); } 1068 | 1069 | void set_out_buf_cap(size_t cap) { out_buf_cap_ = cap; } 1070 | void set_err_buf_cap(size_t cap) { err_buf_cap_ = cap; } 1071 | 1072 | private: 1073 | std::pair communicate_threaded( 1074 | const char* msg, size_t length); 1075 | 1076 | private: 1077 | Streams* stream_; 1078 | size_t out_buf_cap_ = DEFAULT_BUF_CAP_BYTES; 1079 | size_t err_buf_cap_ = DEFAULT_BUF_CAP_BYTES; 1080 | }; 1081 | 1082 | 1083 | 1084 | /*! 1085 | * This is a helper class to Popen. 1086 | * It takes care of management of all the file descriptors 1087 | * and file pointers. 1088 | * It dispatches of the communication aspects to the 1089 | * Communication class. 1090 | * Read through the data members to understand about the 1091 | * various file descriptors used. 1092 | */ 1093 | class Streams 1094 | { 1095 | public: 1096 | Streams():comm_(this) {} 1097 | void operator=(const Streams&) = delete; 1098 | 1099 | public: 1100 | void setup_comm_channels(); 1101 | 1102 | void cleanup_fds() 1103 | { 1104 | if (write_to_child_ != -1 && read_from_parent_ != -1) { 1105 | close(write_to_child_); 1106 | } 1107 | if (write_to_parent_ != -1 && read_from_child_ != -1) { 1108 | close(read_from_child_); 1109 | } 1110 | if (err_write_ != -1 && err_read_ != -1) { 1111 | close(err_read_); 1112 | } 1113 | } 1114 | 1115 | void close_parent_fds() 1116 | { 1117 | if (write_to_child_ != -1) close(write_to_child_); 1118 | if (read_from_child_ != -1) close(read_from_child_); 1119 | if (err_read_ != -1) close(err_read_); 1120 | } 1121 | 1122 | void close_child_fds() 1123 | { 1124 | if (write_to_parent_ != -1) close(write_to_parent_); 1125 | if (read_from_parent_ != -1) close(read_from_parent_); 1126 | if (err_write_ != -1) close(err_write_); 1127 | } 1128 | 1129 | FILE* input() { return input_.get(); } 1130 | FILE* output() { return output_.get(); } 1131 | FILE* error() { return error_.get(); } 1132 | 1133 | void input(FILE* fp) { input_.reset(fp, fclose); } 1134 | void output(FILE* fp) { output_.reset(fp, fclose); } 1135 | void error(FILE* fp) { error_.reset(fp, fclose); } 1136 | 1137 | void set_out_buf_cap(size_t cap) { comm_.set_out_buf_cap(cap); } 1138 | void set_err_buf_cap(size_t cap) { comm_.set_err_buf_cap(cap); } 1139 | 1140 | public: /* Communication forwarding API's */ 1141 | int send(const char* msg, size_t length) 1142 | { return comm_.send(msg, length); } 1143 | 1144 | int send(const std::vector& msg) 1145 | { return comm_.send(msg); } 1146 | 1147 | std::pair communicate(const char* msg, size_t length) 1148 | { return comm_.communicate(msg, length); } 1149 | 1150 | std::pair communicate(const std::vector& msg) 1151 | { return comm_.communicate(msg); } 1152 | 1153 | 1154 | public:// Yes they are public 1155 | 1156 | std::shared_ptr input_ = nullptr; 1157 | std::shared_ptr output_ = nullptr; 1158 | std::shared_ptr error_ = nullptr; 1159 | 1160 | #ifdef __USING_WINDOWS__ 1161 | HANDLE g_hChildStd_IN_Rd = nullptr; 1162 | HANDLE g_hChildStd_IN_Wr = nullptr; 1163 | HANDLE g_hChildStd_OUT_Rd = nullptr; 1164 | HANDLE g_hChildStd_OUT_Wr = nullptr; 1165 | HANDLE g_hChildStd_ERR_Rd = nullptr; 1166 | HANDLE g_hChildStd_ERR_Wr = nullptr; 1167 | #endif 1168 | 1169 | // Buffer size for the IO streams 1170 | int bufsiz_ = 0; 1171 | 1172 | // Pipes for communicating with child 1173 | 1174 | // Emulates stdin 1175 | int write_to_child_ = -1; // Parent owned descriptor 1176 | int read_from_parent_ = -1; // Child owned descriptor 1177 | 1178 | // Emulates stdout 1179 | int write_to_parent_ = -1; // Child owned descriptor 1180 | int read_from_child_ = -1; // Parent owned descriptor 1181 | 1182 | // Emulates stderr 1183 | int err_write_ = -1; // Write error to parent (Child owned) 1184 | int err_read_ = -1; // Read error from child (Parent owned) 1185 | 1186 | private: 1187 | Communication comm_; 1188 | }; 1189 | 1190 | } // end namespace detail 1191 | 1192 | 1193 | 1194 | /*! 1195 | * class: Popen 1196 | * This is the single most important class in the whole library 1197 | * and glues together all the helper classes to provide a common 1198 | * interface to the client. 1199 | * 1200 | * API's provided by the class: 1201 | * 1. Popen({"cmd"}, output{..}, error{..}, cwd{..}, ....) 1202 | * Command provided as a sequence. 1203 | * 2. Popen("cmd arg1"m output{..}, error{..}, cwd{..}, ....) 1204 | * Command provided in a single string. 1205 | * 3. wait() - Wait for the child to exit. 1206 | * 4. retcode() - The return code of the exited child. 1207 | * 5. pid() - PID of the spawned child. 1208 | * 6. poll() - Check the status of the running child. 1209 | * 7. kill(sig_num) - Kill the child. SIGTERM used by default. 1210 | * 8. send(...) - Send input to the input channel of the child. 1211 | * 9. communicate(...) - Get the output/error from the child and close the channels 1212 | * from the parent side. 1213 | *10. input() - Get the input channel/File pointer. Can be used for 1214 | * cutomizing the way of sending input to child. 1215 | *11. output() - Get the output channel/File pointer. Usually used 1216 | in case of redirection. See piping examples. 1217 | *12. error() - Get the error channel/File poiner. Usually used 1218 | in case of redirection. 1219 | *13. start_process() - Start the child process. Only to be used when 1220 | * `defer_spawn` option was provided in Popen constructor. 1221 | */ 1222 | class Popen 1223 | { 1224 | public: 1225 | friend struct detail::ArgumentDeducer; 1226 | friend class detail::Child; 1227 | 1228 | template 1229 | Popen(const std::string& cmd_args, Args&& ...args): 1230 | args_(cmd_args) 1231 | { 1232 | vargs_ = util::split(cmd_args); 1233 | init_args(std::forward(args)...); 1234 | 1235 | // Setup the communication channels of the Popen class 1236 | stream_.setup_comm_channels(); 1237 | 1238 | if (!defer_process_start_) execute_process(); 1239 | } 1240 | 1241 | template 1242 | Popen(std::initializer_list cmd_args, Args&& ...args) 1243 | { 1244 | vargs_.insert(vargs_.end(), cmd_args.begin(), cmd_args.end()); 1245 | init_args(std::forward(args)...); 1246 | 1247 | // Setup the communication channels of the Popen class 1248 | stream_.setup_comm_channels(); 1249 | 1250 | if (!defer_process_start_) execute_process(); 1251 | } 1252 | 1253 | template 1254 | Popen(std::vector vargs_, Args &&... args) : vargs_(vargs_) 1255 | { 1256 | init_args(std::forward(args)...); 1257 | 1258 | // Setup the communication channels of the Popen class 1259 | stream_.setup_comm_channels(); 1260 | 1261 | if (!defer_process_start_) execute_process(); 1262 | } 1263 | 1264 | /* 1265 | ~Popen() 1266 | { 1267 | #ifdef __USING_WINDOWS__ 1268 | CloseHandle(this->process_handle_); 1269 | #endif 1270 | } 1271 | */ 1272 | 1273 | void start_process() noexcept(false); 1274 | 1275 | int pid() const noexcept { return child_pid_; } 1276 | 1277 | int retcode() const noexcept { return retcode_; } 1278 | 1279 | int wait() noexcept(false); 1280 | 1281 | int poll() noexcept(false); 1282 | 1283 | // Does not fail, Caller is expected to recheck the 1284 | // status with a call to poll() 1285 | void kill(int sig_num = 9); 1286 | 1287 | void set_out_buf_cap(size_t cap) { stream_.set_out_buf_cap(cap); } 1288 | 1289 | void set_err_buf_cap(size_t cap) { stream_.set_err_buf_cap(cap); } 1290 | 1291 | int send(const char* msg, size_t length) 1292 | { return stream_.send(msg, length); } 1293 | 1294 | int send(const std::string& msg) 1295 | { return send(msg.c_str(), msg.size()); } 1296 | 1297 | int send(const std::vector& msg) 1298 | { return stream_.send(msg); } 1299 | 1300 | std::pair communicate(const char* msg, size_t length) 1301 | { 1302 | auto res = stream_.communicate(msg, length); 1303 | retcode_ = wait(); 1304 | return res; 1305 | } 1306 | 1307 | std::pair communicate(const std::string& msg) 1308 | { 1309 | return communicate(msg.c_str(), msg.size()); 1310 | } 1311 | 1312 | std::pair communicate(const std::vector& msg) 1313 | { 1314 | auto res = stream_.communicate(msg); 1315 | retcode_ = wait(); 1316 | return res; 1317 | } 1318 | 1319 | std::pair communicate() 1320 | { 1321 | return communicate(nullptr, 0); 1322 | } 1323 | 1324 | FILE* input() { return stream_.input(); } 1325 | FILE* output() { return stream_.output();} 1326 | FILE* error() { return stream_.error(); } 1327 | 1328 | /// Stream close APIs 1329 | void close_input() { stream_.input_.reset(); } 1330 | void close_output() { stream_.output_.reset(); } 1331 | void close_error() { stream_.error_.reset(); } 1332 | 1333 | private: 1334 | template 1335 | void init_args(F&& farg, Args&&... args); 1336 | void init_args(); 1337 | void populate_c_argv(); 1338 | void execute_process() noexcept(false); 1339 | 1340 | private: 1341 | detail::Streams stream_; 1342 | 1343 | #ifdef __USING_WINDOWS__ 1344 | HANDLE process_handle_; 1345 | std::future cleanup_future_; 1346 | #endif 1347 | 1348 | bool defer_process_start_ = false; 1349 | bool close_fds_ = false; 1350 | bool has_preexec_fn_ = false; 1351 | bool shell_ = false; 1352 | bool session_leader_ = false; 1353 | 1354 | std::string exe_name_; 1355 | std::string cwd_; 1356 | env_map_t env_; 1357 | preexec_func preexec_fn_; 1358 | 1359 | // Command in string format 1360 | std::string args_; 1361 | // Comamnd provided as sequence 1362 | std::vector vargs_; 1363 | std::vector cargv_; 1364 | 1365 | bool child_created_ = false; 1366 | // Pid of the child process 1367 | int child_pid_ = -1; 1368 | 1369 | int retcode_ = -1; 1370 | }; 1371 | 1372 | inline void Popen::init_args() { 1373 | populate_c_argv(); 1374 | } 1375 | 1376 | template 1377 | inline void Popen::init_args(F&& farg, Args&&... args) 1378 | { 1379 | detail::ArgumentDeducer argd(this); 1380 | argd.set_option(std::forward(farg)); 1381 | init_args(std::forward(args)...); 1382 | } 1383 | 1384 | inline void Popen::populate_c_argv() 1385 | { 1386 | cargv_.clear(); 1387 | cargv_.reserve(vargs_.size() + 1); 1388 | for (auto& arg : vargs_) cargv_.push_back(&arg[0]); 1389 | cargv_.push_back(nullptr); 1390 | } 1391 | 1392 | inline void Popen::start_process() noexcept(false) 1393 | { 1394 | // The process was started/tried to be started 1395 | // in the constructor itself. 1396 | // For explicitly calling this API to start the 1397 | // process, 'defer_spawn' argument must be set to 1398 | // true in the constructor. 1399 | if (!defer_process_start_) { 1400 | assert (0); 1401 | return; 1402 | } 1403 | execute_process(); 1404 | } 1405 | 1406 | inline int Popen::wait() noexcept(false) 1407 | { 1408 | #ifdef __USING_WINDOWS__ 1409 | int ret = WaitForSingleObject(process_handle_, INFINITE); 1410 | 1411 | return 0; 1412 | #else 1413 | int ret, status; 1414 | std::tie(ret, status) = util::wait_for_child_exit(pid()); 1415 | if (ret == -1) { 1416 | if (errno != ECHILD) throw OSError("waitpid failed", errno); 1417 | return 0; 1418 | } 1419 | if (WIFEXITED(status)) return WEXITSTATUS(status); 1420 | if (WIFSIGNALED(status)) return WTERMSIG(status); 1421 | else return 255; 1422 | 1423 | return 0; 1424 | #endif 1425 | } 1426 | 1427 | inline int Popen::poll() noexcept(false) 1428 | { 1429 | #ifdef __USING_WINDOWS__ 1430 | int ret = WaitForSingleObject(process_handle_, 0); 1431 | if (ret != WAIT_OBJECT_0) return -1; 1432 | 1433 | DWORD dretcode_; 1434 | if (FALSE == GetExitCodeProcess(process_handle_, &dretcode_)) 1435 | throw OSError("GetExitCodeProcess", 0); 1436 | 1437 | retcode_ = (int)dretcode_; 1438 | CloseHandle(process_handle_); 1439 | 1440 | return retcode_; 1441 | #else 1442 | if (!child_created_) return -1; // TODO: ?? 1443 | 1444 | int status; 1445 | 1446 | // Returns zero if child is still running 1447 | int ret = waitpid(child_pid_, &status, WNOHANG); 1448 | if (ret == 0) return -1; 1449 | 1450 | if (ret == child_pid_) { 1451 | if (WIFSIGNALED(status)) { 1452 | retcode_ = WTERMSIG(status); 1453 | } else if (WIFEXITED(status)) { 1454 | retcode_ = WEXITSTATUS(status); 1455 | } else { 1456 | retcode_ = 255; 1457 | } 1458 | return retcode_; 1459 | } 1460 | 1461 | if (ret == -1) { 1462 | // From subprocess.py 1463 | // This happens if SIGCHLD is set to be ignored 1464 | // or waiting for child process has otherwise been disabled 1465 | // for our process. This child is dead, we cannot get the 1466 | // status. 1467 | if (errno == ECHILD) retcode_ = 0; 1468 | else throw OSError("waitpid failed", errno); 1469 | } else { 1470 | retcode_ = ret; 1471 | } 1472 | 1473 | return retcode_; 1474 | #endif 1475 | } 1476 | 1477 | inline void Popen::kill(int sig_num) 1478 | { 1479 | #ifdef __USING_WINDOWS__ 1480 | if (!TerminateProcess(this->process_handle_, (UINT)sig_num)) { 1481 | throw OSError("TerminateProcess", 0); 1482 | } 1483 | #else 1484 | if (session_leader_) killpg(child_pid_, sig_num); 1485 | else ::kill(child_pid_, sig_num); 1486 | #endif 1487 | } 1488 | 1489 | 1490 | inline void Popen::execute_process() noexcept(false) 1491 | { 1492 | #ifdef __USING_WINDOWS__ 1493 | if (this->shell_) { 1494 | throw OSError("shell not currently supported on windows", 0); 1495 | } 1496 | 1497 | void* environment_string_table_ptr = nullptr; 1498 | env_vector_t environment_string_vector; 1499 | if(this->env_.size()){ 1500 | environment_string_vector = util::CreateUpdatedWindowsEnvironmentVector(env_); 1501 | environment_string_table_ptr = (void*)environment_string_vector.data(); 1502 | } 1503 | 1504 | if (exe_name_.length()) { 1505 | this->vargs_.insert(this->vargs_.begin(), this->exe_name_); 1506 | this->populate_c_argv(); 1507 | } 1508 | this->exe_name_ = vargs_[0]; 1509 | 1510 | std::wstring_convert> converter; 1511 | std::wstring argument; 1512 | std::wstring command_line; 1513 | 1514 | for (auto arg : this->vargs_) { 1515 | argument = converter.from_bytes(arg); 1516 | util::quote_argument(argument, command_line, false); 1517 | command_line += L" "; 1518 | } 1519 | 1520 | // CreateProcessW can modify szCmdLine so we allocate needed memory 1521 | wchar_t *szCmdline = new wchar_t[command_line.size() + 1]; 1522 | wcscpy_s(szCmdline, command_line.size() + 1, command_line.c_str()); 1523 | PROCESS_INFORMATION piProcInfo; 1524 | STARTUPINFOW siStartInfo; 1525 | BOOL bSuccess = FALSE; 1526 | DWORD creation_flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW; 1527 | 1528 | // Set up members of the PROCESS_INFORMATION structure. 1529 | ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); 1530 | 1531 | // Set up members of the STARTUPINFOW structure. 1532 | // This structure specifies the STDIN and STDOUT handles for redirection. 1533 | 1534 | ZeroMemory(&siStartInfo, sizeof(STARTUPINFOW)); 1535 | siStartInfo.cb = sizeof(STARTUPINFOW); 1536 | 1537 | siStartInfo.hStdError = this->stream_.g_hChildStd_ERR_Wr; 1538 | siStartInfo.hStdOutput = this->stream_.g_hChildStd_OUT_Wr; 1539 | siStartInfo.hStdInput = this->stream_.g_hChildStd_IN_Rd; 1540 | 1541 | siStartInfo.dwFlags |= STARTF_USESTDHANDLES; 1542 | 1543 | // Create the child process. 1544 | bSuccess = CreateProcessW(NULL, 1545 | szCmdline, // command line 1546 | NULL, // process security attributes 1547 | NULL, // primary thread security attributes 1548 | TRUE, // handles are inherited 1549 | creation_flags, // creation flags 1550 | environment_string_table_ptr, // use provided environment 1551 | NULL, // use parent's current directory 1552 | &siStartInfo, // STARTUPINFOW pointer 1553 | &piProcInfo); // receives PROCESS_INFORMATION 1554 | 1555 | // If an error occurs, exit the application. 1556 | if (!bSuccess) { 1557 | DWORD errorMessageID = ::GetLastError(); 1558 | throw CalledProcessError("CreateProcess failed: " + util::get_last_error(errorMessageID), errorMessageID); 1559 | } 1560 | 1561 | CloseHandle(piProcInfo.hThread); 1562 | 1563 | /* 1564 | TODO: use common apis to close linux handles 1565 | */ 1566 | 1567 | this->process_handle_ = piProcInfo.hProcess; 1568 | 1569 | this->cleanup_future_ = std::async(std::launch::async, [this] { 1570 | WaitForSingleObject(this->process_handle_, INFINITE); 1571 | 1572 | CloseHandle(this->stream_.g_hChildStd_ERR_Wr); 1573 | CloseHandle(this->stream_.g_hChildStd_OUT_Wr); 1574 | CloseHandle(this->stream_.g_hChildStd_IN_Rd); 1575 | }); 1576 | 1577 | /* 1578 | NOTE: In the linux version, there is a check to make sure that the process 1579 | has been started. Here, we do nothing because CreateProcess will throw 1580 | if we fail to create the process. 1581 | */ 1582 | 1583 | 1584 | #else 1585 | 1586 | int err_rd_pipe, err_wr_pipe; 1587 | std::tie(err_rd_pipe, err_wr_pipe) = util::pipe_cloexec(); 1588 | 1589 | if (shell_) { 1590 | auto new_cmd = util::join(vargs_); 1591 | vargs_.clear(); 1592 | vargs_.insert(vargs_.begin(), {"/bin/sh", "-c"}); 1593 | vargs_.push_back(new_cmd); 1594 | populate_c_argv(); 1595 | } 1596 | 1597 | if (exe_name_.length()) { 1598 | vargs_.insert(vargs_.begin(), exe_name_); 1599 | populate_c_argv(); 1600 | } 1601 | exe_name_ = vargs_[0]; 1602 | 1603 | child_pid_ = fork(); 1604 | 1605 | if (child_pid_ < 0) { 1606 | close(err_rd_pipe); 1607 | close(err_wr_pipe); 1608 | throw OSError("fork failed", errno); 1609 | } 1610 | 1611 | child_created_ = true; 1612 | 1613 | if (child_pid_ == 0) 1614 | { 1615 | // Close descriptors belonging to parent 1616 | stream_.close_parent_fds(); 1617 | 1618 | //Close the read end of the error pipe 1619 | close(err_rd_pipe); 1620 | 1621 | detail::Child chld(this, err_wr_pipe); 1622 | chld.execute_child(); 1623 | } 1624 | else 1625 | { 1626 | close (err_wr_pipe);// close child side of pipe, else get stuck in read below 1627 | 1628 | stream_.close_child_fds(); 1629 | 1630 | try { 1631 | char err_buf[SP_MAX_ERR_BUF_SIZ] = {0,}; 1632 | 1633 | int read_bytes = util::read_atmost_n( 1634 | fdopen(err_rd_pipe, "r"), 1635 | err_buf, 1636 | SP_MAX_ERR_BUF_SIZ); 1637 | close(err_rd_pipe); 1638 | 1639 | if (read_bytes || strlen(err_buf)) { 1640 | // Call waitpid to reap the child process 1641 | // waitpid suspends the calling process until the 1642 | // child terminates. 1643 | int retcode = wait(); 1644 | 1645 | // Throw whatever information we have about child failure 1646 | throw CalledProcessError(err_buf, retcode); 1647 | } 1648 | } catch (std::exception& exp) { 1649 | stream_.cleanup_fds(); 1650 | throw; 1651 | } 1652 | 1653 | } 1654 | #endif 1655 | } 1656 | 1657 | namespace detail { 1658 | 1659 | inline void ArgumentDeducer::set_option(executable&& exe) { 1660 | popen_->exe_name_ = std::move(exe.arg_value); 1661 | } 1662 | 1663 | inline void ArgumentDeducer::set_option(cwd&& cwdir) { 1664 | popen_->cwd_ = std::move(cwdir.arg_value); 1665 | } 1666 | 1667 | inline void ArgumentDeducer::set_option(bufsize&& bsiz) { 1668 | popen_->stream_.bufsiz_ = bsiz.bufsiz; 1669 | } 1670 | 1671 | inline void ArgumentDeducer::set_option(environment&& env) { 1672 | popen_->env_ = std::move(env.env_); 1673 | } 1674 | 1675 | inline void ArgumentDeducer::set_option(defer_spawn&& defer) { 1676 | popen_->defer_process_start_ = defer.defer; 1677 | } 1678 | 1679 | inline void ArgumentDeducer::set_option(shell&& sh) { 1680 | popen_->shell_ = sh.shell_; 1681 | } 1682 | 1683 | inline void ArgumentDeducer::set_option(session_leader&& sleader) { 1684 | popen_->session_leader_ = sleader.leader_; 1685 | } 1686 | 1687 | inline void ArgumentDeducer::set_option(input&& inp) { 1688 | if (inp.rd_ch_ != -1) popen_->stream_.read_from_parent_ = inp.rd_ch_; 1689 | if (inp.wr_ch_ != -1) popen_->stream_.write_to_child_ = inp.wr_ch_; 1690 | } 1691 | 1692 | inline void ArgumentDeducer::set_option(output&& out) { 1693 | if (out.wr_ch_ != -1) popen_->stream_.write_to_parent_ = out.wr_ch_; 1694 | if (out.rd_ch_ != -1) popen_->stream_.read_from_child_ = out.rd_ch_; 1695 | } 1696 | 1697 | inline void ArgumentDeducer::set_option(error&& err) { 1698 | if (err.deferred_) { 1699 | if (popen_->stream_.write_to_parent_) { 1700 | popen_->stream_.err_write_ = popen_->stream_.write_to_parent_; 1701 | } else { 1702 | throw std::runtime_error("Set output before redirecting error to output"); 1703 | } 1704 | } 1705 | if (err.wr_ch_ != -1) popen_->stream_.err_write_ = err.wr_ch_; 1706 | if (err.rd_ch_ != -1) popen_->stream_.err_read_ = err.rd_ch_; 1707 | } 1708 | 1709 | inline void ArgumentDeducer::set_option(close_fds&& cfds) { 1710 | popen_->close_fds_ = cfds.close_all; 1711 | } 1712 | 1713 | inline void ArgumentDeducer::set_option(preexec_func&& prefunc) { 1714 | popen_->preexec_fn_ = std::move(prefunc); 1715 | popen_->has_preexec_fn_ = true; 1716 | } 1717 | 1718 | 1719 | inline void Child::execute_child() { 1720 | #ifndef __USING_WINDOWS__ 1721 | int sys_ret = -1; 1722 | auto& stream = parent_->stream_; 1723 | 1724 | try { 1725 | if (stream.write_to_parent_ == 0) 1726 | stream.write_to_parent_ = dup(stream.write_to_parent_); 1727 | 1728 | if (stream.err_write_ == 0 || stream.err_write_ == 1) 1729 | stream.err_write_ = dup(stream.err_write_); 1730 | 1731 | // Make the child owned descriptors as the 1732 | // stdin, stdout and stderr for the child process 1733 | auto _dup2_ = [](int fd, int to_fd) { 1734 | if (fd == to_fd) { 1735 | // dup2 syscall does not reset the 1736 | // CLOEXEC flag if the descriptors 1737 | // provided to it are same. 1738 | // But, we need to reset the CLOEXEC 1739 | // flag as the provided descriptors 1740 | // are now going to be the standard 1741 | // input, output and error 1742 | util::set_clo_on_exec(fd, false); 1743 | } else if(fd != -1) { 1744 | int res = dup2(fd, to_fd); 1745 | if (res == -1) throw OSError("dup2 failed", errno); 1746 | } 1747 | }; 1748 | 1749 | // Create the standard streams 1750 | _dup2_(stream.read_from_parent_, 0); // Input stream 1751 | _dup2_(stream.write_to_parent_, 1); // Output stream 1752 | _dup2_(stream.err_write_, 2); // Error stream 1753 | 1754 | // Close the duped descriptors 1755 | if (stream.read_from_parent_ != -1 && stream.read_from_parent_ > 2) 1756 | close(stream.read_from_parent_); 1757 | 1758 | if (stream.write_to_parent_ != -1 && stream.write_to_parent_ > 2) 1759 | close(stream.write_to_parent_); 1760 | 1761 | if (stream.err_write_ != -1 && stream.err_write_ > 2) 1762 | close(stream.err_write_); 1763 | 1764 | // Close all the inherited fd's except the error write pipe 1765 | if (parent_->close_fds_) { 1766 | int max_fd = sysconf(_SC_OPEN_MAX); 1767 | if (max_fd == -1) throw OSError("sysconf failed", errno); 1768 | 1769 | for (int i = 3; i < max_fd; i++) { 1770 | if (i == err_wr_pipe_) continue; 1771 | close(i); 1772 | } 1773 | } 1774 | 1775 | // Change the working directory if provided 1776 | if (parent_->cwd_.length()) { 1777 | sys_ret = chdir(parent_->cwd_.c_str()); 1778 | if (sys_ret == -1) throw OSError("chdir failed", errno); 1779 | } 1780 | 1781 | if (parent_->has_preexec_fn_) { 1782 | parent_->preexec_fn_(); 1783 | } 1784 | 1785 | if (parent_->session_leader_) { 1786 | sys_ret = setsid(); 1787 | if (sys_ret == -1) throw OSError("setsid failed", errno); 1788 | } 1789 | 1790 | // Replace the current image with the executable 1791 | if (parent_->env_.size()) { 1792 | for (auto& kv : parent_->env_) { 1793 | setenv(kv.first.c_str(), kv.second.c_str(), 1); 1794 | } 1795 | sys_ret = execvp(parent_->exe_name_.c_str(), parent_->cargv_.data()); 1796 | } else { 1797 | sys_ret = execvp(parent_->exe_name_.c_str(), parent_->cargv_.data()); 1798 | } 1799 | 1800 | if (sys_ret == -1) throw OSError("execve failed", errno); 1801 | 1802 | } catch (const OSError& exp) { 1803 | // Just write the exception message 1804 | // TODO: Give back stack trace ? 1805 | std::string err_msg(exp.what()); 1806 | //ATTN: Can we do something on error here ? 1807 | util::write_n(err_wr_pipe_, err_msg.c_str(), err_msg.length()); 1808 | } 1809 | 1810 | // Calling application would not get this 1811 | // exit failure 1812 | _exit (EXIT_FAILURE); 1813 | #endif 1814 | } 1815 | 1816 | 1817 | inline void Streams::setup_comm_channels() 1818 | { 1819 | #ifdef __USING_WINDOWS__ 1820 | util::configure_pipe(&this->g_hChildStd_IN_Rd, &this->g_hChildStd_IN_Wr, &this->g_hChildStd_IN_Wr); 1821 | this->input(util::file_from_handle(this->g_hChildStd_IN_Wr, "w")); 1822 | this->write_to_child_ = _fileno(this->input()); 1823 | 1824 | util::configure_pipe(&this->g_hChildStd_OUT_Rd, &this->g_hChildStd_OUT_Wr, &this->g_hChildStd_OUT_Rd); 1825 | this->output(util::file_from_handle(this->g_hChildStd_OUT_Rd, "r")); 1826 | this->read_from_child_ = _fileno(this->output()); 1827 | 1828 | util::configure_pipe(&this->g_hChildStd_ERR_Rd, &this->g_hChildStd_ERR_Wr, &this->g_hChildStd_ERR_Rd); 1829 | this->error(util::file_from_handle(this->g_hChildStd_ERR_Rd, "r")); 1830 | this->err_read_ = _fileno(this->error()); 1831 | #else 1832 | 1833 | if (write_to_child_ != -1) input(fdopen(write_to_child_, "wb")); 1834 | if (read_from_child_ != -1) output(fdopen(read_from_child_, "rb")); 1835 | if (err_read_ != -1) error(fdopen(err_read_, "rb")); 1836 | 1837 | auto handles = {input(), output(), error()}; 1838 | 1839 | for (auto& h : handles) { 1840 | if (h == nullptr) continue; 1841 | switch (bufsiz_) { 1842 | case 0: 1843 | setvbuf(h, nullptr, _IONBF, BUFSIZ); 1844 | break; 1845 | case 1: 1846 | setvbuf(h, nullptr, _IONBF, BUFSIZ); 1847 | break; 1848 | default: 1849 | setvbuf(h, nullptr, _IOFBF, bufsiz_); 1850 | }; 1851 | } 1852 | #endif 1853 | } 1854 | 1855 | inline int Communication::send(const char* msg, size_t length) 1856 | { 1857 | if (stream_->input() == nullptr) return -1; 1858 | return std::fwrite(msg, sizeof(char), length, stream_->input()); 1859 | } 1860 | 1861 | inline int Communication::send(const std::vector& msg) 1862 | { 1863 | return send(msg.data(), msg.size()); 1864 | } 1865 | 1866 | inline std::pair 1867 | Communication::communicate(const char* msg, size_t length) 1868 | { 1869 | // Optimization from subprocess.py 1870 | // If we are using one pipe, or no pipe 1871 | // at all, using select() or threads is unnecessary. 1872 | auto hndls = {stream_->input(), stream_->output(), stream_->error()}; 1873 | int count = std::count(std::begin(hndls), std::end(hndls), nullptr); 1874 | const int len_conv = length; 1875 | 1876 | if (count >= 2) { 1877 | OutBuffer obuf; 1878 | ErrBuffer ebuf; 1879 | if (stream_->input()) { 1880 | if (msg) { 1881 | int wbytes = std::fwrite(msg, sizeof(char), length, stream_->input()); 1882 | if (wbytes < len_conv) { 1883 | if (errno != EPIPE && errno != EINVAL) { 1884 | throw OSError("fwrite error", errno); 1885 | } 1886 | } 1887 | } 1888 | // Close the input stream 1889 | stream_->input_.reset(); 1890 | } else if (stream_->output()) { 1891 | // Read till EOF 1892 | // ATTN: This could be blocking, if the process 1893 | // at the other end screws up, we get screwed as well 1894 | obuf.add_cap(out_buf_cap_); 1895 | 1896 | int rbytes = util::read_all( 1897 | stream_->output(), 1898 | obuf.buf); 1899 | 1900 | if (rbytes == -1) { 1901 | throw OSError("read to obuf failed", errno); 1902 | } 1903 | 1904 | obuf.length = rbytes; 1905 | // Close the output stream 1906 | stream_->output_.reset(); 1907 | 1908 | } else if (stream_->error()) { 1909 | // Same screwness applies here as well 1910 | ebuf.add_cap(err_buf_cap_); 1911 | 1912 | int rbytes = util::read_atmost_n( 1913 | stream_->error(), 1914 | ebuf.buf.data(), 1915 | ebuf.buf.size()); 1916 | 1917 | if (rbytes == -1) { 1918 | throw OSError("read to ebuf failed", errno); 1919 | } 1920 | 1921 | ebuf.length = rbytes; 1922 | // Close the error stream 1923 | stream_->error_.reset(); 1924 | } 1925 | return std::make_pair(std::move(obuf), std::move(ebuf)); 1926 | } 1927 | 1928 | return communicate_threaded(msg, length); 1929 | } 1930 | 1931 | 1932 | inline std::pair 1933 | Communication::communicate_threaded(const char* msg, size_t length) 1934 | { 1935 | OutBuffer obuf; 1936 | ErrBuffer ebuf; 1937 | std::future out_fut, err_fut; 1938 | const int length_conv = length; 1939 | 1940 | if (stream_->output()) { 1941 | obuf.add_cap(out_buf_cap_); 1942 | 1943 | out_fut = std::async(std::launch::async, 1944 | [&obuf, this] { 1945 | return util::read_all(this->stream_->output(), obuf.buf); 1946 | }); 1947 | } 1948 | if (stream_->error()) { 1949 | ebuf.add_cap(err_buf_cap_); 1950 | 1951 | err_fut = std::async(std::launch::async, 1952 | [&ebuf, this] { 1953 | return util::read_all(this->stream_->error(), ebuf.buf); 1954 | }); 1955 | } 1956 | if (stream_->input()) { 1957 | if (msg) { 1958 | int wbytes = std::fwrite(msg, sizeof(char), length, stream_->input()); 1959 | if (wbytes < length_conv) { 1960 | if (errno != EPIPE && errno != EINVAL) { 1961 | throw OSError("fwrite error", errno); 1962 | } 1963 | } 1964 | } 1965 | stream_->input_.reset(); 1966 | } 1967 | 1968 | if (out_fut.valid()) { 1969 | int res = out_fut.get(); 1970 | if (res != -1) obuf.length = res; 1971 | else obuf.length = 0; 1972 | } 1973 | if (err_fut.valid()) { 1974 | int res = err_fut.get(); 1975 | if (res != -1) ebuf.length = res; 1976 | else ebuf.length = 0; 1977 | } 1978 | 1979 | return std::make_pair(std::move(obuf), std::move(ebuf)); 1980 | } 1981 | 1982 | } // end namespace detail 1983 | 1984 | 1985 | 1986 | // Convenience Functions 1987 | // 1988 | // 1989 | namespace detail 1990 | { 1991 | template 1992 | OutBuffer check_output_impl(F& farg, Args&&... args) 1993 | { 1994 | static_assert(!detail::has_type>::value, "output not allowed in args"); 1995 | auto p = Popen(std::forward(farg), std::forward(args)..., output{PIPE}); 1996 | auto res = p.communicate(); 1997 | auto retcode = p.retcode(); 1998 | if (retcode > 0) { 1999 | throw CalledProcessError("Command failed : Non zero retcode", retcode); 2000 | } 2001 | return std::move(res.first); 2002 | } 2003 | 2004 | template 2005 | int call_impl(F& farg, Args&&... args) 2006 | { 2007 | return Popen(std::forward(farg), std::forward(args)...).wait(); 2008 | } 2009 | 2010 | static inline void pipeline_impl(std::vector& cmds) 2011 | { 2012 | /* EMPTY IMPL */ 2013 | } 2014 | 2015 | template 2016 | static inline void pipeline_impl(std::vector& cmds, 2017 | const std::string& cmd, 2018 | Args&&... args) 2019 | { 2020 | if (cmds.size() == 0) { 2021 | cmds.emplace_back(cmd, output{PIPE}, defer_spawn{true}); 2022 | } else { 2023 | cmds.emplace_back(cmd, input{cmds.back().output()}, output{PIPE}, defer_spawn{true}); 2024 | } 2025 | 2026 | pipeline_impl(cmds, std::forward(args)...); 2027 | } 2028 | 2029 | } 2030 | 2031 | /*----------------------------------------------------------- 2032 | * CONVIENIENCE FUNCTIONS 2033 | *----------------------------------------------------------- 2034 | */ 2035 | 2036 | 2037 | /*! 2038 | * Run the command with arguments and wait for it to complete. 2039 | * The parameters passed to the argument are exactly the same 2040 | * one would use for Popen constructors. 2041 | */ 2042 | template 2043 | int call(std::initializer_list plist, Args&&... args) 2044 | { 2045 | return (detail::call_impl(plist, std::forward(args)...)); 2046 | } 2047 | 2048 | template 2049 | int call(const std::string& arg, Args&&... args) 2050 | { 2051 | return (detail::call_impl(arg, std::forward(args)...)); 2052 | } 2053 | 2054 | template 2055 | int call(std::vector plist, Args &&... args) 2056 | { 2057 | return (detail::call_impl(plist, std::forward(args)...)); 2058 | } 2059 | 2060 | 2061 | /*! 2062 | * Run the command with arguments and wait for it to complete. 2063 | * If the exit code was non-zero it raises a CalledProcessError. 2064 | * The arguments are the same as for the Popen constructor. 2065 | */ 2066 | template 2067 | OutBuffer check_output(std::initializer_list plist, Args&&... args) 2068 | { 2069 | return (detail::check_output_impl(plist, std::forward(args)...)); 2070 | } 2071 | 2072 | template 2073 | OutBuffer check_output(const std::string& arg, Args&&... args) 2074 | { 2075 | return (detail::check_output_impl(arg, std::forward(args)...)); 2076 | } 2077 | 2078 | template 2079 | OutBuffer check_output(std::vector plist, Args &&... args) 2080 | { 2081 | return (detail::check_output_impl(plist, std::forward(args)...)); 2082 | } 2083 | 2084 | 2085 | /*! 2086 | * An easy way to pipeline easy commands. 2087 | * Provide the commands that needs to be pipelined in the order they 2088 | * would appear in a regular command. 2089 | * It would wait for the last command provided in the pipeline 2090 | * to finish and then return the OutBuffer. 2091 | */ 2092 | template 2093 | // Args expected to be flat commands using string instead of initializer_list 2094 | OutBuffer pipeline(Args&&... args) 2095 | { 2096 | std::vector pcmds; 2097 | detail::pipeline_impl(pcmds, std::forward(args)...); 2098 | 2099 | for (auto& p : pcmds) p.start_process(); 2100 | return (pcmds.back().communicate().first); 2101 | } 2102 | 2103 | } 2104 | 2105 | #endif // SUBPROCESS_HPP 2106 | --------------------------------------------------------------------------------