lots done
This commit is contained in:
parent
1ba7c39c30
commit
fbd0ba445a
59 changed files with 6074 additions and 3598 deletions
269
src/Automata.Ui/ViewModels/RobotoViewModel.cs
Normal file
269
src/Automata.Ui/ViewModels/RobotoViewModel.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Numerics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Roboto.Core;
|
||||
using Roboto.Engine;
|
||||
using Roboto.Input;
|
||||
using Roboto.Navigation;
|
||||
|
||||
namespace Automata.Ui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe snapshot read by the overlay layer each frame.
|
||||
/// </summary>
|
||||
public sealed class EntityOverlayData
|
||||
{
|
||||
public Vector2 PlayerPosition;
|
||||
public float PlayerZ;
|
||||
public Matrix4x4? CameraMatrix;
|
||||
public EntityOverlayEntry[] Entries = [];
|
||||
}
|
||||
|
||||
public readonly struct EntityOverlayEntry
|
||||
{
|
||||
public readonly float X, Y;
|
||||
public readonly string Label;
|
||||
|
||||
public EntityOverlayEntry(float x, float y, string label)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model item for entity checkbox list.
|
||||
/// </summary>
|
||||
public partial class EntityListItem : ObservableObject
|
||||
{
|
||||
public uint Id { get; }
|
||||
public string Label { get; }
|
||||
public string Category { get; }
|
||||
public string Distance { get; set; }
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
|
||||
[ObservableProperty] private bool _isChecked;
|
||||
|
||||
public EntityListItem(uint id, string label, string category, float distance, float x, float y)
|
||||
{
|
||||
Id = id;
|
||||
Label = label;
|
||||
Category = category;
|
||||
Distance = $"{distance:F0}";
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RobotoViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly BotEngine _engine;
|
||||
private long _lastUiUpdate;
|
||||
private bool _disposed;
|
||||
|
||||
[ObservableProperty] private string _statusText = "Stopped";
|
||||
[ObservableProperty] private bool _isRunning;
|
||||
|
||||
// Player state
|
||||
[ObservableProperty] private string _playerPosition = "—";
|
||||
[ObservableProperty] private string _playerLife = "—";
|
||||
[ObservableProperty] private string _playerMana = "—";
|
||||
[ObservableProperty] private string _playerEs = "—";
|
||||
|
||||
// Game state
|
||||
[ObservableProperty] private string _areaInfo = "—";
|
||||
[ObservableProperty] private string _dangerLevel = "—";
|
||||
[ObservableProperty] private string _entityCount = "—";
|
||||
[ObservableProperty] private string _hostileCount = "—";
|
||||
[ObservableProperty] private string _tickInfo = "—";
|
||||
|
||||
// Systems
|
||||
[ObservableProperty] private string _systemsInfo = "—";
|
||||
|
||||
// Navigation
|
||||
[ObservableProperty] private string _navMode = "Idle";
|
||||
[ObservableProperty] private string _navStatus = "—";
|
||||
|
||||
// Entity list for checkbox UI
|
||||
[ObservableProperty] private bool _showAllEntities;
|
||||
public ObservableCollection<EntityListItem> Entities { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe snapshot for the overlay layer (written on UI thread, read on overlay thread).
|
||||
/// </summary>
|
||||
public static volatile EntityOverlayData? OverlayData;
|
||||
|
||||
public RobotoViewModel()
|
||||
{
|
||||
var config = new BotConfig();
|
||||
var memory = new MemoryAdapter();
|
||||
var humanizer = new Humanizer(config);
|
||||
var input = new InterceptionInputController(humanizer);
|
||||
|
||||
_engine = new BotEngine(config, memory, input);
|
||||
|
||||
_engine.StatusChanged += status =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() => StatusText = status);
|
||||
};
|
||||
|
||||
_engine.StateUpdated += OnStateUpdated;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Start()
|
||||
{
|
||||
if (_engine.IsRunning) return;
|
||||
var ok = _engine.Start();
|
||||
IsRunning = _engine.IsRunning;
|
||||
if (!ok)
|
||||
StatusText = _engine.Status;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Stop()
|
||||
{
|
||||
_engine.Stop();
|
||||
IsRunning = false;
|
||||
StatusText = "Stopped";
|
||||
PlayerPosition = "—";
|
||||
PlayerLife = "—";
|
||||
PlayerMana = "—";
|
||||
PlayerEs = "—";
|
||||
AreaInfo = "—";
|
||||
DangerLevel = "—";
|
||||
EntityCount = "—";
|
||||
HostileCount = "—";
|
||||
TickInfo = "—";
|
||||
NavMode = "Idle";
|
||||
NavStatus = "—";
|
||||
Entities.Clear();
|
||||
OverlayData = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Explore()
|
||||
{
|
||||
if (!_engine.IsRunning) return;
|
||||
_engine.Nav.Explore();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void StopNav()
|
||||
{
|
||||
_engine.Nav.Stop();
|
||||
}
|
||||
|
||||
private void OnStateUpdated()
|
||||
{
|
||||
// Throttle UI updates to ~10Hz
|
||||
var now = Environment.TickCount64;
|
||||
if (now - _lastUiUpdate < 100) return;
|
||||
_lastUiUpdate = now;
|
||||
|
||||
var state = _engine.CurrentState;
|
||||
if (state is null) return;
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() => UpdateFromState(state));
|
||||
}
|
||||
|
||||
private void UpdateFromState(GameState state)
|
||||
{
|
||||
var p = state.Player;
|
||||
PlayerPosition = p.HasPosition
|
||||
? $"({p.Position.X:F1}, {p.Position.Y:F1})"
|
||||
: "—";
|
||||
PlayerLife = p.LifeTotal > 0 ? $"{p.LifeCurrent}/{p.LifeTotal} ({p.LifePercent:F0}%)" : "—";
|
||||
PlayerMana = p.ManaTotal > 0 ? $"{p.ManaCurrent}/{p.ManaTotal} ({p.ManaPercent:F0}%)" : "—";
|
||||
PlayerEs = p.EsTotal > 0 ? $"{p.EsCurrent}/{p.EsTotal} ({p.EsPercent:F0}%)" : "—";
|
||||
|
||||
AreaInfo = state.AreaHash != 0
|
||||
? $"Level {state.AreaLevel} (0x{state.AreaHash:X8})"
|
||||
: "—";
|
||||
DangerLevel = state.Danger.ToString();
|
||||
EntityCount = $"{state.Entities.Count} total";
|
||||
HostileCount = $"{state.HostileMonsters.Count} hostile";
|
||||
TickInfo = $"Tick {state.TickNumber}, dt={state.DeltaTime * 1000:F0}ms";
|
||||
|
||||
// Systems status
|
||||
var systems = _engine.Systems;
|
||||
var enabled = systems.Count(s => s.IsEnabled);
|
||||
SystemsInfo = $"{enabled}/{systems.Count} active";
|
||||
|
||||
// Navigation
|
||||
NavMode = _engine.Nav.Mode.ToString();
|
||||
NavStatus = _engine.Nav.Status;
|
||||
|
||||
// Entity list
|
||||
UpdateEntityList(state);
|
||||
}
|
||||
|
||||
private void UpdateEntityList(GameState state)
|
||||
{
|
||||
// Build lookup of currently checked IDs
|
||||
var checkedIds = new HashSet<uint>();
|
||||
foreach (var item in Entities)
|
||||
if (item.IsChecked) checkedIds.Add(item.Id);
|
||||
|
||||
// Rebuild the list
|
||||
Entities.Clear();
|
||||
foreach (var e in state.Entities)
|
||||
{
|
||||
var shortLabel = GetShortLabel(e.Path);
|
||||
var item = new EntityListItem(e.Id, shortLabel, e.Category.ToString(), e.DistanceToPlayer, e.Position.X, e.Position.Y);
|
||||
if (checkedIds.Contains(e.Id))
|
||||
item.IsChecked = true;
|
||||
Entities.Add(item);
|
||||
}
|
||||
|
||||
// Build overlay snapshot from checked (or all) entities
|
||||
var showAll = ShowAllEntities;
|
||||
var overlayEntries = new List<EntityOverlayEntry>();
|
||||
foreach (var item in Entities)
|
||||
{
|
||||
if (!showAll && !item.IsChecked) continue;
|
||||
overlayEntries.Add(new EntityOverlayEntry(item.X, item.Y, item.Label));
|
||||
}
|
||||
|
||||
if (overlayEntries.Count > 0)
|
||||
{
|
||||
OverlayData = new EntityOverlayData
|
||||
{
|
||||
PlayerPosition = state.Player.Position,
|
||||
PlayerZ = state.Player.Z,
|
||||
CameraMatrix = state.CameraMatrix,
|
||||
Entries = overlayEntries.ToArray(),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
OverlayData = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetShortLabel(string? path)
|
||||
{
|
||||
if (path is null) return "?";
|
||||
// Strip @N instance suffix
|
||||
var atIdx = path.LastIndexOf('@');
|
||||
if (atIdx > 0) path = path[..atIdx];
|
||||
// Take last segment
|
||||
var slashIdx = path.LastIndexOf('/');
|
||||
return slashIdx >= 0 ? path[(slashIdx + 1)..] : path;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_engine.Stop();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_engine.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue