mirror of
https://github.com/Nighthawk42/YodaStoriesNG.git
synced 2026-08-30 09:02:26 +00:00
Fix bot exploration and add hostility detection
- Track unreachable NPCs to avoid infinite retries - Clear unreachable list when changing zones - Add name-based hostility detection for NPCs - Check top layer tiles for walkability - Fix MissionSolver to skip likely hostile NPCs by name patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
0087c9399a
commit
3d3cb2920a
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.WriteLine("Usage: DecompressSzdd input.DA_ output.DTA");
|
||||
return;
|
||||
}
|
||||
|
||||
DecompressSzdd(args[0], args[1]);
|
||||
}
|
||||
|
||||
static void DecompressSzdd(string inputPath, string outputPath)
|
||||
{
|
||||
using var fs = File.OpenRead(inputPath);
|
||||
using var reader = new BinaryReader(fs);
|
||||
|
||||
// Read magic "SZDD"
|
||||
var magic = reader.ReadBytes(4);
|
||||
if (magic[0] != 'S' || magic[1] != 'Z' || magic[2] != 'D' || magic[3] != 'D')
|
||||
{
|
||||
Console.WriteLine($"Not an SZDD file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read header
|
||||
var compMode = reader.ReadByte();
|
||||
var missingChar = reader.ReadByte();
|
||||
reader.ReadBytes(2); // padding
|
||||
|
||||
var uncompressedSize = reader.ReadUInt32();
|
||||
Console.WriteLine($"Uncompressed size: {uncompressedSize}");
|
||||
|
||||
// Read compressed data
|
||||
var compressed = reader.ReadBytes((int)(fs.Length - fs.Position));
|
||||
|
||||
// Decompress
|
||||
var output = new MemoryStream();
|
||||
var ringBuffer = new byte[4096];
|
||||
int ringPos = 4096 - 16;
|
||||
|
||||
int i = 0;
|
||||
while (i < compressed.Length && output.Length < uncompressedSize)
|
||||
{
|
||||
byte flags = compressed[i++];
|
||||
|
||||
for (int bit = 0; bit < 8 && i < compressed.Length && output.Length < uncompressedSize; bit++)
|
||||
{
|
||||
if ((flags & (1 << bit)) != 0)
|
||||
{
|
||||
// Literal byte
|
||||
byte b = compressed[i++];
|
||||
output.WriteByte(b);
|
||||
ringBuffer[ringPos] = b;
|
||||
ringPos = (ringPos + 1) & 0xFFF;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Back reference
|
||||
if (i + 1 >= compressed.Length) break;
|
||||
|
||||
int b1 = compressed[i++];
|
||||
int b2 = compressed[i++];
|
||||
|
||||
int offset = b1 | ((b2 & 0xF0) << 4);
|
||||
int length = (b2 & 0x0F) + 3;
|
||||
|
||||
for (int j = 0; j < length && output.Length < uncompressedSize; j++)
|
||||
{
|
||||
byte b = ringBuffer[offset];
|
||||
output.WriteByte(b);
|
||||
ringBuffer[ringPos] = b;
|
||||
ringPos = (ringPos + 1) & 0xFFF;
|
||||
offset = (offset + 1) & 0xFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllBytes(outputPath, output.ToArray());
|
||||
Console.WriteLine($"Decompressed {output.Length} bytes to {outputPath}");
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ public class MissionBot
|
||||
// Random for exploration
|
||||
private readonly Random _random = new();
|
||||
|
||||
// Track NPCs we can't reach (to avoid retrying forever)
|
||||
private readonly HashSet<(int, int)> _unreachableNpcs = new();
|
||||
|
||||
// Events
|
||||
public event Action<BotActionType, int, int, Direction>? OnActionRequested;
|
||||
|
||||
@@ -95,6 +98,7 @@ public class MissionBot
|
||||
_stuckTimer = 0;
|
||||
_explorationAttempts = 0;
|
||||
_lastPosition = (_state.PlayerX, _state.PlayerY, _state.CurrentZoneId);
|
||||
_unreachableNpcs.Clear();
|
||||
|
||||
Console.WriteLine("[BOT] Started");
|
||||
LogMissionState();
|
||||
@@ -344,12 +348,20 @@ public class MissionBot
|
||||
|
||||
// Check if there's anything useful in current zone first
|
||||
var friendlyNpc = _solver.FindNearestFriendlyNpc();
|
||||
if (friendlyNpc != null)
|
||||
if (friendlyNpc != null && !_unreachableNpcs.Contains((friendlyNpc.X, friendlyNpc.Y)))
|
||||
{
|
||||
Console.WriteLine($"[BOT] Found friendly NPC at ({friendlyNpc.X},{friendlyNpc.Y})");
|
||||
_actions.TalkToNpc(friendlyNpc);
|
||||
_currentState = BotState.ExecutingObjective;
|
||||
return;
|
||||
if (_actions.TalkToNpc(friendlyNpc))
|
||||
{
|
||||
_currentState = BotState.ExecutingObjective;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find path to this NPC, mark as unreachable
|
||||
Console.WriteLine($"[BOT] Marking NPC at ({friendlyNpc.X},{friendlyNpc.Y}) as unreachable");
|
||||
_unreachableNpcs.Add((friendlyNpc.X, friendlyNpc.Y));
|
||||
}
|
||||
}
|
||||
|
||||
// Look for unexplored doors
|
||||
@@ -490,6 +502,14 @@ public class MissionBot
|
||||
{
|
||||
var currentPos = (_state.PlayerX, _state.PlayerY, _state.CurrentZoneId);
|
||||
|
||||
// Clear unreachable NPCs when zone changes
|
||||
if (currentPos.CurrentZoneId != _lastPosition.ZoneId)
|
||||
{
|
||||
Console.WriteLine($"[BOT] Zone changed to {currentPos.CurrentZoneId}, clearing unreachable NPCs");
|
||||
_unreachableNpcs.Clear();
|
||||
_explorationAttempts = 0;
|
||||
}
|
||||
|
||||
if (currentPos == _lastPosition)
|
||||
{
|
||||
_stuckTimer += deltaTime;
|
||||
|
||||
@@ -15,6 +15,12 @@ public class ActionExecutor
|
||||
// Action execution context
|
||||
private int _lastRandomValue;
|
||||
private Zone? _currentZone;
|
||||
private ActionTrigger _currentTrigger;
|
||||
|
||||
/// <summary>
|
||||
/// When true, dialogue instructions are suppressed.
|
||||
/// </summary>
|
||||
public bool SuppressDialogue { get; set; }
|
||||
|
||||
// Current interaction context
|
||||
public int? PlacedItemId { get; set; }
|
||||
@@ -40,6 +46,7 @@ public class ActionExecutor
|
||||
public void ExecuteZoneActions(ActionTrigger trigger)
|
||||
{
|
||||
_currentZone = _state.CurrentZone;
|
||||
_currentTrigger = trigger;
|
||||
if (_currentZone == null)
|
||||
return;
|
||||
|
||||
@@ -52,6 +59,16 @@ public class ActionExecutor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current trigger is player-initiated (NpcTalk, UseItem).
|
||||
/// Dialogue should only show for these triggers, not for ZoneEnter, Walk, Bump, etc.
|
||||
/// </summary>
|
||||
private bool IsPlayerInitiatedTrigger()
|
||||
{
|
||||
return _currentTrigger == ActionTrigger.NpcTalk ||
|
||||
_currentTrigger == ActionTrigger.UseItem;
|
||||
}
|
||||
|
||||
private bool EvaluateConditions(List<Condition> conditions, ActionTrigger trigger)
|
||||
{
|
||||
foreach (var condition in conditions)
|
||||
@@ -339,7 +356,8 @@ public class ActionExecutor
|
||||
break;
|
||||
|
||||
case InstructionOpcode.SpeakHero:
|
||||
if (!string.IsNullOrEmpty(instruction.Text))
|
||||
// Only show dialogue if not suppressed AND trigger is player-initiated
|
||||
if (!string.IsNullOrEmpty(instruction.Text) && !SuppressDialogue && IsPlayerInitiatedTrigger())
|
||||
{
|
||||
OnDialogue?.Invoke("Luke", instruction.Text);
|
||||
}
|
||||
@@ -347,7 +365,8 @@ public class ActionExecutor
|
||||
|
||||
case InstructionOpcode.SpeakNpc:
|
||||
case InstructionOpcode.SpeakNpc2:
|
||||
if (!string.IsNullOrEmpty(instruction.Text))
|
||||
// Only show dialogue if not suppressed AND trigger is player-initiated
|
||||
if (!string.IsNullOrEmpty(instruction.Text) && !SuppressDialogue && IsPlayerInitiatedTrigger())
|
||||
{
|
||||
// Get NPC name from argument if provided
|
||||
string npcName = "NPC";
|
||||
|
||||
@@ -36,6 +36,7 @@ public class GameState
|
||||
public bool IsGameOver { get; set; }
|
||||
public bool IsGameWon { get; set; }
|
||||
public bool IsPaused { get; set; }
|
||||
public bool HasLocator { get; set; } // Has picked up R2D2/locator droid
|
||||
|
||||
// Animation state
|
||||
public int AnimationFrame { get; set; }
|
||||
@@ -89,6 +90,7 @@ public class GameState
|
||||
IsGameOver = false;
|
||||
IsGameWon = false;
|
||||
IsPaused = false;
|
||||
HasLocator = false;
|
||||
AnimationFrame = 0;
|
||||
AnimationTimer = 0;
|
||||
CameraX = 0;
|
||||
|
||||
@@ -220,18 +220,19 @@ public class WorldGenerator
|
||||
|
||||
/// <summary>
|
||||
/// Parses and dumps IZAX entity data.
|
||||
/// IZAX format: 2 bytes count, then for each entity: charId(2), x(2), y(2), itemTile(2), itemQuantity(2), data(6)
|
||||
/// IZAX format: 4 bytes header, 2 bytes count, then for each entity: charId(2), x(2), y(2), itemTile(2), itemQuantity(2), data(6)
|
||||
/// </summary>
|
||||
private void DumpIzaxEntities(byte[] izaxData)
|
||||
{
|
||||
if (izaxData.Length < 2) return;
|
||||
if (izaxData.Length < 6) return;
|
||||
|
||||
using var ms = new MemoryStream(izaxData);
|
||||
using var reader = new BinaryReader(ms);
|
||||
|
||||
try
|
||||
{
|
||||
// IZAX starts with entity count (2 bytes)
|
||||
// Skip 4-byte header, then read entity count (2 bytes)
|
||||
reader.ReadUInt32();
|
||||
var entityCount = reader.ReadUInt16();
|
||||
Console.WriteLine($" Entity count: {entityCount}");
|
||||
|
||||
@@ -542,14 +543,11 @@ public class WorldGenerator
|
||||
CurrentWorld.StartingZoneId = dagobahZones[0]; // Should be 93
|
||||
CurrentWorld.XWingZoneId = CurrentWorld.StartingZoneId;
|
||||
|
||||
// Randomly pick one of 4 positions for Yoda to appear
|
||||
// (Yoda can appear in any of the 4 Dagobah outdoor zones)
|
||||
var yodaZoneIndex = _random.Next(Math.Min(4, dagobahZones.Count));
|
||||
CurrentWorld.YodaZoneId = dagobahZones[yodaZoneIndex];
|
||||
// Yoda always appears in the starting zone for easy access
|
||||
CurrentWorld.YodaZoneId = CurrentWorld.StartingZoneId;
|
||||
|
||||
// Random position within the zone for Yoda (4 possible positions)
|
||||
var yodaPositions = new[] { (5, 5), (12, 5), (5, 12), (12, 12) };
|
||||
CurrentWorld.YodaPosition = yodaPositions[_random.Next(yodaPositions.Length)];
|
||||
// Yoda spawns near the player (player starts around 13,4, Yoda at 11,4)
|
||||
CurrentWorld.YodaPosition = (11, 4);
|
||||
|
||||
Console.WriteLine($"Yoda will appear in zone {CurrentWorld.YodaZoneId} at position {CurrentWorld.YodaPosition}");
|
||||
|
||||
|
||||
@@ -458,20 +458,23 @@ public class DtaParser
|
||||
|
||||
/// <summary>
|
||||
/// Parses IZAX entity data from raw bytes.
|
||||
/// IZAX format: 2 bytes count, then for each entity: charId(2), x(2), y(2), itemTile(2), itemQty(2), data(6)
|
||||
/// IZAX format: 4 bytes header, 2 bytes count, then for each entity: charId(2), x(2), y(2), itemTile(2), itemQty(2), data(6)
|
||||
/// </summary>
|
||||
private ZoneAuxData ParseIZAXData(byte[] data)
|
||||
{
|
||||
var auxData = new ZoneAuxData { RawData = data };
|
||||
|
||||
// Parse entity data from raw bytes
|
||||
if (data.Length >= 2)
|
||||
// IZAX has a 4-byte header before the entity count
|
||||
if (data.Length >= 6)
|
||||
{
|
||||
using var ms = new MemoryStream(data);
|
||||
using var entityReader = new BinaryReader(ms);
|
||||
|
||||
try
|
||||
{
|
||||
// Skip 4-byte header
|
||||
entityReader.ReadUInt32();
|
||||
var entityCount = entityReader.ReadUInt16();
|
||||
|
||||
// Each entity is 16 bytes: charId(2) + x(2) + y(2) + itemTile(2) + itemQty(2) + data(6)
|
||||
|
||||
@@ -163,4 +163,13 @@ public class MessageSystem
|
||||
_messageQueue.Clear();
|
||||
_currentDialogue = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears only dialogue (the left-side box), keeping other messages.
|
||||
/// </summary>
|
||||
public void ClearDialogue()
|
||||
{
|
||||
_messageQueue.Clear();
|
||||
_currentDialogue = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user