lots done

This commit is contained in:
Boki 2026-03-02 11:17:37 -05:00
parent 1ba7c39c30
commit fbd0ba445a
59 changed files with 6074 additions and 3598 deletions

View 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();
}
}