using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; public struct Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public static implicit operator Point((int x, int y) tuple) => new Point(tuple.x, tuple.y); public bool Equals(Point other) => X == other.X && Y == other.Y; public override bool Equals(object obj) => obj is Point other && Equals(other); public override int GetHashCode() => HashCode.Combine(X, Y); public static bool operator ==(Point left, Point right) => left.Equals(right); public static bool operator !=(Point left, Point right) => !(left == right); public override string ToString() => $"({X},{Y})"; } Log("=== Hope Habubu Avoider + Auto Gate ==="); // ==================== KONFIGURATION ==================== const string DANGER_FURNI_NAME = "Hope Habubu"; const string GATE_FURNI_NAME = "One Way Gate"; const int DANGER_RADIUS = 2; const int REACTION_DISTANCE = 4; const int LOOP_DELAY_MS = 25; // ======================================================= Regex mvRegex = new Regex(@"/mv (\d+),(\d+),([\d\.]+)/"); HashSet walkableTiles = new HashSet(); bool floorPlanReady = false; DateTime lastFloorPlanParse = DateTime.MinValue; Dictionary trackedThreats = new Dictionary(); Point myTargetTile = (-1, -1); DateTime lastMoveTime = DateTime.MinValue; bool IsInvalidPoint(Point p) { return p.X == -1 && p.Y == -1; } Point GetMyPosition() { if (Self == null || Self.Location == null) return (-1, -1); if (!IsInvalidPoint(myTargetTile) && (DateTime.UtcNow - lastMoveTime).TotalMilliseconds < 300) return myTargetTile; return (Self.Location.X, Self.Location.Y); } void DoMove(int x, int y) { Move(x, y); myTargetTile = (x, y); lastMoveTime = DateTime.UtcNow; } double GetDistance(Point a, Point b) { return Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2)); } void ParseFloorPlan() { if ((DateTime.UtcNow - lastFloorPlanParse).TotalSeconds < 5 && floorPlanReady) return; lastFloorPlanParse = DateTime.UtcNow; try { dynamic fp = FloorPlan; if (fp == null) { floorPlanReady = false; return; } int width = fp.Width; int length = fp.Length; if (width <= 0 || length <= 0) { floorPlanReady = false; return; } var tiles = new HashSet(); IReadOnlyList tilesData = null; string heightmap = null; try { tilesData = fp.Tiles; } catch { } try { heightmap = fp.Heightmap?.Replace("\r", "").Replace("\n", ""); } catch { } if (tilesData != null) { for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { int idx = y * width + x; if (idx < tilesData.Count && tilesData[idx] >= 0 && tilesData[idx] < 250) tiles.Add((x, y)); } } } else if (heightmap != null && heightmap.Length == width * length) { for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { if (heightmap[y * width + x] != 'x') tiles.Add((x, y)); } } } if (tiles.Count > 0) { walkableTiles = tiles; floorPlanReady = true; Log($"FloorPlan: {tiles.Count} tiles"); } } catch (Exception ex) { Log($"FloorPlan error: {ex.Message}"); floorPlanReady = false; } } bool IsWalkable(int x, int y) { if (!floorPlanReady || walkableTiles == null) return true; return walkableTiles.Contains((x, y)); } // ==================== GATE LOGIC ==================== void CheckAndEnterGate(int userX, int userY) { if (FloorItems == null) return; foreach (var item in FloorItems) { if (item == null || item.Location == null) continue; string itemName = null; try { itemName = item.GetName(); } catch { continue; } if (itemName != null && itemName.Contains(GATE_FURNI_NAME)) { int gateX = item.Location.X; int gateY = item.Location.Y; int gateDir = item.Direction; long gateId = item.Id; bool shouldEnter = false; if (gateDir == 0) { if (userX == gateX && userY == gateY - 1) shouldEnter = true; } else if (gateDir == 2) { if (userX == gateX + 1 && userY == gateY) shouldEnter = true; } else if (gateDir == 4) { if (userX == gateX && userY == gateY + 1) shouldEnter = true; } else if (gateDir == 6) { if (userX == gateX - 1 && userY == gateY) shouldEnter = true; } if (shouldEnter) { Log($"Gate! ID:{gateId} at ({gateX},{gateY}) Dir:{gateDir}"); Send(Out.EnterOneWayDoor, gateId); return; } } } } void CheckGateOnObjectUpdate(int furniId, int itemX, int itemY, int newDir) { if (Self == null || Self.Location == null) return; var item = FloorItems?.FirstOrDefault(f => f != null && f.Id == furniId); if (item == null) return; string itemName = null; try { itemName = item.GetName(); } catch { return; } if (itemName != null && itemName.Contains(GATE_FURNI_NAME)) { int userX = Self.Location.X; int userY = Self.Location.Y; bool shouldEnter = false; if (newDir == 0) { if (userX == itemX && userY == itemY - 1) shouldEnter = true; } else if (newDir == 2) { if (userX == itemX + 1 && userY == itemY) shouldEnter = true; } else if (newDir == 4) { if (userX == itemX && userY == itemY + 1) shouldEnter = true; } else if (newDir == 6) { if (userX == itemX - 1 && userY == itemY) shouldEnter = true; } if (shouldEnter) { Log($"Gate rotated! ID:{furniId}"); Send(Out.EnterOneWayDoor, furniId); } } } // ==================== THREAT LOGIC ==================== void ScanThreats() { if (FloorItems == null) return; trackedThreats.Clear(); foreach (var item in FloorItems) { if (item == null || item.Location == null) continue; string name = null; try { name = item.GetName(); } catch { continue; } if (name != null && name.Contains(DANGER_FURNI_NAME)) { trackedThreats[item.Id] = (item.Location.X, item.Location.Y); } } } Point FindBestEscapeStep(Point myPos) { Point bestStep = myPos; double bestScore = double.MinValue; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { Point candidate = (myPos.X + dx, myPos.Y + dy); if (!IsWalkable(candidate.X, candidate.Y)) continue; double minDangerDist = double.MaxValue; foreach (var threat in trackedThreats.Values) { double d = GetDistance(candidate, threat); if (d < minDangerDist) minDangerDist = d; } double score = minDangerDist * 10; if (dx != 0 || dy != 0) score += 1; if (score > bestScore) { bestScore = score; bestStep = candidate; } } } return bestStep; } // ==================== EVENT HANDLERS ==================== void HandleUserUpdate(dynamic e) { if (Self == null) return; var packet = e.Packet; int numUpdates = packet.ReadInt(); for (int i = 0; i < numUpdates; i++) { int entityIndex = packet.ReadInt(); int currentX = packet.ReadInt(); int currentY = packet.ReadInt(); packet.ReadString(); packet.ReadInt(); packet.ReadInt(); string action = packet.ReadString(); if (entityIndex == Self.Index) { Point tileForGateCheck; Match match = mvRegex.Match(action); if (match.Success) { try { int targetX = int.Parse(match.Groups[1].Value); int targetY = int.Parse(match.Groups[2].Value); myTargetTile = (targetX, targetY); lastMoveTime = DateTime.UtcNow; tileForGateCheck = myTargetTile; } catch { tileForGateCheck = (currentX, currentY); } } else { if (action.EndsWith("//") && !action.Contains("/mv")) myTargetTile = (-1, -1); tileForGateCheck = (currentX, currentY); } CheckAndEnterGate(tileForGateCheck.X, tileForGateCheck.Y); } } } void HandleObjectUpdate(dynamic e) { if (Self == null || Self.Location == null) return; var packet = e.Packet; int furniId = packet.ReadInt(); packet.ReadInt(); int itemX = packet.ReadInt(); int itemY = packet.ReadInt(); int newDir = packet.ReadInt(); // Update threat position if (trackedThreats.ContainsKey(furniId)) { trackedThreats[furniId] = (itemX, itemY); } // Check gate CheckGateOnObjectUpdate(furniId, itemX, itemY, newDir); } void HandleWiredMovements(dynamic e) { var packet = e.Packet; int count = packet.ReadInt(); for (int i = 0; i < count; i++) { packet.ReadInt(); packet.ReadInt(); packet.ReadInt(); int toX = packet.ReadInt(); int toY = packet.ReadInt(); packet.ReadString(); packet.ReadString(); int id = packet.ReadInt(); packet.ReadInt(); packet.ReadInt(); if (trackedThreats.ContainsKey(id)) { trackedThreats[id] = (toX, toY); } } } void HandleRoomEnter(dynamic e) { Log("Room entered - scanning..."); trackedThreats.Clear(); myTargetTile = (-1, -1); ParseFloorPlan(); ScanThreats(); Log($"Found: {trackedThreats.Count} '{DANGER_FURNI_NAME}' objects"); if (Self != null && Self.Location != null) CheckAndEnterGate(Self.Location.X, Self.Location.Y); } // ==================== REGISTER EVENTS ==================== OnIntercept(In["UserUpdate"], e => HandleUserUpdate(e)); OnIntercept(In["ObjectUpdate"], e => HandleObjectUpdate(e)); OnIntercept(In["WiredMovements"], e => HandleWiredMovements(e)); OnEnteredRoom(e => HandleRoomEnter(e)); // Initial setup if (Self != null && Self.Location != null) { ParseFloorPlan(); ScanThreats(); Log($"Initial: {trackedThreats.Count} threats found"); CheckAndEnterGate(Self.Location.X, Self.Location.Y); } // ==================== MAIN LOOP ==================== int tick = 0; while (Run) { try { tick++; if (!floorPlanReady) ParseFloorPlan(); if (tick % 50 == 0) ScanThreats(); Point myPos = GetMyPosition(); if (IsInvalidPoint(myPos)) { Delay(LOOP_DELAY_MS); continue; } if (trackedThreats.Count == 0) { Delay(LOOP_DELAY_MS); continue; } double closestDanger = double.MaxValue; Point closestThreat = (-1, -1); foreach (var threat in trackedThreats.Values) { double d = GetDistance(myPos, threat); if (d < closestDanger) { closestDanger = d; closestThreat = threat; } } if (closestDanger <= REACTION_DISTANCE) { Point nextStep = FindBestEscapeStep(myPos); if (!nextStep.Equals(myPos)) { if (tick % 10 == 0) Log($"DODGE! Threat at {closestThreat} dist:{closestDanger:F1} -> {nextStep}"); DoMove(nextStep.X, nextStep.Y); } } else if (tick % 100 == 0 && trackedThreats.Count > 0) { Log($"Monitoring {trackedThreats.Count} threats... nearest: {closestDanger:F1}"); } } catch (Exception ex) { Log($"Error: {ex.Message}"); } if (!Run) break; Delay(LOOP_DELAY_MS); } Log("=== Stopped ===");