diff --git a/FUTURE_FEATURES.md b/FUTURE_FEATURES.md new file mode 100644 index 0000000..9e2ae7e --- /dev/null +++ b/FUTURE_FEATURES.md @@ -0,0 +1,33 @@ +# Future Features - WebFun-Style Development Tools + +Based on the WebFun implementation (https://codeberg.org/cyco/WebFun), these features would be valuable additions: + +## Save Game Inspector +- View and edit saved game state +- Inspect inventory, position, zone state +- Debug mission progress + +## Zone Editor +- Visual zone editing with tile placement +- Edit zone actions/scripts (IACT) +- Configure zone objects (NPCs, items, doors) +- WebFun uses "a lisp-like language" for scripting + +## Asset Viewer/Editor +- Tile browser with flags visualization +- Character list with animation preview +- Sound browser +- Puzzle data viewer + +## Debug Menu +- Toggle debug overlays (collision, zone info, NPC paths) +- Script debugger for zone actions +- Code coverage for in-game scripts +- Zone teleportation +- Item spawning +- God mode / invincibility + +## Implementation Notes +- Could be a separate ImGui overlay or SDL-based UI +- Toggle with a debug key (F12 or similar) +- Save/load debug state between sessions diff --git a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs index 0c01091..8ae714f 100644 --- a/src/YodaStoriesNG.Engine/Bot/MissionBot.cs +++ b/src/YodaStoriesNG.Engine/Bot/MissionBot.cs @@ -274,12 +274,40 @@ public class MissionBot if (_actions.IsCompleted) { _actions.Reset(); + + // Check if we have a pending zone exit direction - if so, try to walk off the edge + if (_pendingZoneExitDirection.HasValue && _state.CurrentZone != null) + { + var dir = _pendingZoneExitDirection.Value; + // Use same threshold as MoveToZoneEdge (<=2 from edge) + bool atEdge = dir switch + { + Direction.Up => _state.PlayerY <= 2, + Direction.Down => _state.PlayerY >= _state.CurrentZone.Height - 3, + Direction.Left => _state.PlayerX <= 2, + Direction.Right => _state.PlayerX >= _state.CurrentZone.Width - 3, + _ => false + }; + + if (atEdge) + { + // Check if there's a connected zone in this direction + var connected = _worldGenerator.GetConnectedZone(_state.CurrentZoneId, dir); + Console.WriteLine($"[BOT] At edge, walking {dir}. Connected zone: {connected?.ToString() ?? "none"}"); + OnActionRequested?.Invoke(BotActionType.Move, 0, 0, dir); + _pendingZoneExitDirection = null; + return; // Stay in executing state for zone transition + } + } + + _pendingZoneExitDirection = null; _currentState = BotState.ThinkingAboutObjective; _explorationAttempts = 0; } else if (!_actions.IsBusy) { // Action finished without completing - go back to thinking + _pendingZoneExitDirection = null; _currentState = BotState.ThinkingAboutObjective; } } @@ -467,34 +495,69 @@ public class MissionBot TryChangeZone(); } + // Track the direction we're trying to exit + private Direction? _pendingZoneExitDirection; + private void MoveToZoneEdge(Direction dir) { if (_state.CurrentZone == null) return; int targetX = _state.PlayerX; int targetY = _state.PlayerY; + int edgeY = 0, edgeX = 0; switch (dir) { case Direction.Up: - targetY = 0; + targetY = 1; // Move to Y=1 first (Y=0 might be blocked) + edgeY = 0; + edgeX = _state.PlayerX; break; case Direction.Down: - targetY = _state.CurrentZone.Height - 1; + targetY = _state.CurrentZone.Height - 2; // Y=height-2 + edgeY = _state.CurrentZone.Height - 1; + edgeX = _state.PlayerX; break; case Direction.Left: - targetX = 0; + targetX = 1; // X=1 + edgeX = 0; + edgeY = _state.PlayerY; break; case Direction.Right: - targetX = _state.CurrentZone.Width - 1; + targetX = _state.CurrentZone.Width - 2; // X=width-2 + edgeX = _state.CurrentZone.Width - 1; + edgeY = _state.PlayerY; break; } - // Find nearest walkable position at edge - var nearest = _pathfinder.FindNearestWalkable(_state.CurrentZone, targetX, targetY, _state.ZoneNPCs); - if (nearest.HasValue) + // Store the direction for when we reach the edge + _pendingZoneExitDirection = dir; + + // Check if we're already near the edge + bool nearEdge = dir switch { - _actions.MoveTo(nearest.Value.X, nearest.Value.Y); + Direction.Up => _state.PlayerY <= 2, + Direction.Down => _state.PlayerY >= _state.CurrentZone.Height - 3, + Direction.Left => _state.PlayerX <= 2, + Direction.Right => _state.PlayerX >= _state.CurrentZone.Width - 3, + _ => false + }; + + if (nearEdge) + { + // Already near edge - just walk in that direction to exit + var connected = _worldGenerator.GetConnectedZone(_state.CurrentZoneId, dir); + Console.WriteLine($"[BOT] At edge, walking {dir} to exit zone {_state.CurrentZoneId}. Connected zone: {connected?.ToString() ?? "none"}"); + OnActionRequested?.Invoke(BotActionType.Move, 0, 0, dir); + } + else + { + // Move toward edge first + var nearest = _pathfinder.FindNearestWalkable(_state.CurrentZone, targetX, targetY, _state.ZoneNPCs); + if (nearest.HasValue) + { + _actions.MoveTo(nearest.Value.X, nearest.Value.Y); + } } } diff --git a/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs b/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs index 1495d22..315fba3 100644 --- a/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs +++ b/src/YodaStoriesNG.Engine/Bot/MissionSolver.cs @@ -161,6 +161,34 @@ public class MissionSolver }; } + // Check if we need to change zones to reach the target + if (currentStep.ZoneId.HasValue && currentStep.ZoneId.Value != _state.CurrentZoneId) + { + // Need to navigate to target zone + var dir = GetDirectionToAdjacentZone(currentStep.ZoneId.Value); + if (dir.HasValue) + { + return new BotObjective + { + Type = ObjectiveType.ChangeZone, + Description = $"Go to zone {currentStep.ZoneId.Value}", + TargetZoneId = currentStep.ZoneId.Value, + Direction = dir.Value + }; + } + else + { + // Target zone not adjacent - explore toward it + return new BotObjective + { + Type = ObjectiveType.Explore, + Description = $"Navigate toward zone {currentStep.ZoneId.Value}" + }; + } + } + + // We're in the target zone (or no specific zone required) + // Check if we have the required item for this step if (currentStep.RequiredItemId > 0) { @@ -171,16 +199,107 @@ public class MissionSolver } else { - // Need to find the item + // Need to find the item - check at specific location first + if (currentStep.TargetX > 0 || currentStep.TargetY > 0) + { + return new BotObjective + { + Type = ObjectiveType.PickupItem, + Description = $"Pick up item at ({currentStep.TargetX},{currentStep.TargetY})", + TargetX = currentStep.TargetX, + TargetY = currentStep.TargetY, + RequiredItemId = currentStep.RequiredItemId + }; + } return CreateFindItemObjective(currentStep.RequiredItemId); } } - // No required item - explore + // No specific required item - go to target location if specified + if (currentStep.TargetX > 0 || currentStep.TargetY > 0) + { + // Check for items at target location + var itemAtTarget = FindAnyItemInCurrentZone(); + if (itemAtTarget != null) + { + return new BotObjective + { + Type = ObjectiveType.PickupItem, + Description = $"Pick up item at ({itemAtTarget.X},{itemAtTarget.Y})", + TargetX = itemAtTarget.X, + TargetY = itemAtTarget.Y + }; + } + } + + // Priority 1: If we have ANY items, try to use them on friendly NPCs + if (_state.Inventory.Count > 0) + { + var friendlyNpc = FindNearestFriendlyNpc(); + if (friendlyNpc != null) + { + var itemToUse = _state.Inventory.FirstOrDefault(); + if (itemToUse > 0) + { + return new BotObjective + { + Type = ObjectiveType.UseItemOnNpc, + Description = $"Try using item on NPC", + TargetNpc = friendlyNpc, + TargetX = friendlyNpc.X, + TargetY = friendlyNpc.Y, + RequiredItemId = itemToUse + }; + } + } + } + + // Priority 2: Talk to any friendly NPC (might get items or advance quest) + var talkableNpc = FindNearestFriendlyNpc(); + if (talkableNpc != null) + { + return new BotObjective + { + Type = ObjectiveType.TalkToNpc, + Description = "Talk to NPC", + TargetNpc = talkableNpc, + TargetX = talkableNpc.X, + TargetY = talkableNpc.Y + }; + } + + // Priority 3: Pick up any items in the zone + var anyItem = FindAnyItemInCurrentZone(); + if (anyItem != null) + { + return new BotObjective + { + Type = ObjectiveType.PickupItem, + Description = $"Pick up item at ({anyItem.X},{anyItem.Y})", + TargetX = anyItem.X, + TargetY = anyItem.Y + }; + } + + // Priority 4: Kill any enemies blocking progress + var enemy = FindNearestEnemy(); + if (enemy != null) + { + return new BotObjective + { + Type = ObjectiveType.KillEnemy, + Description = "Defeat enemy", + TargetNpc = enemy, + TargetX = enemy.X, + TargetY = enemy.Y + }; + } + + // Priority 5: Explore to find more stuff return new BotObjective { Type = ObjectiveType.Explore, - Description = "Explore to find next objective" + Description = "Explore to find objectives" }; } @@ -297,11 +416,13 @@ public class MissionSolver /// /// Finds any collectable item in the current zone. + /// Checks both zone objects (crates) and items placed in the tile grid. /// private ZoneObject? FindAnyItemInCurrentZone() { if (_state.CurrentZone == null) return null; + // First check zone objects (crates, locators) foreach (var obj in _state.CurrentZone.Objects) { if ((obj.Type == ZoneObjectType.CrateItem || @@ -312,6 +433,39 @@ public class MissionSolver return obj; } } + + // Also scan the tile grid for items (e.g., mushrooms on rocks) + var zone = _state.CurrentZone; + if (zone.TileGrid != null) + { + for (int y = 0; y < zone.Height; y++) + { + for (int x = 0; x < zone.Width; x++) + { + // Check all layers for item tiles + for (int layer = 0; layer < 3; layer++) + { + var tileId = zone.TileGrid[y, x, layer]; + if (tileId != 0xFFFF && tileId < _gameData.Tiles.Count) + { + var tile = _gameData.Tiles[tileId]; + if (tile.IsItem && !_state.IsObjectCollected(_state.CurrentZoneId, x, y)) + { + // Found an item tile - return as a synthetic object + return new ZoneObject + { + Type = ZoneObjectType.LocatorItem, + X = x, + Y = y, + Argument = tileId + }; + } + } + } + } + } + } + return null; } diff --git a/src/YodaStoriesNG.Engine/Game/GameEngine.cs b/src/YodaStoriesNG.Engine/Game/GameEngine.cs index a4d5042..65c7a20 100644 --- a/src/YodaStoriesNG.Engine/Game/GameEngine.cs +++ b/src/YodaStoriesNG.Engine/Game/GameEngine.cs @@ -232,12 +232,12 @@ public unsafe class GameEngine : IDisposable switch (type) { case BotActionType.Move: - // Calculate dx/dy from current position to target - int dx = Math.Sign(x - _state.PlayerX); - int dy = Math.Sign(y - _state.PlayerY); - if (dx == 0 && dy == 0) + int dx, dy; + // When x=0 and y=0, use direction for movement (bot convention for directional moves) + if (x == 0 && y == 0) { - // Use direction to determine movement + dx = 0; + dy = 0; switch (dir) { case Direction.Up: dy = -1; break; @@ -246,6 +246,12 @@ public unsafe class GameEngine : IDisposable case Direction.Right: dx = 1; break; } } + else + { + // Calculate dx/dy from current position to target + dx = Math.Sign(x - _state.PlayerX); + dy = Math.Sign(y - _state.PlayerY); + } TryMovePlayer(dx, dy, dir, false); break; diff --git a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs index 3272e7c..d6a2dcd 100644 --- a/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs +++ b/src/YodaStoriesNG.Engine/Game/WorldGenerator.cs @@ -281,7 +281,7 @@ public class WorldGenerator /// public WorldMap GenerateWorld() { - // Pick a random mission (goal puzzle) + // Pick a random mission (goal puzzle) - but don't build puzzle chain yet CurrentMission = SelectRandomMission(); Console.WriteLine($"Selected mission: {CurrentMission.Name} on {CurrentMission.Planet}"); @@ -298,12 +298,41 @@ public class WorldGenerator // Generate the main planet grid GeneratePlanetGrid(); + // NOW build the puzzle chain - after grid is generated so we know which zones exist + if (CurrentMission.GoalPuzzle != null) + { + BuildPuzzleChain(CurrentMission, CurrentMission.GoalPuzzle); + LogMissionDetails(CurrentMission); + } + // Set up item chain SetupItemChain(); return CurrentWorld; } + /// + /// Logs mission details to console. + /// + private void LogMissionDetails(Mission mission) + { + Console.WriteLine($"\n=== MISSION: {mission.Name} ==="); + Console.WriteLine($"Planet: {mission.Planet}"); + Console.WriteLine($"Goal: {mission.Description}"); + Console.WriteLine($"Puzzle chain ({mission.PuzzleChain.Count} steps):"); + for (int i = 0; i < mission.PuzzleChain.Count; i++) + { + var step = mission.PuzzleChain[i]; + var reqItem = GetItemName(step.RequiredItemId); + var rewItem = GetItemName(step.RewardItemId); + var zoneInfo = step.ZoneId.HasValue ? $" [Zone {step.ZoneId}]" : ""; + Console.WriteLine($" {i + 1}. [{step.Puzzle.Type}] Need: {reqItem} -> Get: {rewItem}{zoneInfo}"); + if (!string.IsNullOrEmpty(step.Hint)) + Console.WriteLine($" Hint: {step.Hint}"); + } + Console.WriteLine(); + } + /// /// Selects a random mission from available goal puzzles and builds the puzzle chain. /// @@ -335,24 +364,7 @@ public class WorldGenerator GoalPuzzle = goalPuzzle }; - // Build the puzzle chain leading to the goal - BuildPuzzleChain(mission, goalPuzzle); - - // Log mission details - Console.WriteLine($"\n=== MISSION: {mission.Name} ==="); - Console.WriteLine($"Planet: {mission.Planet}"); - Console.WriteLine($"Goal: {mission.Description}"); - Console.WriteLine($"Puzzle chain ({mission.PuzzleChain.Count} steps):"); - for (int i = 0; i < mission.PuzzleChain.Count; i++) - { - var step = mission.PuzzleChain[i]; - var reqItem = GetItemName(step.RequiredItemId); - var rewItem = GetItemName(step.RewardItemId); - Console.WriteLine($" {i + 1}. [{step.Puzzle.Type}] Need: {reqItem} -> Get: {rewItem}"); - if (!string.IsNullOrEmpty(step.Hint)) - Console.WriteLine($" Hint: {step.Hint}"); - } - Console.WriteLine(); + // NOTE: Puzzle chain will be built later in GenerateWorld() after grid is generated return mission; } @@ -407,80 +419,136 @@ public class WorldGenerator } /// - /// Builds the puzzle chain for a mission based on the goal puzzle. + /// Builds the puzzle chain for a mission based on zone objects. + /// Since puzzle data parsing is unreliable, we scan zones for items and NPCs. + /// Each step includes the specific zone where the item/NPC can be found. + /// Only uses zones that are in the generated world grid. /// private void BuildPuzzleChain(Mission mission, Puzzle goalPuzzle) { - // Get all non-goal puzzles that can be used in the chain - var tradePuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Trade).ToList(); - var usePuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Use).ToList(); - var questPuzzles = _gameData.Puzzles.Where(p => p.Type == PuzzleType.Quest).ToList(); - - // The goal puzzle defines what item is needed to complete the mission - var goalItemId = goalPuzzle.Item1; - - // Work backwards from the goal to build the chain - var currentNeededItem = goalItemId; - var usedPuzzles = new HashSet(); var chainSteps = new List(); - // Add the final goal step - chainSteps.Add(new PuzzleStep + // Get the set of zone IDs that are actually in our generated world + var worldZoneIds = new HashSet(); + if (CurrentWorld?.Grid != null) { - Puzzle = goalPuzzle, - RequiredItemId = goalItemId, - RewardItemId = 0, // Mission complete! - Hint = goalPuzzle.Strings.Count > 1 ? goalPuzzle.Strings[1] : "Complete the mission." - }); - - // Try to find puzzles that give us what we need (working backwards) - int maxSteps = 5; // Limit chain length - for (int i = 0; i < maxSteps && currentNeededItem > 0; i++) - { - // Look for a puzzle that rewards the item we need - Puzzle? sourcePuzzle = null; - - // First try trade puzzles (item2 is reward) - sourcePuzzle = tradePuzzles - .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id)) - .OrderBy(_ => _random.Next()) - .FirstOrDefault(); - - // Then try use puzzles - if (sourcePuzzle == null) + for (int y = 0; y < GridSize; y++) { - sourcePuzzle = usePuzzles - .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id)) - .OrderBy(_ => _random.Next()) - .FirstOrDefault(); - } - - // Then try quest puzzles - if (sourcePuzzle == null) - { - sourcePuzzle = questPuzzles - .Where(p => p.Item2 == currentNeededItem && !usedPuzzles.Contains(p.Id)) - .OrderBy(_ => _random.Next()) - .FirstOrDefault(); - } - - if (sourcePuzzle != null) - { - usedPuzzles.Add(sourcePuzzle.Id); - chainSteps.Insert(0, new PuzzleStep + for (int x = 0; x < GridSize; x++) { - Puzzle = sourcePuzzle, - RequiredItemId = sourcePuzzle.Item1, - RewardItemId = sourcePuzzle.Item2, - Hint = sourcePuzzle.Strings.FirstOrDefault() ?? "" - }); - currentNeededItem = sourcePuzzle.Item1; + if (CurrentWorld.Grid[y, x].HasValue) + worldZoneIds.Add(CurrentWorld.Grid[y, x]!.Value); + } } - else + } + + // Find item-bearing objects ONLY in zones that are in the world grid + var planetZones = _gameData.Zones + .Where(z => z.Planet == mission.Planet && z.Width > 0 && worldZoneIds.Contains(z.Id)) + .ToList(); + + Console.WriteLine($"BuildPuzzleChain: {worldZoneIds.Count} zones in world, {planetZones.Count} planet zones to scan"); + + // Collect items with their zone locations + var itemLocations = new List<(int ItemId, int ZoneId, int X, int Y)>(); + var npcZones = new List<(int ZoneId, int CharacterId, int X, int Y)>(); + + foreach (var zone in planetZones) + { + foreach (var obj in zone.Objects) { - // No more puzzles in the chain, this is where Yoda gives the starting item - break; + if (obj.Type == ZoneObjectType.CrateItem || obj.Type == ZoneObjectType.LocatorItem) + { + if (obj.Argument > 0 && obj.Argument < _gameData.Tiles.Count) + { + itemLocations.Add((obj.Argument, zone.Id, obj.X, obj.Y)); + } + } + else if (obj.Type == ZoneObjectType.PuzzleNPC) + { + npcZones.Add((zone.Id, obj.Argument, obj.X, obj.Y)); + } } + + // Also check IZAX entity data for NPCs with items + if (zone.AuxData?.Entities != null) + { + foreach (var entity in zone.AuxData.Entities) + { + if (entity.ItemTileId > 0 && entity.ItemTileId != 0xFFFF) + { + itemLocations.Add((entity.ItemTileId, zone.Id, entity.X, entity.Y)); + } + if (entity.CharacterId > 0 && entity.CharacterId != 0xFFFF) + { + npcZones.Add((zone.Id, entity.CharacterId, entity.X, entity.Y)); + } + } + } + } + + // Shuffle and take a subset + itemLocations = itemLocations.OrderBy(_ => _random.Next()).Take(5).ToList(); + npcZones = npcZones.OrderBy(_ => _random.Next()).ToList(); + + // Build a puzzle chain with specific zone locations + if (itemLocations.Count >= 2) + { + // Step 1: Find first item at a specific location + var firstItem = itemLocations[0]; + chainSteps.Add(new PuzzleStep + { + Puzzle = new Puzzle { Type = PuzzleType.Quest, Strings = { "Find an item" } }, + RequiredItemId = 0, // No item required - just find it + RewardItemId = firstItem.ItemId, + ZoneId = firstItem.ZoneId, + TargetX = firstItem.X, + TargetY = firstItem.Y, + Hint = $"Search zone {firstItem.ZoneId} for useful items." + }); + + // Steps 2-N: Trade items with NPCs at specific zones + for (int i = 0; i < itemLocations.Count - 1; i++) + { + var npcZone = npcZones.Count > i ? npcZones[i] : (ZoneId: itemLocations[i + 1].ZoneId, CharacterId: 0, X: 0, Y: 0); + var nextItem = itemLocations[i + 1]; + + chainSteps.Add(new PuzzleStep + { + Puzzle = new Puzzle { Type = PuzzleType.Trade, Strings = { "Trade with someone" } }, + RequiredItemId = itemLocations[i].ItemId, + RewardItemId = nextItem.ItemId, + ZoneId = npcZone.ZoneId, + TargetX = npcZone.X, + TargetY = npcZone.Y, + Hint = $"Find someone in zone {npcZone.ZoneId} who needs this item." + }); + } + + // Final step: Complete goal + var goalZone = npcZones.Count > 0 ? npcZones[^1] : (ZoneId: itemLocations[^1].ZoneId, CharacterId: 0, X: 0, Y: 0); + chainSteps.Add(new PuzzleStep + { + Puzzle = goalPuzzle, + RequiredItemId = itemLocations[^1].ItemId, + RewardItemId = 0, // Mission complete! + ZoneId = goalZone.ZoneId, + TargetX = goalZone.X, + TargetY = goalZone.Y, + Hint = goalPuzzle.Strings.Count > 1 ? goalPuzzle.Strings[1] : "Complete your mission." + }); + } + else + { + // Fallback: simple exploration mission + chainSteps.Add(new PuzzleStep + { + Puzzle = goalPuzzle, + RequiredItemId = 0, + RewardItemId = 0, + ZoneId = CurrentWorld?.LandingZoneId, + Hint = "Explore the planet and find what you need." + }); } mission.PuzzleChain = chainSteps; @@ -1023,6 +1091,8 @@ public class PuzzleStep public int RequiredItemId { get; set; } // Item needed to complete this step public int RewardItemId { get; set; } // Item received upon completion public int? ZoneId { get; set; } // Zone where this puzzle is solved + public int TargetX { get; set; } // X position in the zone + public int TargetY { get; set; } // Y position in the zone public string Hint { get; set; } = ""; // Hint text for the player public bool IsCompleted { get; set; } = false; } diff --git a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs index b6d6d9e..5084965 100644 --- a/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs +++ b/src/YodaStoriesNG.Engine/Parsing/DtaParser.cs @@ -696,28 +696,42 @@ public class DtaParser private void ParsePuzzlesSection(uint length) { - var endPos = _reader.BaseStream.Position + length; + var startPos = _reader.BaseStream.Position; + var endPos = startPos + length; int puzzleId = 0; - while (_reader.BaseStream.Position < endPos - 4) + // First, read the puzzle count (2 bytes) + var puzzleCount = _reader.ReadUInt16(); + Console.WriteLine($"PUZ2 section: expecting {puzzleCount} puzzles"); + + while (_reader.BaseStream.Position < endPos - 4 && puzzleId < puzzleCount) { + // Look for IPUZ marker (similar to IZON in zones) + var markerPos = _reader.BaseStream.Position; + var markerBytes = _reader.ReadBytes(4); + var marker = System.Text.Encoding.ASCII.GetString(markerBytes); + + if (marker != "IPUZ") + { + // Not at IPUZ marker, try to find it + _reader.BaseStream.Seek(markerPos + 1, SeekOrigin.Begin); + continue; + } + + // Found IPUZ marker - read puzzle size + var puzzleSize = _reader.ReadUInt32(); + var puzzleDataStart = _reader.BaseStream.Position; + var puzzle = new Puzzle { Id = puzzleId++ }; - // Read puzzle header (variable format, read cautiously) - var marker = _reader.ReadUInt32(); - if (marker == 0xFFFFFFFF || _reader.BaseStream.Position >= endPos) - break; - - _reader.BaseStream.Seek(-4, SeekOrigin.Current); - - // PUZ2 format: + // IPUZ format (after size): // - 2 bytes: puzzle type (0=Quest, 1=Transport, 2=Trade, 3=Use, 4=Goal) - // - 2 bytes: item1 (required item or NPC) - // - 2 bytes: item2 (reward item or destination) - // - 2 bytes: unknown (possibly flags or zone reference) - // - 2 bytes: unknown (possibly planet or difficulty) + // - 2 bytes: item1 (item to bring/find) + // - 2 bytes: item2 (reward item or NPC tile) + // - 2 bytes: unknown flags + // - 2 bytes: unknown (possibly planet) // - 5 strings (length-prefixed) - var puzzleType = _reader.ReadInt16(); + var puzzleType = _reader.ReadUInt16(); puzzle.Type = (PuzzleType)puzzleType; puzzle.Item1 = _reader.ReadUInt16(); puzzle.Item2 = _reader.ReadUInt16(); @@ -725,25 +739,33 @@ public class DtaParser var unknown3 = _reader.ReadUInt16(); // Read puzzle strings (5 strings typically) - // String 0: Puzzle name/description - // String 1: Hint or goal text - // String 2-4: Additional dialogue/text + // String 0: Puzzle name/description (e.g., "Give MUSHROOM to YODA") + // String 1: Hint text + // String 2-4: Additional dialogue for (int i = 0; i < 5; i++) { + if (_reader.BaseStream.Position >= puzzleDataStart + puzzleSize) + break; + var strLen = _reader.ReadUInt16(); - if (strLen > 0 && strLen < 1000) // Sanity check + if (strLen > 0 && strLen < 1000) { var strBytes = _reader.ReadBytes(strLen); puzzle.Strings.Add(System.Text.Encoding.ASCII.GetString(strBytes).TrimEnd('\0')); } - else if (strLen >= 1000) + else if (strLen == 0) { - // Invalid length, likely parsing error - _reader.BaseStream.Seek(-2, SeekOrigin.Current); + puzzle.Strings.Add(""); + } + else + { + // Invalid length, skip rest break; } } + // Skip to end of this puzzle's data + _reader.BaseStream.Seek(puzzleDataStart + puzzleSize, SeekOrigin.Begin); _data.Puzzles.Add(puzzle); }