Fix bot zone transitions and puzzle parsing

- Fix HandleBotAction to use direction when coords are (0,0)
- Fix edge detection mismatch in MissionBot (use consistent <=2 threshold)
- Update puzzle parser to look for IPUZ markers inside PUZ2 section
- Add debug logging for zone connections
- Improve puzzle chain building with zone filtering
- Add FUTURE_FEATURES.md for WebFun-style development tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ned Heller
2026-01-29 16:25:33 -08:00
co-authored by Claude Opus 4.5
parent 3d3cb2920a
commit 1d10d368a3
6 changed files with 468 additions and 120 deletions
+33
View File
@@ -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
+71 -8
View File
@@ -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);
}
}
}
+157 -3
View File
@@ -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
/// <summary>
/// Finds any collectable item in the current zone.
/// Checks both zone objects (crates) and items placed in the tile grid.
/// </summary>
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;
}
+11 -5
View File
@@ -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;
+152 -82
View File
@@ -281,7 +281,7 @@ public class WorldGenerator
/// </summary>
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;
}
/// <summary>
/// Logs mission details to console.
/// </summary>
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();
}
/// <summary>
/// Selects a random mission from available goal puzzles and builds the puzzle chain.
/// </summary>
@@ -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
}
/// <summary>
/// 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.
/// </summary>
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<int>();
var chainSteps = new List<PuzzleStep>();
// 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<int>();
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;
}
+44 -22
View File
@@ -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);
}