/* * Walkbot.cpp * * Created on: Jul 23, 2017 * Author: nullifiedcat */ #include "common.hpp" #include "hack.hpp" #include #include #include #include namespace hacks { namespace shared { namespace walkbot { using index_t = unsigned; using connection = uint8_t; constexpr unsigned VERSION = 2; constexpr index_t BAD_NODE = unsigned(-1); constexpr connection MAX_CONNECTIONS = 6; constexpr connection BAD_CONNECTION = uint8_t(-1); index_t CreateNode(const Vector &xyz); void DeleteNode(index_t node); float distance_2d(Vector &xyz); void Save(std::string filename); bool Load(std::string filename); const char *prevlvlname = ""; enum ENodeFlags { NF_GOOD = (1 << 0), NF_DUCK = (1 << 1), NF_JUMP = (1 << 2) }; enum EWalkbotState { WB_DISABLED, WB_RECORDING, WB_EDITING, WB_REPLAYING }; struct walkbot_header_s { unsigned version{ VERSION }; size_t node_count{ 0 }; size_t map_length{ 0 }; size_t author_length{ 0 }; }; enum EConnectionFlags { CF_GOOD = (1 << 0), CF_LOW_HEALTH = (1 << 1), CF_LOW_AMMO = (1 << 2), CF_RED = (1 << 3), CF_BLU = (1 << 4), CF_CAPPED_1 = (1 << 5), CF_CAPPED_2 = (1 << 6), CF_CAPPED_3 = (1 << 7), CF_CAPPED_4 = (1 << 8), CF_CAPPED_5 = (1 << 9) }; struct connection_s { index_t node{ BAD_NODE }; unsigned flags{ 0 }; void link(index_t a) { flags |= CF_GOOD; node = a; } void unlink() { flags = 0; node = BAD_NODE; } bool good() const { return (flags & CF_GOOD); } bool free() const { return not good(); } }; struct walkbot_node_s { float x{ 0 }; float y{ 0 }; float z{ 0 }; unsigned flags{ 0 }; connection_s connections[MAX_CONNECTIONS]; Vector &xyz() { return *reinterpret_cast(&x); } connection free_connection() const { for (connection i = 0; i < MAX_CONNECTIONS; i++) { if (connections[i].free()) return i; } return BAD_CONNECTION; } void link(index_t node) { connection free = free_connection(); if (free == BAD_CONNECTION) { logging::Info("[wb] Too many connections! Node at (%.2f %.2f %.2f)", x, y, z); return; } connections[free].link(node); } void unlink(index_t node) { for (connection i = 0; i < MAX_CONNECTIONS; i++) { if (connections[i].good() and connections[i].node == node) { connections[i].unlink(); } } } }; // 40 float distance_2d(Vector &xyz) { float dx = xyz.x - g_pLocalPlayer->v_Origin.x; float dy = xyz.y - g_pLocalPlayer->v_Origin.y; return sqrt(dx * dx + dy * dy); } namespace state { index_t free_node(); // A vector containing all loaded nodes, used in both recording and replaying std::vector nodes{}; bool node_good(index_t node) { return node != BAD_NODE && node < nodes.size() && (nodes[node].flags & NF_GOOD); } // Target node when replaying, selected node when editing, last node when // recording index_t active_node{ BAD_NODE }; walkbot_node_s *active() { if (node_good(active_node)) return &nodes[active_node]; return nullptr; } // Last reached node when replaying index_t last_node{ BAD_NODE }; walkbot_node_s *last() { if (node_good(last_node)) return &nodes[last_node]; return nullptr; } // Node closest to your crosshair when editing index_t closest_node{ BAD_NODE }; walkbot_node_s *closest() { if (node_good(closest_node)) return &nodes[closest_node]; return nullptr; } // Global state EWalkbotState state{ WB_DISABLED }; // g_pUserCmd->buttons state when last node was recorded int last_node_buttons{ 0 }; // Set to true when bot is moving to nearest node after dying/losing its active // node bool recovery{ true }; // Time when bot started to move towards next point std::chrono::system_clock::time_point time{}; // A little bit too expensive function, finds next free node or creates one if // no free slots exist index_t free_node() { for (index_t i = 0; i < nodes.size(); i++) { if (not(nodes[i].flags & NF_GOOD)) return i; } nodes.emplace_back(); return nodes.size() - 1; } } using state::nodes; using state::node_good; bool HasLowAmmo() { int *weapon_list = (int *) ((unsigned) (RAW_ENT(LOCAL_E)) + netvar.hMyWeapons); for (int i = 0; weapon_list[i]; i++) { int handle = weapon_list[i]; int eid = handle & 0xFFF; if (eid >= 32 && eid <= HIGHEST_ENTITY) { IClientEntity *weapon = g_IEntityList->GetClientEntity(eid); if (weapon and re::C_BaseCombatWeapon::IsBaseCombatWeapon(weapon) and re::C_TFWeaponBase::UsesPrimaryAmmo(weapon) and not re::C_TFWeaponBase::HasPrimaryAmmo(weapon)) { return true; } } } return false; } bool HasLowHealth() { return float(LOCAL_E->m_iHealth) / float(LOCAL_E->m_iMaxHealth) < 0.45; } void DeleteNode(index_t node) { if (not node_good(node)) return; logging::Info("[wb] Deleting node %u", node); auto &n = nodes[node]; for (size_t i = 0; i < MAX_CONNECTIONS; i++) { if (n.connections[i].good() and node_good(n.connections[i].node)) { nodes[n.connections[i].node].unlink(node); } } memset(&n, 0, sizeof(walkbot_node_s)); } #define BINARY_FILE_WRITE(handle, data) \ handle.write(reinterpret_cast(&data), sizeof(data)) #define BINARY_FILE_READ(handle, data) \ handle.read(reinterpret_cast(&data), sizeof(data)) void Save(std::string filename) { if (g_Settings.bInvalid) { logging::Info("Not in-game, cannot save!"); return; } { DIR *walkbot_dir = opendir(DATA_PATH "/walkbot"); if (!walkbot_dir) { logging::Info("Walkbot directory doesn't exist, creating one!"); mkdir(DATA_PATH "/walkbot", S_IRWXU | S_IRWXG); } else closedir(walkbot_dir); } std::string path = format(DATA_PATH "/walkbot/", GetLevelName()); { DIR *level_dir = opendir(path.c_str()); if (!level_dir) { logging::Info( "Walkbot directory for %s doesn't exist, creating one!", GetLevelName().c_str()); mkdir(path.c_str(), S_IRWXU | S_IRWXG); } else closedir(level_dir); } logging::Info("Saving in %s", format(path, "/", filename).c_str()); try { std::ofstream file(format(path, "/", filename), std::ios::out | std::ios::binary); if (not file) { logging::Info("Could not open file!"); return; } walkbot_header_s header; header.node_count = state::nodes.size(); const char *name = g_ISteamFriends->GetPersonaName(); const char *map = g_IEngine->GetLevelName(); size_t name_s = strlen(name); size_t map_s = strlen(map); header.author_length = name_s; header.map_length = map_s; BINARY_FILE_WRITE(file, header); file.write(map, map_s); file.write(name, name_s); file.write(reinterpret_cast(state::nodes.data()), sizeof(walkbot_node_s) * header.node_count); file.close(); logging::Info("Writing successful"); } catch (std::exception &e) { logging::Info("Writing unsuccessful: %s", e.what()); } } bool Load(std::string filename) { { DIR *walkbot_dir = opendir(DATA_PATH "/walkbot"); if (!walkbot_dir) { logging::Info("Walkbot directory doesn't exist!"); return false; } else closedir(walkbot_dir); } std::string path = format(DATA_PATH "/walkbot/", GetLevelName()); { DIR *level_dir = opendir(path.c_str()); if (!level_dir) { logging::Info( "Walkbot directory for %s doesn't exist, creating one!", GetLevelName().c_str()); mkdir(path.c_str(), S_IRWXU | S_IRWXG); } else closedir(level_dir); } try { std::ifstream file(format(path, "/", filename), std::ios::in | std::ios::binary); if (!file) { return false; } walkbot_header_s header; BINARY_FILE_READ(file, header); // FIXME magic number: 1 if (header.version != VERSION) { logging::Info("Outdated/corrupted walkbot file! Cannot load this."); file.close(); return false; } if (header.author_length > 64 or header.map_length > 512 or (not header.author_length or not header.map_length)) { logging::Info("Corrupted author/level data"); } else { char name_buffer[header.author_length + 1]; char map_buffer[header.map_length + 1]; file.read(map_buffer, header.map_length); file.read(name_buffer, header.author_length); name_buffer[header.author_length] = 0; map_buffer[header.map_length] = 0; logging::Info("Walkbot navigation map for %s\nAuthor: %s", map_buffer, name_buffer); } state::nodes.clear(); logging::Info("Reading %i entries...", header.node_count); if (header.node_count > 32768) { logging::Info("Read %d nodes, max is %d. Aborting.", header.node_count, 32768); return false; } state::nodes.resize(header.node_count); file.read(reinterpret_cast(state::nodes.data()), sizeof(walkbot_node_s) * header.node_count); file.close(); logging::Info("Reading successful! Result: %i entries.", state::nodes.size()); return true; } catch (std::exception &e) { logging::Info("Reading unsuccessful: %s", e.what()); } return false; } static CatCommand save("wb_save", "Save", [](const CCommand &args) { logging::Info("Saving"); std::string filename = "default"; if (args.ArgC() > 1) { filename = args.Arg(1); } Save(filename); }); static CatCommand load("wb_load", "Load", [](const CCommand &args) { logging::Info("Loading"); std::string filename = "default"; if (args.ArgC() > 1) { filename = args.Arg(1); } Load(filename); }); index_t CreateNode(const Vector &xyz) { index_t node = state::free_node(); logging::Info("[wb] Creating node %u at (%.2f %.2f %.2f)", node, xyz.x, xyz.y, xyz.z); auto &n = state::nodes[node]; memset(&n, 0, sizeof(n)); n.xyz() = xyz; n.flags |= NF_GOOD; return node; } static CatVar active_recording(CV_SWITCH, "wb_recording", "0", "Do recording", "Use BindToggle with this"); static CatVar draw_info(CV_SWITCH, "wb_info", "1", "Walkbot info"); static CatVar draw_path(CV_SWITCH, "wb_path", "1", "Walkbot path"); static CatVar draw_nodes(CV_SWITCH, "wb_nodes", "1", "Walkbot nodes"); static CatVar draw_indices(CV_SWITCH, "wb_indices", "0", "Node indices"); static CatVar free_move(CV_SWITCH, "wb_freemove", "1", "Allow free movement", "Allow free movement while pressing movement keys"); static CatVar spawn_distance(CV_FLOAT, "wb_node_spawn_distance", "54", "Node spawn distance"); static CatVar max_distance(CV_FLOAT, "wb_replay_max_distance", "100", "Max distance to node when replaying"); static CatVar reach_distance( CV_FLOAT, "wb_replay_reach_distance", "32", "Distance where bot can be considered 'stepping' on the node"); static CatVar draw_connection_flags(CV_SWITCH, "wb_connection_flags", "1", "Connection flags"); static CatVar force_slot(CV_INT, "wb_force_slot", "1", "Force slot", "Walkbot will always select weapon in this slot"); static CatVar leave_if_empty(CV_SWITCH, "wb_leave_if_empty", "0", "Leave if no walkbot", "Leave game if there is no walkbot map"); CatCommand c_start_recording("wb_record", "Start recording", []() { state::state = WB_RECORDING; }); CatCommand c_start_editing("wb_edit", "Start editing", []() { state::state = WB_EDITING; }); CatCommand c_start_replaying("wb_replay", "Start replaying", []() { state::last_node = state::active_node; state::active_node = state::closest_node; state::state = WB_REPLAYING; }); CatCommand c_exit("wb_exit", "Exit", []() { state::state = WB_DISABLED; }); // Selects closest node, clears selection if node is selected CatCommand c_select_node("wb_select", "Select node", []() { if (state::active_node == state::closest_node) { state::active_node = BAD_NODE; } else { state::active_node = state::closest_node; } }); // Makes a new node in the middle of connection between 2 nodes CatCommand c_split_connection("wb_split", "Split connection", []() { if (not(state::node_good(state::active_node) and state::node_good(state::closest_node))) return; if (state::active_node == state::closest_node) return; auto &a = state::nodes[state::active_node]; auto &b = state::nodes[state::closest_node]; a.unlink(state::closest_node); b.unlink(state::active_node); index_t node = CreateNode((a.xyz() + b.xyz()) / 2); auto &n = state::nodes[node]; a.link(node); n.link(state::active_node); b.link(node); n.link(state::closest_node); }); // Deletes closest node and its connections CatCommand c_delete_node("wb_delete", "Delete node", []() { DeleteNode(state::closest_node); }); // Creates a new node under your feet and connects it to closest node to your // crosshair CatCommand c_create_node("wb_create", "Create node", []() { index_t node = CreateNode(g_pLocalPlayer->v_Origin); auto &n = state::nodes[node]; if (g_pUserCmd->buttons & IN_DUCK) n.flags |= NF_DUCK; if (state::node_good(state::closest_node)) { auto &c = state::nodes[state::closest_node]; n.link(state::closest_node); c.link(node); logging::Info("[wb] Node %u linked to node %u at (%.2f %.2f %.2f)", node, state::closest_node, c.x, c.y, c.z); } }); // Connects selected node to closest one CatCommand c_connect_node("wb_connect", "Connect nodes", []() { if (not(state::node_good(state::active_node) and state::node_good(state::closest_node))) return; // Don't link a node to itself, idiot if (state::active_node == state::closest_node) return; auto &a = state::nodes[state::active_node]; auto &b = state::nodes[state::closest_node]; a.link(state::closest_node); b.link(state::active_node); }); // Makes a one-way connection CatCommand c_connect_single_node("wb_connect_single", "Connect nodes (one-way)", []() { if (not(state::node_good(state::active_node) and state::node_good(state::closest_node))) return; // Don't link a node to itself, idiot if (state::active_node == state::closest_node) return; auto &a = state::nodes[state::active_node]; a.link(state::closest_node); }); // Connects selected node to closest one CatCommand c_disconnect_node("wb_disconnect", "Disconnect nodes", []() { if (not(state::node_good(state::active_node) and state::node_good(state::closest_node))) return; // Don't link a node to itself, idiot if (state::active_node == state::closest_node) return; auto &a = state::nodes[state::active_node]; auto &b = state::nodes[state::closest_node]; a.unlink(state::closest_node); b.unlink(state::active_node); }); // Makes a one-way connection CatCommand c_disconnect_single_node( "wb_disconnect_single", "Connect nodes (one-way)", []() { if (not(state::node_good(state::active_node) and state::node_good(state::closest_node))) return; // Don't link a node to itself, idiot if (state::active_node == state::closest_node) return; auto &a = state::nodes[state::active_node]; a.unlink(state::closest_node); }); // Toggles jump flag on closest node CatCommand c_update_duck("wb_duck", "Toggle duck flag", []() { if (not state::node_good(state::closest_node)) return; auto &n = state::nodes[state::closest_node]; if (n.flags & NF_DUCK) n.flags &= ~NF_DUCK; else n.flags |= NF_DUCK; }); // Toggles jump flag on closest node CatCommand c_update_jump("wb_jump", "Toggle jump flag", []() { if (not state::node_good(state::closest_node)) return; auto &n = state::nodes[state::closest_node]; if (n.flags & NF_JUMP) n.flags &= ~NF_JUMP; else n.flags |= NF_JUMP; }); // Assuming node is good and conn is in range [0; MAX_CONNECTIONS) std::string DescribeConnection(index_t node, connection conn) { const connection_s &c = nodes[node].connections[conn]; std::string extra; bool broken = not node_good(c.node); bool oneway = true; if (not broken) { auto &n = state::nodes[c.node]; for (size_t j = 0; j < MAX_CONNECTIONS; j++) { if (n.connections[j].good() and n.connections[j].node == node) { oneway = false; break; } } if (c.flags & CF_LOW_AMMO) { extra += "A"; } if (c.flags & CF_LOW_HEALTH) { extra += "H"; } } std::string result = format(node, ' ', (broken ? "-x>" : (oneway ? "-->" : "<->")), ' ', c.node, ' ', extra); return result; } CatCommand c_toggle_cf_ammo( "wb_conn_ammo", "Toggle 'ammo' flag on connection from ACTIVE to CLOSEST node", []() { auto a = state::active(); auto b = state::closest(); if (not(a and b)) return; for (connection i = 0; i < MAX_CONNECTIONS; i++) { auto &c = a->connections[i]; if (c.free()) continue; if (c.node != state::closest_node) continue; // Actually flip the flag if (c.flags & CF_LOW_AMMO) c.flags &= ~CF_LOW_AMMO; else c.flags |= CF_LOW_AMMO; } }); CatCommand c_toggle_cf_health( "wb_conn_health", "Toggle 'health' flag on connection from ACTIVE to CLOSEST node", []() { auto a = state::active(); auto b = state::closest(); if (not(a and b)) return; for (connection i = 0; i < MAX_CONNECTIONS; i++) { auto &c = a->connections[i]; if (c.free()) continue; if (c.node != state::closest_node) continue; // Actually flip the flag if (c.flags & CF_LOW_HEALTH) c.flags &= ~CF_LOW_HEALTH; else c.flags |= CF_LOW_HEALTH; } }); // Displays all info about closest node and its connections CatCommand c_info("wb_dump", "Show info", []() { index_t node = state::closest_node; if (not node_good(node)) return; auto &n = nodes[node]; logging::Info("[wb] Info about node %u", node); logging::Info("[wb] Flags: Duck=%d, Jump=%d, Raw=%u", n.flags & NF_DUCK, n.flags & NF_JUMP, n.flags); logging::Info("[wb] X: %.2f | Y: %.2f | Z: %.2f", n.x, n.y, n.z); logging::Info("[wb] Connections:"); for (size_t i = 0; i < MAX_CONNECTIONS; i++) { if (n.connections[i].free()) continue; logging::Info("[wb] %s", DescribeConnection(node, i).c_str()); } }); // Deletes a whole region of nodes // Deletes a single closest node if no node is selected CatCommand c_delete_region("wb_delete_region", "Delete region of nodes", []() { logging::Info("< DISABLED >"); /*index_t a = state::active_node; index_t b = state::closest_node; if (not (state::node_good(a) and state::node_good(b))) return; index_t current = state::closest_node; index_t next = INVALID_NODE; do { auto& n = state::nodes[current]; if (n.connection_count > 2) { logging::Info("[wb] More than 2 connections on a node! Quitting."); return; } bool found_next = false; for (size_t i = 0; i < 2; i++) { if (n.connections[i] != current) { next = n.connections[i]; found_next = true; } } DeleteNode(current); current = next; if (not found_next) { logging::Info("[wb] Dead end? Can't find next node after %u", current); break; } } while (state::node_good(current) and (current != a));*/ }); // Clears the state CatCommand c_clear("wb_clear", "Removes all nodes", []() { state::nodes.clear(); }); void Initialize() { } void UpdateClosestNode() { float n_fov = 360.0f; index_t n_idx = BAD_NODE; for (index_t i = 0; i < state::nodes.size(); i++) { auto &node = state::nodes[i]; if (not node.flags & NF_GOOD) continue; // Eclipse shits itself when it sees Vector& beung used as Vector in // GetFov float fov = GetFov(g_pLocalPlayer->v_OrigViewangles, g_pLocalPlayer->v_Eye, node.xyz()); if (fov < n_fov) { n_fov = fov; n_idx = i; } } // Don't select a node if you don't even look at it if (n_fov < 10) state::closest_node = n_idx; else state::closest_node = BAD_NODE; } // Finds nearest node by position, not FOV // Not to be confused with FindClosestNode index_t FindNearestNode(bool traceray) { index_t r_node{ BAD_NODE }; float r_dist{ 65536.0f }; for (index_t i = 0; i < state::nodes.size(); i++) { if (state::node_good(i)) { auto &n = state::nodes[i]; if (traceray and not IsVectorVisible(g_pLocalPlayer->v_Eye, n.xyz())) continue; float dist = distance_2d(n.xyz()); if (dist < r_dist) { r_dist = dist; r_node = i; } } } return r_node; } index_t SelectNextNode() { if (not state::node_good(state::active_node)) { return FindNearestNode(true); } auto &n = state::nodes[state::active_node]; // TODO medkit connections and shit std::vector chance{}; for (index_t i = 0; i < MAX_CONNECTIONS; i++) { if (n.connections[i].good() and n.connections[i].node != state::last_node and node_good(n.connections[i].node)) { if (HasLowAmmo() && (n.connections[i].flags & CF_LOW_AMMO)) { return n.connections[i].node; } if (HasLowHealth() && (n.connections[i].flags & CF_LOW_HEALTH)) { return n.connections[i].node; } if (not(n.connections[i].flags & (CF_LOW_AMMO | CF_LOW_HEALTH))) chance.push_back(n.connections[i].node); } } if (not chance.empty()) { return chance.at(unsigned(rand()) % chance.size()); } else { return BAD_NODE; } return BAD_NODE; } bool free_move_used = false; Timer slot_timer{}; void UpdateSlot() { if (!slot_timer.test_and_set(1000)) return; if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W) && !g_pLocalPlayer->life_state) { IClientEntity *weapon = RAW_ENT(LOCAL_W); // IsBaseCombatWeapon() if (re::C_BaseCombatWeapon::IsBaseCombatWeapon(weapon)) { int slot = re::C_BaseCombatWeapon::GetSlot(weapon); if (slot != int(force_slot) - 1) { hack::ExecuteCommand(format("slot", int(force_slot))); } } } } void UpdateWalker() { free_move_used = false; if (free_move) { if (g_pUserCmd->forwardmove != 0.0f or g_pUserCmd->sidemove != 0.0f) { free_move_used = true; return; } } static int jump_ticks = 0; if (jump_ticks > 0) { g_pUserCmd->buttons |= IN_JUMP; jump_ticks--; } bool timeout = std::chrono::duration_cast( std::chrono::system_clock::now() - state::time) .count() > 1; if (not state::node_good(state::active_node) or timeout) { state::active_node = FindNearestNode(true); state::recovery = true; } auto &n = state::nodes[state::active_node]; WalkTo(n.xyz()); if (state::node_good(state::last_node)) { auto &l = state::nodes[state::last_node]; if (l.flags & NF_DUCK) g_pUserCmd->buttons |= IN_DUCK; } float dist = distance_2d(n.xyz()); if (dist > float(max_distance)) { state::active_node = FindNearestNode(true); state::recovery = true; } if (dist < float(reach_distance)) { state::recovery = false; index_t last = state::active_node; state::active_node = SelectNextNode(); state::last_node = last; state::time = std::chrono::system_clock::now(); if (state::node_good(state::active_node)) { if (state::nodes[state::active_node].flags & NF_JUMP) { g_pUserCmd->buttons |= IN_DUCK; g_pUserCmd->buttons |= IN_JUMP; jump_ticks = 6; } } else { if (not state::recovery) { state::recovery = true; } } } } bool ShouldSpawnNode() { if (not state::node_good(state::active_node)) return true; bool was_jumping = state::last_node_buttons & IN_JUMP; bool is_jumping = g_pUserCmd->buttons & IN_JUMP; if (was_jumping != is_jumping and is_jumping) return true; if ((state::last_node_buttons & IN_DUCK) != (g_pUserCmd->buttons & IN_DUCK)) return true; auto &node = state::nodes[state::active_node]; if (distance_2d(node.xyz()) > float(spawn_distance)) { return true; } return false; } void RecordNode() { index_t node = CreateNode(g_pLocalPlayer->v_Origin); auto &n = state::nodes[node]; if (g_pUserCmd->buttons & IN_DUCK) n.flags |= NF_DUCK; if (g_pUserCmd->buttons & IN_JUMP) n.flags |= NF_JUMP; if (state::node_good(state::active_node)) { auto &c = state::nodes[state::active_node]; n.link(state::active_node); c.link(node); logging::Info("[wb] Node %u auto-linked to node %u at (%.2f %.2f %.2f)", node, state::active_node, c.x, c.y, c.z); } state::last_node_buttons = g_pUserCmd->buttons; state::active_node = node; } #if ENABLE_VISUALS // Draws a single colored connection between 2 nodes void DrawConnection(index_t a, connection_s &b) { if (b.free()) return; if (not(node_good(a) and node_good(b.node))) return; auto &a_ = state::nodes[a]; auto &b_ = state::nodes[b.node]; Vector center = (a_.xyz() + b_.xyz()) / 2; Vector center_connection = (a_.xyz() + center) / 2; Vector wts_a, wts_c, wts_cc; if (not(draw::WorldToScreen(a_.xyz(), wts_a) and draw::WorldToScreen(center, wts_c) and draw::WorldToScreen(center_connection, wts_cc))) return; const rgba_t *color = &colors::white; if ((a_.flags & b_.flags) & NF_JUMP) color = &colors::yellow; else if ((a_.flags & b_.flags) & NF_DUCK) color = &colors::green; draw_api::draw_line(wts_a.x, wts_a.y, wts_c.x - wts_a.x, wts_c.y - wts_a.y, *color, 0.5f); if (draw_connection_flags && b.flags != CF_GOOD) { std::string flags; if (b.flags & CF_LOW_AMMO) flags += "A"; if (b.flags & CF_LOW_HEALTH) flags += "H"; // int size_x = 0, size_y = 0; // FTGL_StringLength(flags, fonts::font_main, &size_x, &size_y); draw_api::draw_string(wts_cc.x, wts_cc.y - 4, flags.c_str(), fonts::main_font, colors::white); } } // Draws a node and its connections void DrawNode(index_t node, bool draw_back) { if (not state::node_good(node)) return; auto &n = state::nodes[node]; for (size_t i = 0; i < MAX_CONNECTIONS; i++) { DrawConnection(node, n.connections[i]); } if (draw_nodes) { const rgba_t *color = &colors::white; if (n.flags & NF_JUMP) color = &colors::yellow; else if (n.flags & NF_DUCK) color = &colors::green; Vector wts; if (not draw::WorldToScreen(n.xyz(), wts)) return; size_t node_size = 2; if (state::state != WB_REPLAYING) { if (node == state::closest_node) node_size = 6; } if (node == state::active_node) color = &colors::red; draw_api::draw_rect(wts.x - node_size, wts.y - node_size, 2 * node_size, 2 * node_size, *color); } if (draw_indices) { const rgba_t *color = &colors::white; if (n.flags & NF_JUMP) color = &colors::yellow; else if (n.flags & NF_DUCK) color = &colors::green; Vector wts; if (not draw::WorldToScreen(n.xyz(), wts)) return; draw_api::draw_string_with_outline( wts.x, wts.y, std::to_string(node).c_str(), fonts::main_font, *color, colors::black, 1.5f); } } void DrawPath() { for (index_t i = 0; i < state::nodes.size(); i++) { DrawNode(i, true); } } void Draw() { if (state::state == WB_DISABLED) return; switch (state::state) { case WB_RECORDING: { AddSideString("Walkbot: Recording"); } break; case WB_EDITING: { AddSideString("Walkbot: Editing"); } break; case WB_REPLAYING: { AddSideString("Walkbot: Replaying"); if (free_move and free_move_used) { AddSideString("Walkbot: FREE MOVEMENT (User override)", colors::green); } if (HasLowAmmo()) { AddSideString("Walkbot: LOW AMMO", colors::yellow); } if (HasLowHealth()) { AddSideString("Walkbot: LOW HEALTH", colors::red); } } break; } if (draw_info) { AddSideString(format("Active node: ", state::active_node)); AddSideString(format("Highlighted node: ", state::closest_node)); AddSideString(format("Last node: ", state::last_node)); AddSideString(format("Node count: ", state::nodes.size())); if (state::recovery) { AddSideString(format("(Recovery mode)")); } } if (draw_path) DrawPath(); } #endif void OnLevelInit() { if (leave_if_empty && state::state == WB_REPLAYING) { nodes.clear(); } } Timer quit_timer{}; Timer map_check{}; int erasedelay = 0; void Move() { if (state::state == WB_DISABLED) return; switch (state::state) { case WB_RECORDING: { UpdateClosestNode(); if (active_recording and ShouldSpawnNode()) { RecordNode(); } } break; case WB_EDITING: { UpdateClosestNode(); } break; case WB_REPLAYING: { if (leave_if_empty) { if (nodes.size() == 0 || g_IEngine->GetLevelName() != prevlvlname) { prevlvlname = g_IEngine->GetLevelName(); if (!boost::contains(prevlvlname, "pl_")) { Load("default"); if (leave_if_empty && nodes.size() == 0 && quit_timer.test_and_set(5000)) { logging::Info("No map file, abandon"); tfmm::abandon(); return; } } } } prevlvlname = g_IEngine->GetLevelName(); std::string prvlvlname = format(prevlvlname); logging::Info("%s %s", prevlvlname, prvlvlname.c_str()); if (boost::contains(prvlvlname, "pl_") || boost::contains(prvlvlname, "cp_")) { logging::Info("1"); bool ret = false; if (lagexploit::pointarr[0] || lagexploit::pointarr[1] || lagexploit::pointarr[2] || lagexploit::pointarr[3] || lagexploit::pointarr[4]) { hacks::shared::followbot::followbot = 1; hacks::shared::followbot::roambot = 1; hacks::shared::followbot::followcart = true; } else { hacks::shared::followbot::followbot = 0; hacks::shared::followbot::roambot = 0; hacks::shared::followbot::followcart = false; } } if (nodes.size() == 0) return; if (force_slot) UpdateSlot(); UpdateWalker(); } break; } } } } }