using System; using System.Collections.Generic; using System.Linq; public struct Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = 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})"; } int ballHandItemId = 2147418815; string[] targetTileNameContains = { "ice skating patch", "snow-covered rocks" }; string passMessage = ":pass"; int passIntervalMs = 300; int loopDelayMs = 80; int moveIntervalMs = 200; int rescanTilesMs = 3000; int approachDistance = 1; int escapeDurationMs = 15000; Point escapeTileOverride = new Point(-1, -1); bool enabled = true; bool hasBall = false; bool escapeMode = false; DateTime escapeStart = DateTime.MinValue; Point escapeTarget = new Point(-1, -1); DateTime lastPass = DateTime.MinValue; DateTime lastMove = DateTime.MinValue; DateTime lastScan = DateTime.MinValue; HashSet targetTiles = new HashSet(); HashSet walkableTiles = new HashSet(); Dictionary> adj = new Dictionary>(); Point[] dirs = { new Point(0, 1), new Point(0, -1), new Point(1, 0), new Point(-1, 0), new Point(1, 1), new Point(1, -1), new Point(-1, 1), new Point(-1, -1) }; bool floorPlanParsed = false; void ParseFloorPlan() { if (floorPlanParsed) return; try { dynamic floorPlan = FloorPlan; if (floorPlan == null) return; int width = floorPlan.Width; int length = floorPlan.Length; walkableTiles.Clear(); IReadOnlyList tilesData = null; string heightmapString = null; try { tilesData = floorPlan.Tiles; } catch { } try { heightmapString = floorPlan.Heightmap; } catch { } if (heightmapString != null) { heightmapString = heightmapString.Replace("\r", "").Replace("\n", ""); for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { if (heightmapString[y * width + x] != 'x') walkableTiles.Add(new Point(x, y)); } } } else if (tilesData != null) { for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { int tileIndex = y * width + x; if (tileIndex < tilesData.Count && tilesData[tileIndex] >= 0 && tilesData[tileIndex] < 250) walkableTiles.Add(new Point(x, y)); } } } BuildAdjacencyMap(); floorPlanParsed = true; } catch { } } void BuildAdjacencyMap() { adj.Clear(); foreach (var t in walkableTiles) { var n = new List(); foreach (var d in dirs) { var p = new Point(t.X + d.X, t.Y + d.Y); if (walkableTiles.Contains(p)) n.Add(p); } adj[t] = n; } } void ScanTargetTiles() { targetTiles.Clear(); if (FloorItems == null) return; foreach (var item in FloorItems) { if (item == null || item.Location == null) continue; string name = null; try { name = item.GetName(); } catch { continue; } if (string.IsNullOrEmpty(name)) continue; string lower = name.ToLowerInvariant(); if (targetTileNameContains.Any(n => lower.Contains(n))) targetTiles.Add(new Point(item.Location.X, item.Location.Y)); } lastScan = DateTime.UtcNow; } Point GetMyPosition() { if (Self == null || Self.Location == null) return new Point(-1, -1); return new Point(Self.Location.X, Self.Location.Y); } int Dist(Point a, Point b) => Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y); IEntity FindNearestTargetUser(Point me) { IEntity best = null; int bestDist = int.MaxValue; foreach (var user in Users) { if (user == null || user.Id == Self.Id || user.Location == null) continue; var p = new Point(user.Location.X, user.Location.Y); if (!targetTiles.Contains(p)) continue; int d = Dist(me, p); if (d < bestDist) { bestDist = d; best = user; } } return best; } Point FindClosestWalkable(Point target) { if (walkableTiles.Contains(target)) return target; Point best = new Point(-1, -1); int bestDist = int.MaxValue; foreach (var t in walkableTiles) { int d = Dist(t, target); if (d < bestDist) { bestDist = d; best = t; } } return bestDist == int.MaxValue ? target : best; } List FindPath(Point start, Point goal) { if (!walkableTiles.Contains(start) || !walkableTiles.Contains(goal)) return new List(); var queue = new Queue(); var visited = new HashSet(); var cameFrom = new Dictionary(); queue.Enqueue(start); visited.Add(start); while (queue.Count > 0) { var cur = queue.Dequeue(); if (cur == goal) break; if (!adj.TryGetValue(cur, out var neighbors)) continue; foreach (var n in neighbors) { if (visited.Contains(n)) continue; visited.Add(n); cameFrom[n] = cur; queue.Enqueue(n); } } if (!visited.Contains(goal)) return new List(); var path = new List(); var node = goal; while (node != start) { path.Add(node); if (!cameFrom.TryGetValue(node, out var prev)) break; node = prev; } path.Add(start); path.Reverse(); return path; } void StepToward(Point goal) { if ((DateTime.UtcNow - lastMove).TotalMilliseconds < moveIntervalMs) return; var me = GetMyPosition(); if (me.X < 0) return; var realGoal = FindClosestWalkable(goal); var path = FindPath(me, realGoal); if (path.Count >= 2) { var next = path[1]; Move(next.X, next.Y); lastMove = DateTime.UtcNow; } else if (walkableTiles.Contains(realGoal)) { Move(realGoal.X, realGoal.Y); lastMove = DateTime.UtcNow; } } Point FindEscapeTile(Point me) { if (escapeTileOverride.X >= 0 && escapeTileOverride.Y >= 0 && walkableTiles.Contains(escapeTileOverride)) return escapeTileOverride; var others = Users.Where(u => u != null && u.Id != Self.Id && u.Location != null) .Select(u => new Point(u.Location.X, u.Location.Y)) .ToList(); if (others.Count == 0) return me; Point best = me; int bestScore = int.MinValue; foreach (var t in walkableTiles) { int minDist = others.Min(o => Dist(t, o)); if (minDist > bestScore) { bestScore = minDist; best = t; } } return best; } void TryPass(IEntity target) { if (target == null) return; if ((DateTime.UtcNow - lastPass).TotalMilliseconds < passIntervalMs) return; var msg = passMessage.Replace("{name}", target.Name); Talk(msg); lastPass = DateTime.UtcNow; } void SetBallState(bool active) { if (active == hasBall) return; hasBall = active; if (hasBall) { escapeMode = false; escapeTarget = new Point(-1, -1); } else { escapeMode = true; escapeStart = DateTime.UtcNow; escapeTarget = FindEscapeTile(GetMyPosition()); } } OnIntercept(In["CarryObject"], e => { int userIndex = e.Packet.ReadInt(); int carrying = e.Packet.ReadInt(); if (Self != null && userIndex == Self.Index) { SetBallState(carrying == ballHandItemId); } }); OnEnteredRoom(e => { floorPlanParsed = false; targetTiles.Clear(); ScanTargetTiles(); }); OnIntercept(Out.Chat, e => { string message = e.Packet.ReadString(); if (message.Equals(".icebot on", StringComparison.OrdinalIgnoreCase)) { enabled = true; e.Block(); Log("Ice bot enabled"); } else if (message.Equals(".icebot off", StringComparison.OrdinalIgnoreCase)) { enabled = false; e.Block(); Log("Ice bot disabled"); } else if (message.Equals(".icebot scan", StringComparison.OrdinalIgnoreCase)) { e.Block(); ScanTargetTiles(); Log($"Target tiles: {targetTiles.Count}"); } }); while (Run) { try { if (!enabled) { Delay(200); continue; } if (Self == null || Self.Location == null) { Delay(200); continue; } if (!floorPlanParsed) ParseFloorPlan(); if (!floorPlanParsed) { Delay(100); continue; } if ((DateTime.UtcNow - lastScan).TotalMilliseconds > rescanTilesMs) ScanTargetTiles(); var me = GetMyPosition(); if (me.X < 0) { Delay(loopDelayMs); continue; } if (hasBall) { var target = FindNearestTargetUser(me); if (target != null && target.Location != null) { var tp = new Point(target.Location.X, target.Location.Y); if (Dist(me, tp) <= approachDistance) TryPass(target); else StepToward(tp); } } else if (escapeMode) { if ((DateTime.UtcNow - escapeStart).TotalMilliseconds > escapeDurationMs) { escapeMode = false; } else if (escapeTarget.X >= 0) { if (me != escapeTarget) StepToward(escapeTarget); } } } catch { } Delay(loopDelayMs); } Wait();