stuff
This commit is contained in:
parent
a8341e8232
commit
a8c43ba7e2
43 changed files with 2618 additions and 48 deletions
|
|
@ -1,8 +1,12 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Roboto.Memory;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Automata.GameLog;
|
||||
using Roboto.Core;
|
||||
using Roboto.Data;
|
||||
using Roboto.Engine;
|
||||
|
|
@ -62,6 +66,11 @@ public partial class EntityListItem : ObservableObject
|
|||
|
||||
public partial class RobotoViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial short GetAsyncKeyState(int vKey);
|
||||
private const int VK_END = 0x23;
|
||||
private bool _endWasDown;
|
||||
|
||||
private readonly BotEngine _engine;
|
||||
private long _lastUiUpdate;
|
||||
private bool _disposed;
|
||||
|
|
@ -84,15 +93,48 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
|
||||
// Systems
|
||||
[ObservableProperty] private string _systemsInfo = "—";
|
||||
[ObservableProperty] private string _apmInfo = "0";
|
||||
|
||||
// Navigation
|
||||
[ObservableProperty] private string _navMode = "Idle";
|
||||
[ObservableProperty] private string _navStatus = "—";
|
||||
|
||||
// Terrain minimap
|
||||
[ObservableProperty] private Bitmap? _terrainImage;
|
||||
private byte[]? _terrainBasePixels;
|
||||
private byte[]? _minimapBuffer;
|
||||
private int _terrainWidth, _terrainHeight;
|
||||
private uint _terrainAreaHash;
|
||||
private const int MinimapViewSize = 400;
|
||||
private const float WorldToGrid = 23.0f / 250.0f;
|
||||
private const float Cos45 = 0.70710678f;
|
||||
private const float Sin45 = 0.70710678f;
|
||||
|
||||
// Entity list for checkbox UI
|
||||
[ObservableProperty] private bool _showAllEntities;
|
||||
public ObservableCollection<EntityListItem> Entities { get; } = [];
|
||||
|
||||
// ── Profile Editor ──
|
||||
[ObservableProperty] private string _characterName = "—";
|
||||
[ObservableProperty] private bool _hasProfile;
|
||||
[ObservableProperty] private string _profileName = "—";
|
||||
[ObservableProperty] private string? _selectedProfile;
|
||||
public ObservableCollection<string> AvailableProfiles { get; } = [];
|
||||
public ObservableCollection<SkillProfileViewModel> SkillProfiles { get; } = [];
|
||||
|
||||
// Flask settings
|
||||
[ObservableProperty] private float _lifeFlaskThreshold = 50f;
|
||||
[ObservableProperty] private float _manaFlaskThreshold = 50f;
|
||||
[ObservableProperty] private int _flaskCooldownMs = 4000;
|
||||
|
||||
// Combat settings
|
||||
[ObservableProperty] private int _globalCooldownMs = 500;
|
||||
[ObservableProperty] private float _attackRange = 600f;
|
||||
[ObservableProperty] private float _safeRange = 400f;
|
||||
[ObservableProperty] private bool _kiteEnabled;
|
||||
[ObservableProperty] private float _kiteRange = 300f;
|
||||
[ObservableProperty] private int _kiteDelayMs = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe snapshot for the overlay layer (written on UI thread, read on overlay thread).
|
||||
/// </summary>
|
||||
|
|
@ -103,14 +145,14 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
/// </summary>
|
||||
public static volatile GameDataCache? SharedCache;
|
||||
|
||||
public RobotoViewModel()
|
||||
public RobotoViewModel(IClientLogWatcher logWatcher)
|
||||
{
|
||||
var config = new BotConfig();
|
||||
var reader = new GameMemoryReader();
|
||||
var humanizer = new Humanizer(config);
|
||||
var input = new InterceptionInputController(humanizer);
|
||||
|
||||
_engine = new BotEngine(config, reader, input);
|
||||
_engine = new BotEngine(config, reader, input, humanizer);
|
||||
|
||||
_engine.StatusChanged += status =>
|
||||
{
|
||||
|
|
@ -118,6 +160,17 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
};
|
||||
|
||||
_engine.StateUpdated += OnStateUpdated;
|
||||
|
||||
_engine.ProfileChanged += profile =>
|
||||
{
|
||||
if (!_suppressProfileChanged)
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() => PopulateFromProfile(profile));
|
||||
};
|
||||
|
||||
// Bridge area name from log watcher to engine
|
||||
logWatcher.AreaEntered += area => _engine.SetCurrentAreaName(area);
|
||||
if (logWatcher.CurrentArea is { Length: > 0 } current)
|
||||
_engine.SetCurrentAreaName(current);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -147,6 +200,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
EntityCount = "—";
|
||||
HostileCount = "—";
|
||||
TickInfo = "—";
|
||||
ApmInfo = "0";
|
||||
NavMode = "Idle";
|
||||
NavStatus = "—";
|
||||
Entities.Clear();
|
||||
|
|
@ -167,8 +221,147 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
_engine.Nav.Stop();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveProfile()
|
||||
{
|
||||
var profile = _engine.ActiveProfile;
|
||||
if (profile is null) return;
|
||||
|
||||
// Write VM fields back to profile model
|
||||
profile.Flasks.LifeFlaskThreshold = LifeFlaskThreshold;
|
||||
profile.Flasks.ManaFlaskThreshold = ManaFlaskThreshold;
|
||||
profile.Flasks.FlaskCooldownMs = FlaskCooldownMs;
|
||||
profile.Combat.GlobalCooldownMs = GlobalCooldownMs;
|
||||
profile.Combat.AttackRange = AttackRange;
|
||||
profile.Combat.SafeRange = SafeRange;
|
||||
profile.Combat.KiteEnabled = KiteEnabled;
|
||||
profile.Combat.KiteRange = KiteRange;
|
||||
profile.Combat.KiteDelayMs = KiteDelayMs;
|
||||
|
||||
// Skill write-through is handled by SkillProfileViewModel partial methods
|
||||
|
||||
_engine.Profiles.Save(profile);
|
||||
|
||||
// Apply to engine without rebuilding UI (same profile object, just re-apply to systems)
|
||||
_suppressProfileChanged = true;
|
||||
_engine.ApplyProfile(profile);
|
||||
_suppressProfileChanged = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DuplicateProfile()
|
||||
{
|
||||
var profile = _engine.ActiveProfile;
|
||||
if (profile is null) return;
|
||||
|
||||
var newName = $"{profile.Name}_Copy";
|
||||
var dupe = _engine.Profiles.Duplicate(profile.Name, newName);
|
||||
if (dupe is null) return;
|
||||
|
||||
RefreshAvailableProfiles();
|
||||
_suppressProfileSwitch = true;
|
||||
SelectedProfile = newName;
|
||||
_suppressProfileSwitch = false;
|
||||
SwitchToProfile(newName);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteProfile()
|
||||
{
|
||||
var charName = _engine.Cache.CharacterName;
|
||||
if (charName is null || SelectedProfile is null) return;
|
||||
|
||||
// Don't allow deleting the last profile
|
||||
if (AvailableProfiles.Count <= 1) return;
|
||||
|
||||
var toDelete = SelectedProfile;
|
||||
_engine.Profiles.Delete(toDelete);
|
||||
RefreshAvailableProfiles();
|
||||
|
||||
// If we deleted the active profile, switch to the first available
|
||||
if (_engine.ActiveProfile?.Name == toDelete && AvailableProfiles.Count > 0)
|
||||
{
|
||||
var fallback = AvailableProfiles[0];
|
||||
_suppressProfileSwitch = true;
|
||||
SelectedProfile = fallback;
|
||||
_suppressProfileSwitch = false;
|
||||
SwitchToProfile(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
// When the user picks a different profile from the ComboBox, switch immediately
|
||||
private bool _suppressProfileSwitch;
|
||||
private bool _suppressProfileChanged;
|
||||
partial void OnSelectedProfileChanged(string? value)
|
||||
{
|
||||
if (_suppressProfileSwitch || value is null) return;
|
||||
|
||||
var charName = _engine.Cache.CharacterName;
|
||||
if (charName is null) return;
|
||||
|
||||
// Don't re-switch if it's already the active profile
|
||||
if (_engine.ActiveProfile?.Name == value) return;
|
||||
|
||||
SwitchToProfile(value);
|
||||
}
|
||||
|
||||
private void SwitchToProfile(string profileName)
|
||||
{
|
||||
var charName = _engine.Cache.CharacterName;
|
||||
if (charName is null) return;
|
||||
|
||||
_engine.Profiles.AssignToCharacter(charName, profileName);
|
||||
var profile = _engine.Profiles.LoadForCharacter(charName);
|
||||
_engine.ApplyProfile(profile);
|
||||
}
|
||||
|
||||
private void PopulateFromProfile(CharacterProfile profile)
|
||||
{
|
||||
HasProfile = true;
|
||||
ProfileName = profile.Name;
|
||||
|
||||
// Flask settings
|
||||
LifeFlaskThreshold = profile.Flasks.LifeFlaskThreshold;
|
||||
ManaFlaskThreshold = profile.Flasks.ManaFlaskThreshold;
|
||||
FlaskCooldownMs = profile.Flasks.FlaskCooldownMs;
|
||||
|
||||
// Combat settings
|
||||
GlobalCooldownMs = profile.Combat.GlobalCooldownMs;
|
||||
AttackRange = profile.Combat.AttackRange;
|
||||
SafeRange = profile.Combat.SafeRange;
|
||||
KiteEnabled = profile.Combat.KiteEnabled;
|
||||
KiteRange = profile.Combat.KiteRange;
|
||||
KiteDelayMs = profile.Combat.KiteDelayMs;
|
||||
|
||||
// Skills
|
||||
SkillProfiles.Clear();
|
||||
foreach (var skill in profile.Skills)
|
||||
SkillProfiles.Add(new SkillProfileViewModel(skill));
|
||||
|
||||
RefreshAvailableProfiles();
|
||||
_suppressProfileSwitch = true;
|
||||
SelectedProfile = profile.Name;
|
||||
_suppressProfileSwitch = false;
|
||||
}
|
||||
|
||||
private void RefreshAvailableProfiles()
|
||||
{
|
||||
AvailableProfiles.Clear();
|
||||
foreach (var name in _engine.Profiles.ListProfiles())
|
||||
AvailableProfiles.Add(name);
|
||||
}
|
||||
|
||||
private void OnStateUpdated()
|
||||
{
|
||||
// Emergency stop: END key
|
||||
var endDown = (GetAsyncKeyState(VK_END) & 0x8000) != 0;
|
||||
if (endDown && !_endWasDown)
|
||||
{
|
||||
Serilog.Log.Warning("END pressed — emergency stop");
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(Stop);
|
||||
}
|
||||
_endWasDown = endDown;
|
||||
|
||||
// Throttle UI updates to ~10Hz
|
||||
var now = Environment.TickCount64;
|
||||
if (now - _lastUiUpdate < 100) return;
|
||||
|
|
@ -197,6 +390,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
EntityCount = $"{state.Entities.Count} total";
|
||||
HostileCount = $"{state.HostileMonsters.Count} hostile";
|
||||
TickInfo = $"Tick {state.TickNumber}, dt={state.DeltaTime * 1000:F0}ms";
|
||||
ApmInfo = $"{_engine.CurrentApm} / {250}";
|
||||
|
||||
// Systems status
|
||||
var systems = _engine.Systems;
|
||||
|
|
@ -207,6 +401,35 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
NavMode = _engine.Nav.Mode.ToString();
|
||||
NavStatus = _engine.Nav.Status;
|
||||
|
||||
// Character name
|
||||
if (p.CharacterName is { Length: > 0 })
|
||||
CharacterName = p.CharacterName;
|
||||
|
||||
// Populate available skill names from memory (stripped of "Player" suffix)
|
||||
if (p.Skills.Count > 0)
|
||||
{
|
||||
var names = p.Skills
|
||||
.Where(s => s.Name is { Length: > 0 })
|
||||
.Select(s => SkillProfileViewModel.CleanSkillName(s.Name))
|
||||
.Distinct()
|
||||
.OrderBy(n => n)
|
||||
.ToList();
|
||||
|
||||
foreach (var vm in SkillProfiles)
|
||||
{
|
||||
var current = vm.AvailableSkillNames;
|
||||
if (current.Count != names.Count || !current.SequenceEqual(names))
|
||||
{
|
||||
current.Clear();
|
||||
foreach (var n in names)
|
||||
current.Add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Terrain minimap
|
||||
UpdateTerrainMinimap(state);
|
||||
|
||||
// Entity list
|
||||
UpdateEntityList(state);
|
||||
}
|
||||
|
|
@ -255,6 +478,220 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateTerrainMinimap(GameState state)
|
||||
{
|
||||
if (state.IsLoading || state.Terrain is null)
|
||||
{
|
||||
_terrainBasePixels = null;
|
||||
_terrainAreaHash = 0;
|
||||
var old = TerrainImage;
|
||||
TerrainImage = null;
|
||||
old?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
var terrain = state.Terrain;
|
||||
var w = terrain.Width;
|
||||
var h = terrain.Height;
|
||||
|
||||
// Invalidate cache on area change or terrain resize
|
||||
if ((state.AreaHash != 0 && state.AreaHash != _terrainAreaHash)
|
||||
|| w != _terrainWidth || h != _terrainHeight)
|
||||
{
|
||||
_terrainBasePixels = null;
|
||||
_terrainAreaHash = 0;
|
||||
var old = TerrainImage;
|
||||
TerrainImage = null;
|
||||
old?.Dispose();
|
||||
}
|
||||
|
||||
// Build base pixels from terrain (walkable/blocked only — explored overlay applied per frame)
|
||||
if (_terrainBasePixels is null)
|
||||
{
|
||||
_terrainWidth = w;
|
||||
_terrainHeight = h;
|
||||
_terrainAreaHash = state.AreaHash;
|
||||
_terrainBasePixels = new byte[w * h * 4];
|
||||
}
|
||||
|
||||
// Rebuild base pixels each frame to include explored overlay
|
||||
var basePixels = _terrainBasePixels;
|
||||
var exploredGrid = _engine.Nav.ExploredGrid;
|
||||
var ew = _engine.Nav.ExploredWidth;
|
||||
var eh = _engine.Nav.ExploredHeight;
|
||||
|
||||
for (var y = 0; y < h; y++)
|
||||
{
|
||||
var srcY = h - 1 - y; // flip Y: game Y-up → bitmap Y-down
|
||||
for (var x = 0; x < w; x++)
|
||||
{
|
||||
var i = (y * w + x) * 4;
|
||||
if (terrain.IsWalkable(x, srcY))
|
||||
{
|
||||
// Check explored status
|
||||
var isExplored = exploredGrid is not null
|
||||
&& x < ew && srcY < eh
|
||||
&& exploredGrid[srcY * ew + x];
|
||||
|
||||
var gray = isExplored ? (byte)0x80 : (byte)0x30;
|
||||
basePixels[i] = gray; // B
|
||||
basePixels[i + 1] = gray; // G
|
||||
basePixels[i + 2] = gray; // R
|
||||
basePixels[i + 3] = 0xFF; // A
|
||||
}
|
||||
else
|
||||
{
|
||||
basePixels[i] = 0x00;
|
||||
basePixels[i + 1] = 0x00;
|
||||
basePixels[i + 2] = 0x00;
|
||||
basePixels[i + 3] = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (w < 2 || h < 2) return;
|
||||
|
||||
// Player grid position — center of the output
|
||||
float pgx, pgy;
|
||||
if (state.Player.HasPosition)
|
||||
{
|
||||
pgx = state.Player.Position.X * WorldToGrid;
|
||||
pgy = h - 1 - state.Player.Position.Y * WorldToGrid;
|
||||
}
|
||||
else
|
||||
{
|
||||
pgx = w * 0.5f;
|
||||
pgy = h * 0.5f;
|
||||
}
|
||||
|
||||
var bufSize = MinimapViewSize * MinimapViewSize * 4;
|
||||
if (_minimapBuffer is null || _minimapBuffer.Length != bufSize)
|
||||
_minimapBuffer = new byte[bufSize];
|
||||
var buf = _minimapBuffer;
|
||||
Array.Clear(buf, 0, bufSize);
|
||||
|
||||
var cx = MinimapViewSize * 0.5f;
|
||||
var cy = MinimapViewSize * 0.5f;
|
||||
|
||||
// Sample terrain with -45° rotation (nearest-neighbor, unsafe)
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* srcPtr = basePixels, dstPtr = buf)
|
||||
{
|
||||
var srcInt = (int*)srcPtr;
|
||||
var dstInt = (int*)dstPtr;
|
||||
for (var ry = 0; ry < MinimapViewSize; ry++)
|
||||
{
|
||||
var dy = ry - cy;
|
||||
var baseX = -dy * Sin45 + pgx;
|
||||
var baseY = dy * Cos45 + pgy;
|
||||
for (var rx = 0; rx < MinimapViewSize; rx++)
|
||||
{
|
||||
var dx = rx - cx;
|
||||
var sx = (int)(dx * Cos45 + baseX);
|
||||
var sy = (int)(dx * Sin45 + baseY);
|
||||
if ((uint)sx >= (uint)w || (uint)sy >= (uint)h) continue;
|
||||
|
||||
dstInt[ry * MinimapViewSize + rx] = srcInt[sy * w + sx];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw entity dots
|
||||
var stride = MinimapViewSize * 4;
|
||||
foreach (var e in state.Entities)
|
||||
{
|
||||
if (e.Position == Vector2.Zero) continue;
|
||||
|
||||
byte db, dg, dr;
|
||||
int dotRadius;
|
||||
switch (e.Category)
|
||||
{
|
||||
case EntityCategory.Monster when e.IsAlive:
|
||||
db = 0x44; dg = 0x44; dr = 0xFF; dotRadius = 3; break; // red
|
||||
case EntityCategory.Npc:
|
||||
db = 0x00; dg = 0x8C; dr = 0xFF; dotRadius = 3; break; // orange
|
||||
case EntityCategory.AreaTransition:
|
||||
db = 0xFF; dg = 0xFF; dr = 0x00; dotRadius = 4; break; // cyan
|
||||
case EntityCategory.Chest when !e.IsAlive: // opened chests
|
||||
continue;
|
||||
case EntityCategory.Chest:
|
||||
db = 0x00; dg = 0xD4; dr = 0xFF; dotRadius = 3; break; // gold
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
var edx = e.Position.X * WorldToGrid - pgx;
|
||||
var edy = (h - 1 - e.Position.Y * WorldToGrid) - pgy;
|
||||
var esx = (int)(edx * Cos45 + edy * Sin45 + cx);
|
||||
var esy = (int)(-edx * Sin45 + edy * Cos45 + cy);
|
||||
|
||||
if (esx >= 0 && esx < MinimapViewSize && esy >= 0 && esy < MinimapViewSize)
|
||||
DrawDot(buf, stride, MinimapViewSize, MinimapViewSize, esx, esy, dotRadius, db, dg, dr);
|
||||
}
|
||||
|
||||
// Draw nav path as green line
|
||||
var navPath = _engine.Nav.CurrentPath;
|
||||
if (navPath is { Count: > 1 })
|
||||
{
|
||||
for (var i = 0; i < navPath.Count; i++)
|
||||
{
|
||||
var px = navPath[i].X * WorldToGrid - pgx;
|
||||
var py = (h - 1 - navPath[i].Y * WorldToGrid) - pgy;
|
||||
var ex = (int)(px * Cos45 + py * Sin45 + cx);
|
||||
var ey = (int)(-px * Sin45 + py * Cos45 + cy);
|
||||
|
||||
if (ex >= 0 && ex < MinimapViewSize && ey >= 0 && ey < MinimapViewSize)
|
||||
DrawDot(buf, stride, MinimapViewSize, MinimapViewSize, ex, ey, 2, 0x50, 0xB9, 0x3F);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw player dot at center (white)
|
||||
if (state.Player.HasPosition)
|
||||
DrawDot(buf, stride, MinimapViewSize, MinimapViewSize, (int)cx, (int)cy, 4, 0xFF, 0xFF, 0xFF);
|
||||
|
||||
// Create WriteableBitmap
|
||||
var bmp = new WriteableBitmap(
|
||||
new PixelSize(MinimapViewSize, MinimapViewSize),
|
||||
new Avalonia.Vector(96, 96),
|
||||
Avalonia.Platform.PixelFormat.Bgra8888,
|
||||
Avalonia.Platform.AlphaFormat.Premul);
|
||||
|
||||
using (var fb = bmp.Lock())
|
||||
{
|
||||
Marshal.Copy(buf, 0, fb.Address, buf.Length);
|
||||
}
|
||||
|
||||
var old2 = TerrainImage;
|
||||
TerrainImage = bmp;
|
||||
old2?.Dispose();
|
||||
}
|
||||
|
||||
private static void DrawDot(byte[] buf, int stride, int bw, int bh, int cx, int cy, int radius, byte b, byte g, byte r)
|
||||
{
|
||||
var outer = radius + 1;
|
||||
for (var dy = -outer; dy <= outer; dy++)
|
||||
{
|
||||
for (var dx = -outer; dx <= outer; dx++)
|
||||
{
|
||||
var sx = cx + dx;
|
||||
var sy = cy + dy;
|
||||
if (sx < 0 || sx >= bw || sy < 0 || sy >= bh) continue;
|
||||
|
||||
var dist = MathF.Sqrt(dx * dx + dy * dy);
|
||||
if (dist > radius + 0.5f) continue;
|
||||
|
||||
var alpha = Math.Clamp(radius + 0.5f - dist, 0f, 1f);
|
||||
var i = sy * stride + sx * 4;
|
||||
buf[i] = (byte)(b * alpha + buf[i] * (1 - alpha));
|
||||
buf[i + 1] = (byte)(g * alpha + buf[i + 1] * (1 - alpha));
|
||||
buf[i + 2] = (byte)(r * alpha + buf[i + 2] * (1 - alpha));
|
||||
buf[i + 3] = (byte)(Math.Max(alpha, buf[i + 3] / 255f) * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetShortLabel(string? path)
|
||||
{
|
||||
if (path is null) return "?";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue