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,81 @@
namespace Roboto.Core;
public class ActionQueue
{
private readonly List<BotAction> _actions = [];
public IReadOnlyList<BotAction> Actions => _actions;
public void Submit(BotAction action)
{
// Bump lower-priority same-type actions
for (var i = _actions.Count - 1; i >= 0; i--)
{
if (_actions[i].GetType() == action.GetType() && _actions[i].Priority >= action.Priority)
_actions.RemoveAt(i);
}
_actions.Add(action);
}
public void Enqueue(BotAction action) => Submit(action);
public void Clear() => _actions.Clear();
public T? GetHighestPriority<T>() where T : BotAction
{
T? best = null;
foreach (var action in _actions)
{
if (action is T typed && (best is null || typed.Priority < best.Priority))
best = typed;
}
return best;
}
/// <summary>
/// Resolve conflicts and return the final action list:
/// 1. FlaskActions always pass through
/// 2. Get highest-priority MoveAction + CastAction
/// 3. Urgent move (priority ≤ 10) → include move, BLOCK cast (flee)
/// 4. Normal → include both cast + move
/// 5. All other actions pass through
/// </summary>
public List<BotAction> Resolve()
{
var resolved = new List<BotAction>();
// Flasks always pass through
foreach (var action in _actions)
{
if (action is FlaskAction)
resolved.Add(action);
}
var bestMove = GetHighestPriority<MoveAction>();
var bestCast = GetHighestPriority<CastAction>();
if (bestMove is not null)
{
resolved.Add(bestMove);
// Urgent flee (priority ≤ 10) blocks casting
if (bestMove.Priority > 10 && bestCast is not null)
resolved.Add(bestCast);
}
else if (bestCast is not null)
{
resolved.Add(bestCast);
}
// Pass through everything else (Key, Click, Chat, Wait) except types already handled
foreach (var action in _actions)
{
if (action is MoveAction or CastAction or FlaskAction)
continue;
resolved.Add(action);
}
Clear();
return resolved;
}
}

View file

@ -0,0 +1,26 @@
using System.Numerics;
namespace Roboto.Core;
public enum ClickType { Left, Right }
public enum KeyActionType { Press, Down, Up }
public abstract record BotAction(int Priority);
public record MoveAction(int Priority, Vector2 Direction) : BotAction(Priority)
{
/// <summary>Normalized movement direction in world space.</summary>
public Vector2 Direction { get; init; } = Direction;
}
public record ClickAction(int Priority, Vector2 ScreenPosition, ClickType Type = ClickType.Left) : BotAction(Priority);
public record KeyAction(int Priority, ushort ScanCode, KeyActionType Type = KeyActionType.Press) : BotAction(Priority);
public record CastAction(int Priority, ushort SkillScanCode, Vector2? TargetScreenPos = null) : BotAction(Priority);
public record FlaskAction(int Priority, ushort FlaskScanCode) : BotAction(Priority);
public record ChatAction(int Priority, string Message) : BotAction(Priority);
public record WaitAction(int Priority, int DurationMs) : BotAction(Priority);

View file

@ -0,0 +1,40 @@
namespace Roboto.Core;
public class BotConfig
{
// Tick rates
public int LogicTickRateHz { get; set; } = 60;
public int MemoryPollRateHz { get; set; } = 30;
// Movement
public float SafeDistance { get; set; } = 400f;
public float RepulsionWeight { get; set; } = 1.5f;
public float WaypointReachedDistance { get; set; } = 80f;
// Navigation
public float WorldToGrid { get; set; } = 23f / 250f;
// Combat
public float CriticalHpPercent { get; set; } = 30f;
public float LowHpPercent { get; set; } = 50f;
// Loot
public float LootPickupRange { get; set; } = 600f;
// Humanization
public int MinReactionDelayMs { get; set; } = 50;
public int MaxReactionDelayMs { get; set; } = 150;
public float ClickJitterRadius { get; set; } = 3f;
public float TimingNoiseStdDev { get; set; } = 0.15f;
public int MaxApm { get; set; } = 250;
// Anti-detection
public float PollIntervalJitter { get; set; } = 0.2f;
// Flasks
public float LifeFlaskThreshold { get; set; } = 50f;
public float ManaFlaskThreshold { get; set; } = 50f;
public int FlaskCooldownMs { get; set; } = 4000;
public ushort LifeFlaskScanCode { get; set; } = 0x02; // Key1
public ushort ManaFlaskScanCode { get; set; } = 0x03; // Key2
}

9
src/Roboto.Core/Buff.cs Normal file
View file

@ -0,0 +1,9 @@
namespace Roboto.Core;
public record Buff
{
public string? Name { get; init; }
public float DurationRemaining { get; init; }
public int Charges { get; init; }
public bool IsDebuff { get; init; }
}

View file

@ -0,0 +1,42 @@
using System.Numerics;
namespace Roboto.Core;
public enum EntityCategory
{
Unknown,
Player,
Monster,
Npc,
WorldItem,
Chest,
Portal,
AreaTransition,
Effect,
Terrain,
MiscObject,
}
public enum MonsterThreatLevel
{
None,
Normal,
Magic,
Rare,
Unique,
}
public record EntitySnapshot
{
public uint Id { get; init; }
public string? Path { get; init; }
public EntityCategory Category { get; init; }
public MonsterThreatLevel ThreatLevel { get; init; }
public Vector2 Position { get; init; }
public float DistanceToPlayer { get; init; }
public bool IsAlive { get; init; }
public int LifeCurrent { get; init; }
public int LifeTotal { get; init; }
public bool IsTargetable { get; init; }
public HashSet<string>? Components { get; init; }
}

20
src/Roboto.Core/Enums.cs Normal file
View file

@ -0,0 +1,20 @@
namespace Roboto.Core;
public enum DangerLevel
{
Safe,
Low,
Medium,
High,
Critical,
}
public static class SystemPriority
{
public const int Threat = 50;
public const int Movement = 100;
public const int Navigation = 200;
public const int Combat = 300;
public const int Resource = 400;
public const int Loot = 500;
}

View file

@ -0,0 +1,10 @@
namespace Roboto.Core;
public record FlaskState
{
public int SlotIndex { get; init; }
public int ChargesCurrent { get; init; }
public int ChargesMax { get; init; }
public bool IsActive { get; init; }
public float CooldownRemaining { get; init; }
}

View file

@ -0,0 +1,28 @@
using System.Numerics;
namespace Roboto.Core;
public class GameState
{
public long TickNumber { get; set; }
public float DeltaTime { get; set; }
public long TimestampMs { get; set; }
public PlayerState Player { get; set; } = new();
public IReadOnlyList<EntitySnapshot> Entities { get; set; } = [];
public IReadOnlyList<EntitySnapshot> HostileMonsters { get; set; } = [];
public IReadOnlyList<EntitySnapshot> NearbyLoot { get; set; } = [];
public WalkabilitySnapshot? Terrain { get; set; }
public uint AreaHash { get; set; }
public int AreaLevel { get; set; }
public bool IsLoading { get; set; }
public bool IsEscapeOpen { get; set; }
public DangerLevel Danger { get; set; }
public Matrix4x4? CameraMatrix { get; set; }
// Derived (computed once per tick by GameStateEnricher)
public ThreatMap Threats { get; set; } = new();
public IReadOnlyList<EntitySnapshot> NearestEnemies { get; set; } = [];
public IReadOnlyList<GroundEffect> GroundEffects { get; set; } = [];
}

View file

@ -0,0 +1,20 @@
using System.Numerics;
namespace Roboto.Core;
public record GroundEffect
{
public Vector2 Position { get; init; }
public float Radius { get; init; }
public GroundEffectType Type { get; init; }
}
public enum GroundEffectType
{
Unknown,
Fire,
Cold,
Lightning,
Chaos,
Physical,
}

View file

@ -0,0 +1,17 @@
namespace Roboto.Core;
public interface IInputController
{
bool IsInitialized { get; }
void KeyDown(ushort scanCode);
void KeyUp(ushort scanCode);
void KeyPress(ushort scanCode, int holdMs = 50);
void MouseMoveTo(int x, int y);
void MouseMoveBy(int dx, int dy);
void LeftClick(int x, int y);
void RightClick(int x, int y);
void LeftDown();
void LeftUp();
void RightDown();
void RightUp();
}

View file

@ -0,0 +1,9 @@
namespace Roboto.Core;
public interface IMemoryProvider
{
bool IsAttached { get; }
bool Attach();
void Detach();
GameState ReadGameState(GameState? previous);
}

View file

@ -0,0 +1,9 @@
namespace Roboto.Core;
public interface ISystem
{
int Priority { get; }
string Name { get; }
bool IsEnabled { get; set; }
void Update(GameState state, ActionQueue actions);
}

View file

@ -0,0 +1,30 @@
using System.Numerics;
namespace Roboto.Core;
public record PlayerState
{
public Vector2 Position { get; init; }
public float Z { get; init; }
public bool HasPosition { get; init; }
public int LifeCurrent { get; init; }
public int LifeTotal { get; init; }
public int ManaCurrent { get; init; }
public int ManaTotal { get; init; }
public int EsCurrent { get; init; }
public int EsTotal { get; init; }
public float LifePercent => LifeTotal > 0 ? (float)LifeCurrent / LifeTotal * 100f : 0f;
public float ManaPercent => ManaTotal > 0 ? (float)ManaCurrent / ManaTotal * 100f : 0f;
public float EsPercent => EsTotal > 0 ? (float)EsCurrent / EsTotal * 100f : 0f;
// Flask state (populated by memory when available)
public IReadOnlyList<FlaskState> Flasks { get; init; } = [];
// Active buffs (populated by memory when available)
public IReadOnlyList<Buff> Buffs { get; init; } = [];
// Skill slots (populated by memory when available)
public IReadOnlyList<SkillState> Skills { get; init; } = [];
}

View file

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,12 @@
namespace Roboto.Core;
public record SkillState
{
public int SlotIndex { get; init; }
public ushort ScanCode { get; init; }
public string? Name { get; init; }
public int ChargesCurrent { get; init; }
public int ChargesMax { get; init; }
public float CooldownRemaining { get; init; }
public bool CanUse => CooldownRemaining <= 0 && ChargesCurrent > 0;
}

View file

@ -0,0 +1,14 @@
using System.Numerics;
namespace Roboto.Core;
public class ThreatMap
{
public int TotalHostiles { get; init; }
public int CloseRange { get; init; } // < 300 units
public int MidRange { get; init; } // 300600
public int FarRange { get; init; } // 6001200
public float ClosestDistance { get; init; } = float.MaxValue;
public Vector2 ThreatCentroid { get; init; }
public bool HasRareOrUnique { get; init; }
}

View file

@ -0,0 +1,15 @@
namespace Roboto.Core;
public record WalkabilitySnapshot
{
public int Width { get; init; }
public int Height { get; init; }
public byte[] Data { get; init; } = [];
public bool IsWalkable(int x, int y)
{
if (x < 0 || x >= Width || y < 0 || y >= Height)
return false;
return Data[y * Width + x] != 0;
}
}