diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01f9cb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.vscode/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2c2ae47 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.26) + +project(collision_cacher) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_SHARED_LINKER_FLAGS "-Os -s -m32 -static-libgcc -static-libstdc++ -static") +set(CMAKE_SHARED_LIBRARY_PREFIX "") + +if (NOT DEFINED ALPACABOT_DIR) + set(ALPACABOT_DIR $ENV{USERPROFILE}\\AlpacaBot) +endif() + +if (NOT DEFINED ALPACABOT_INCLUDE_DIR) + set(ALPACABOT_INCLUDE_DIR ${ALPACABOT_DIR}\\Include) +endif() + +if (NOT DEFINED ALPACABOT_LIB_DIR) + set(ALPACABOT_LIB_DIR ${ALPACABOT_DIR}\\Library) +endif() + +if (NOT DEFINED ALPACABOT_SCRIPT_DIR) + set(ALPACABOT_SCRIPT_DIR ${ALPACABOT_DIR}\\Script\\Local) +endif() + + +set(SOURCE_FILES + src/main.cpp +) + +add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) + +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${ALPACABOT_SCRIPT_DIR}/${PROJECT_NAME} +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${ALPACABOT_INCLUDE_DIR} +) + +target_link_directories(${PROJECT_NAME} PRIVATE + ${ALPACABOT_LIB_DIR} +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + AlpacaLibrary +) diff --git a/collision_map.png b/collision_map.png new file mode 100644 index 0000000..4f45ad9 Binary files /dev/null and b/collision_map.png differ diff --git a/collision_map_plane_0.png b/collision_map_plane_0.png new file mode 100644 index 0000000..a5de09e Binary files /dev/null and b/collision_map_plane_0.png differ diff --git a/collision_map_plane_1.png b/collision_map_plane_1.png new file mode 100644 index 0000000..d538e4b Binary files /dev/null and b/collision_map_plane_1.png differ diff --git a/collision_map_plane_2.png b/collision_map_plane_2.png new file mode 100644 index 0000000..ac2f7aa Binary files /dev/null and b/collision_map_plane_2.png differ diff --git a/image_gen.go b/image_gen.go new file mode 100644 index 0000000..35080a3 --- /dev/null +++ b/image_gen.go @@ -0,0 +1,134 @@ +package main + +import ( + "encoding/csv" + "image" + "image/color" + "image/png" + "log" + "os" + "path/filepath" + "strconv" +) + +func main() { + // Path to the CSV file + userProfile := os.Getenv("USERPROFILE") + filePath := filepath.Join(userProfile, "AlpacaBot", "Collision Data", "collision_data.csv") + + // Open the CSV file + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("Failed to open file: %v", err) + } + defer file.Close() + + // Read the CSV file + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + + // Group records by plane + dataByPlane := make(map[string][][]string) + for _, record := range records[1:] { // Skip the header row + plane := record[1] + dataByPlane[plane] = append(dataByPlane[plane], record) + } + + for plane, records := range dataByPlane { + // Find the min and max x, y coordinates + minX, minY, maxX, maxY := 1<<31-1, 1<<31-1, 0, 0 + for _, record := range records { + x, err := strconv.Atoi(record[2]) + if err != nil { + continue + } + y, err := strconv.Atoi(record[3]) + if err != nil { + continue + } + if x < minX { + minX = x + } + if y < minY { + minY = y + } + if x > maxX { + maxX = x + } + if y > maxY { + maxY = y + } + } + + // Create an empty image + width, height := maxX-minX+1, maxY-minY+1 + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // Fill in the image with data + for _, record := range records { + x, err := strconv.Atoi(record[2]) + if err != nil { + continue + } + y, err := strconv.Atoi(record[3]) + if err != nil { + continue + } + flag, err := strconv.Atoi(record[4]) + if err != nil { + continue + } + + // Determine the color based on the flag value + var col color.Color + switch flag { + case 0: + col = color.RGBA{0, 255, 0, 255} + case 0xFFFFFF: + col = color.RGBA{255, 0, 0, 255} + case 0x1000000: + col = color.RGBA{0, 0, 255, 255} + case 0x100: + col = color.RGBA{255, 255, 0, 255} + case 0x20000: + col = color.RGBA{128, 0, 128, 255} + case 0x200000: + col = color.RGBA{255, 128, 0, 255} + case 0x2: + col = color.RGBA{0, 255, 255, 255} + case 0x8: + col = color.RGBA{255, 0, 255, 255} + case 0x20: + col = color.RGBA{128, 128, 0, 255} + case 0x80: + col = color.RGBA{128, 128, 128, 255} + case 0x4: + col = color.RGBA{0, 128, 0, 255} + case 0x10: + col = color.RGBA{128, 0, 0, 255} + case 0x40: + col = color.RGBA{0, 0, 128, 255} + case 0x1: + col = color.RGBA{0, 128, 128, 255} + default: + col = color.RGBA{0, 0, 0, 255} + } + + // Set the pixel in the image + img.Set(x-minX, height-(y-minY)-1, col) + } + + // Save the image to a file + outputFileName := "collision_map_plane_" + plane + ".png" + outputFile, err := os.Create(outputFileName) + if err != nil { + log.Fatalf("Failed to create output file: %v", err) + } + defer outputFile.Close() + + png.Encode(outputFile, img) + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9d4309e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,247 @@ +#include +#include +#include +#include + +#include + +void Setup() { + ScriptInfo Info; + Info.Name = "Collision Cacher"; + Info.Description = "Caches collision data around the player."; + Info.Version = "1.00"; + Info.Category = "Magic"; + Info.Author = "Warren"; + Info.UID = "UID"; + Info.ForumPage = "forum.alpacabot.org"; + SetScriptInfo(Info); +} + +class region_key { +public: + std::int32_t region; + std::int32_t plane; + + region_key() = default; + + region_key(std::int32_t region, std::int32_t plane) + : region(region), plane(plane) {} + + bool operator==(const region_key& other) const { + return region == other.region && plane == other.plane; + } +}; + +class collision { +public: + Tile tile; + Pathfinding::COLLISION_FLAG flag; + + collision(const Tile& tile, Pathfinding::COLLISION_FLAG flag) : tile(tile), flag(flag) {} + + bool operator==(const collision& other) const { + return tile == other.tile && flag == other.flag; + } +}; + +namespace std { + template<> + struct hash { + std::size_t operator()(const region_key& key) const { + std::size_t h1 = std::hash()(key.region); + std::size_t h2 = std::hash()(key.plane); + return h1 ^ (h2 << 1); + } + }; + + template<> + struct hash { + std::size_t operator()(const Tile& tile) const { + std::size_t h1 = std::hash()(tile.X); + std::size_t h2 = std::hash()(tile.Y); + std::size_t h3 = std::hash()(tile.Plane); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + + template<> + struct hash { + std::size_t operator()(const collision& collision) const { + std::size_t h1 = std::hash()(collision.tile); + std::size_t h2 = std::hash()(static_cast(collision.flag)); + return h1 ^ (h2 << 1); + } + }; +} + +std::unordered_map> cached_regions; + +void cache_current_region() { + const auto get_tile_region = [](const Tile& tile) { + return ((std::int32_t)(((tile.X >> 6) << 8) | (tile.Y >> 6))); + }; + + const auto to_world_tile = [](const Pathfinding::TileNode& node, std::int32_t client_x, std::int32_t client_y, std::int32_t client_plane) { + return Tile( + node.X + client_x, + node.Y + client_y, + client_plane + ); + }; + + Pathfinding::GenerateNodes(); + const std::int32_t client_x = Internal::GetClientX(); + const std::int32_t client_y = Internal::GetClientY(); + const std::int32_t client_plane = Internal::GetClientPlane(); + + for (const auto& nodes : Pathfinding::GetNodes()) { + for (const auto& node : nodes) { + const auto flag = node.Flag; + const auto world_tile = to_world_tile(node, client_x, client_y, client_plane); + + if ((flag & Pathfinding::UNINITIALIZED) || flag == Pathfinding::UNINITIALIZED) { + continue; + } + + if ((flag == Pathfinding::CLOSED)) { + continue; + } + + std::int32_t current_region = get_tile_region(world_tile); + + cached_regions[region_key(current_region, client_plane)].emplace(world_tile, static_cast(flag)); + } + } +} + +void paint() { + Paint::Clear(); + std::vector texts; + auto local_tile = Players::GetLocal().GetTile(); + for (const auto& [key, collision_data] : cached_regions) { + texts.emplace_back( + "(" + std::to_string(key.region) + ", " + std::to_string(key.plane) + "): " + std::to_string(collision_data.size()) + ); + + for (const auto& collision : collision_data) { + if (collision.tile.DistanceFrom(local_tile) > 12) + continue; + + if ((collision.flag & Pathfinding::BLOCKED) || (collision.flag & Pathfinding::OCCUPIED)) { + Paint::DrawTile(collision.tile, 255, 0, 0, 255); + } else if (collision.flag & Pathfinding::NORTH) { + Paint::DrawTile(collision.tile, 255, 0, 255, 255); + } else if (collision.flag & Pathfinding::EAST) { + Paint::DrawTile(collision.tile, 0, 255, 0, 255); + } else if (collision.flag & Pathfinding::SOUTH) { + Paint::DrawTile(collision.tile, 0, 0, 255, 255); + } else if (collision.flag & Pathfinding::WEST) { + Paint::DrawTile(collision.tile, 255, 165, 0, 255); + } + } + } + + Point p = Point(0, 5); + for (const auto& text : texts) { + p += Point(0, 20); + Paint::DrawString(text, p, 0, 255, 0, 255); + } + Paint::SwapBuffer(); +} + +void verify_and_clean() { + // verify each tile in a region actually belongs to it, if not, remove it + for (auto it = cached_regions.begin(); it != cached_regions.end();) { + const auto& [key, collision_data] = *it; + const auto get_tile_region = [](const Tile& tile) { + return ((std::int32_t)(((tile.X >> 6) << 8) | (tile.Y >> 6))); + }; + + for (const auto& collision : collision_data) { + if (get_tile_region(collision.tile) != key.region) { + it->second.erase(collision); + } + } + + if (it->second.empty()) { + it = cached_regions.erase(it); + } else { + ++it; + } + } +} + +void write_collision_data(std::ofstream& file) { + file << "region,plane,x,y,flag\n"; + verify_and_clean(); + for (const auto& [key, collision_data] : cached_regions) { + for (const auto& collision : collision_data) { + if (collision.flag == Pathfinding::OPEN) + continue; + + file << key.region << "," << key.plane << "," << collision.tile.X << "," << collision.tile.Y << "," << static_cast(collision.flag) << "\n"; + } + } +} + +void read_collision_data(std::ifstream& file) { + std::string line; + std::getline(file, line); // skip csv header + while (std::getline(file, line)) { + std::stringstream ss(line); + std::string token; + std::vector tokens; + while (std::getline(ss, token, ',')) { + tokens.emplace_back(token); + } + + const auto region = std::stoi(tokens[0]); + const auto plane = std::stoi(tokens[1]); + const auto x = std::stoi(tokens[2]); + const auto y = std::stoi(tokens[3]); + const auto flag = static_cast(std::stoi(tokens[4])); + + cached_regions[region_key(region, plane)].emplace(Tile(x, y, plane), flag); + } +} + +std::string data_url() { + char *userprofile = std::getenv("USERPROFILE"); + if (userprofile == nullptr) + return ""; + + return std::string(userprofile) + "\\AlpacaBot\\Collision Data\\collision_data.csv"; +} + +bool OnStart() { + SetLoopDelay(5); + + std::string path = data_url(); + std::ifstream file(path); + read_collision_data(file); + return true; +} + +bool Loop() { + cache_current_region(); + paint(); + return true; +} + +bool OnBreak() { + return true; +} + +void OnEnd() { + char *userprofile = std::getenv("USERPROFILE"); + if (userprofile == nullptr) + return; + + std::string path = data_url(); + if (!std::filesystem::exists(path)) { + std::filesystem::create_directories(std::string(userprofile) + "\\AlpacaBot\\Collision Data\\"); + } + + std::ofstream file(path); + write_collision_data(file); +} \ No newline at end of file