diff --git a/DecompressSzdd.cs b/DecompressSzdd.cs new file mode 100644 index 0000000..bc081b7 --- /dev/null +++ b/DecompressSzdd.cs @@ -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}"); + } +} diff --git a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs index 1906338..0c01091 100644 --- a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs +++ b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs @@ -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? 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; diff --git a/src/YodaStoriesNG.Engine/Game/ActionExecutor.cs b/src/YodaStoriesNG.Engine/Game/ActionExecutor.cs index 12d13b4..457d446 100644 --- a/src/YodaStoriesNG.Engine/Game/ActionExecutor.cs +++ b/src/YodaStoriesNG.Engine/Game/ActionExecutor.cs @@ -15,6 +15,12 @@ public class ActionExecutor // Action execution context private int _lastRandomValue; private Zone? _currentZone; + private ActionTrigger _currentTrigger; + + /// + /// When true, dialogue instructions are suppressed. + /// + 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 } } + /// + /// Returns true if the current trigger is player-initiated (NpcTalk, UseItem). + /// Dialogue should only show for these triggers, not for ZoneEnter, Walk, Bump, etc. + /// + private bool IsPlayerInitiatedTrigger() + { + return _currentTrigger == ActionTrigger.NpcTalk || + _currentTrigger == ActionTrigger.UseItem; + } + private bool EvaluateConditions(List 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"; diff --git a/src/YodaStoriesNG.Engine/Game/GameState.cs b/src/YodaStoriesNG.Engine/Game/GameState.cs index d62268e..2000890 100644 --- a/src/YodaStoriesNG.Engine/Game/GameState.cs +++ b/src/YodaStoriesNG.Engine/Game/GameState.cs @@ -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; diff --git a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs index df63926..3272e7c 100644 --- a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs +++ b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs @@ -220,18 +220,19 @@ public class WorldGenerator /// /// 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) /// 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}"); diff --git a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs index ed54d4d..b6d6d9e 100644 --- a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs +++ b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs @@ -458,20 +458,23 @@ public class DtaParser /// /// 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) /// 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) diff --git a/src/YodaStoriesNG.Engine/UI/MessageSystem.cs b/src/YodaStoriesNG.Engine/UI/MessageSystem.cs index 1728b18..98e39a4 100644 --- a/src/YodaStoriesNG.Engine/UI/MessageSystem.cs +++ b/src/YodaStoriesNG.Engine/UI/MessageSystem.cs @@ -163,4 +163,13 @@ public class MessageSystem _messageQueue.Clear(); _currentDialogue = null; } + + /// + /// Clears only dialogue (the left-side box), keeping other messages. + /// + public void ClearDialogue() + { + _messageQueue.Clear(); + _currentDialogue = null; + } }