finished quest pathing
This commit is contained in:
parent
568df4c7fe
commit
419e2eb4a4
17 changed files with 637 additions and 31 deletions
|
|
@ -110,6 +110,7 @@ public partial class MemoryViewModel : ObservableObject
|
|||
private MemoryNodeViewModel? _areaHasWaypoint;
|
||||
private MemoryNodeViewModel? _areaMonsterLevel;
|
||||
private MemoryNodeViewModel? _worldAreaId;
|
||||
private MemoryNodeViewModel? _connectedAreas;
|
||||
private MemoryNodeViewModel? _uiElementsNode;
|
||||
private MemoryNodeViewModel? _questLinkedListNode;
|
||||
|
||||
|
|
@ -247,6 +248,7 @@ public partial class MemoryViewModel : ObservableObject
|
|||
_areaHasWaypoint = new MemoryNodeViewModel("HasWaypoint:");
|
||||
_areaMonsterLevel = new MemoryNodeViewModel("MonsterLevel:");
|
||||
_worldAreaId = new MemoryNodeViewModel("WorldAreaId:");
|
||||
_connectedAreas = new MemoryNodeViewModel("Connected:");
|
||||
areaTemplateGroup.Children.Add(_areaRawName);
|
||||
areaTemplateGroup.Children.Add(_areaDisplayName);
|
||||
areaTemplateGroup.Children.Add(_areaAct);
|
||||
|
|
@ -254,6 +256,7 @@ public partial class MemoryViewModel : ObservableObject
|
|||
areaTemplateGroup.Children.Add(_areaHasWaypoint);
|
||||
areaTemplateGroup.Children.Add(_areaMonsterLevel);
|
||||
areaTemplateGroup.Children.Add(_worldAreaId);
|
||||
areaTemplateGroup.Children.Add(_connectedAreas);
|
||||
|
||||
// Quest Linked Lists (all quests + tracked merged from GameUi)
|
||||
_questLinkedListNode = new MemoryNodeViewModel("Quests");
|
||||
|
|
@ -378,6 +381,28 @@ public partial class MemoryViewModel : ObservableObject
|
|||
_areaMonsterLevel!.Set(snap.AreaMonsterLevel > 0 ? snap.AreaMonsterLevel.ToString() : "—", snap.AreaMonsterLevel > 0);
|
||||
_worldAreaId!.Set(snap.WorldAreaId > 0 ? snap.WorldAreaId.ToString() : "—", snap.WorldAreaId > 0);
|
||||
|
||||
// Connected areas
|
||||
if (snap.ConnectedAreas is { Count: > 0 })
|
||||
{
|
||||
_connectedAreas!.Set($"{snap.ConnectedAreas.Count} areas", true);
|
||||
_connectedAreas.ValueColor = "#3fb950";
|
||||
_connectedAreas.Children.Clear();
|
||||
foreach (var conn in snap.ConnectedAreas)
|
||||
{
|
||||
var label = conn.Name ?? conn.Id ?? "?";
|
||||
var detail = $"Act {conn.Act}, Lv{conn.MonsterLevel}";
|
||||
if (conn.IsTown) detail += " [Town]";
|
||||
if (conn.HasWaypoint) detail += " [WP]";
|
||||
var node = new MemoryNodeViewModel(label) { Value = detail, ValueColor = "#8b949e" };
|
||||
_connectedAreas.Children.Add(node);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_connectedAreas!.Set("—", false);
|
||||
_connectedAreas.Children.Clear();
|
||||
}
|
||||
|
||||
// Player position
|
||||
if (snap.HasPosition)
|
||||
_playerPos!.Set($"({snap.PlayerX:F1}, {snap.PlayerY:F1}, {snap.PlayerZ:F1})");
|
||||
|
|
@ -530,7 +555,10 @@ public partial class MemoryViewModel : ObservableObject
|
|||
var prefix = q.IsTracked ? "[T] " : "";
|
||||
var stateLabel = q.StateText ?? $"step {q.StateId}";
|
||||
var label = $"{prefix}{q.DisplayName ?? q.InternalId ?? $"[{i}]"}";
|
||||
var value = $"Act{q.Act} {stateLabel}";
|
||||
var targetSuffix = q.TargetAreas is { Count: > 0 }
|
||||
? $" → {string.Join(", ", q.TargetAreas.Select(a => a.Name ?? a.Id ?? "?"))}"
|
||||
: "";
|
||||
var value = $"Act{q.Act} {stateLabel}{targetSuffix}";
|
||||
|
||||
var color = q.IsTracked ? "#58a6ff" : "#d29922";
|
||||
|
||||
|
|
@ -561,6 +589,15 @@ public partial class MemoryViewModel : ObservableObject
|
|||
};
|
||||
if (q.StateText is not null)
|
||||
details.Add(("StateText:", q.StateText));
|
||||
if (q.MapPinsText is not null)
|
||||
details.Add(("MapPinsText:", q.MapPinsText));
|
||||
if (q.TargetAreas is { Count: > 0 })
|
||||
{
|
||||
foreach (var area in q.TargetAreas)
|
||||
details.Add(("TargetArea:", $"{area.Name ?? area.Id} (Act{area.Act}, Lv{area.MonsterLevel}{(area.IsTown ? ", Town" : "")}{(area.HasWaypoint ? ", WP" : "")})"));
|
||||
}
|
||||
if (q.PathToTarget is { Count: > 1 })
|
||||
details.Add(("Path:", string.Join(" → ", q.PathToTarget)));
|
||||
if (q.QuestDatPtr != 0)
|
||||
details.Add(("QuestDatPtr:", $"0x{q.QuestDatPtr:X}"));
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public sealed class DatFile<TRow> where TRow : class
|
|||
var tableInfo = scanner.FindDatFile(_fileName);
|
||||
if (tableInfo is null)
|
||||
{
|
||||
Log.Debug("DatFile<{Type}>: {File} not found in FileRoot", typeof(TRow).Name, _fileName);
|
||||
Log.Warning("DatFile<{Type}>: {File} not found in FileRoot", typeof(TRow).Name, _fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,16 +43,17 @@ public sealed class FileRootScanner
|
|||
|
||||
if (!_fileCache.TryGetValue(filename, out var fileObjAddr))
|
||||
{
|
||||
// Try case-insensitive match
|
||||
foreach (var (key, value) in _fileCache)
|
||||
{
|
||||
if (string.Equals(key, filename, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
fileObjAddr = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fileObjAddr == 0) return null;
|
||||
// Log near-matches to help find the correct path
|
||||
var searchTerm = Path.GetFileNameWithoutExtension(filename);
|
||||
var nearMatches = _fileCache.Keys
|
||||
.Where(k => k.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(5)
|
||||
.ToList();
|
||||
if (nearMatches.Count > 0)
|
||||
Log.Information("FileRootScanner: '{File}' not found, near matches: {Matches}",
|
||||
filename, string.Join(", ", nearMatches));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReadDatHeader(fileObjAddr);
|
||||
|
|
@ -489,8 +490,11 @@ public sealed class FileRootScanner
|
|||
var firstPtr = (nint)BitConverter.ToInt64(data, 0);
|
||||
if (firstPtr == 0 || !_ctx.IsDatPtr(firstPtr)) return null;
|
||||
|
||||
// Find the smallest row size where ≥80% of sampled rows start with a valid .dat pointer.
|
||||
// Return the first one that passes — it's the true row size, not a multiple.
|
||||
// Find the smallest row size where rows start with valid .dat pointers.
|
||||
// Require BOTH:
|
||||
// - At least 40% of samples are valid non-zero pointers (positive evidence)
|
||||
// - At most 20% are non-zero non-pointer garbage (negative evidence)
|
||||
// Zeros are neutral — could be null FKs in real rows or padding in wrong-size rows.
|
||||
var bestRowSize = 0;
|
||||
foreach (var rowSize in candidates)
|
||||
{
|
||||
|
|
@ -498,6 +502,7 @@ public sealed class FileRootScanner
|
|||
if (rowCount < 2) continue;
|
||||
|
||||
var validPtrs = 0;
|
||||
var garbagePtrs = 0;
|
||||
var sampleCount = Math.Min(rowCount, 50);
|
||||
|
||||
for (var r = 0; r < sampleCount; r++)
|
||||
|
|
@ -506,11 +511,15 @@ public sealed class FileRootScanner
|
|||
if (off + 8 > data.Length) break;
|
||||
|
||||
var ptr = (nint)BitConverter.ToInt64(data, off);
|
||||
if (ptr != 0 && _ctx.IsDatPtr(ptr))
|
||||
if (ptr == 0)
|
||||
continue; // neutral — don't count for or against
|
||||
else if (_ctx.IsDatPtr(ptr))
|
||||
validPtrs++;
|
||||
else
|
||||
garbagePtrs++;
|
||||
}
|
||||
|
||||
if (validPtrs >= sampleCount * 80 / 100)
|
||||
if (validPtrs >= sampleCount * 40 / 100 && garbagePtrs <= sampleCount * 20 / 100)
|
||||
{
|
||||
bestRowSize = rowSize;
|
||||
break; // smallest passing candidate = true row size
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -13,6 +15,9 @@ public sealed class FilesContainer
|
|||
public FileRootScanner Scanner { get; }
|
||||
|
||||
private DatFile<QuestStateDatRow>? _questStates;
|
||||
private DatFile<WorldAreaDatRow>? _worldAreas;
|
||||
private Dictionary<nint, List<nint>>? _connectionGraph;
|
||||
private bool _graphBuilt;
|
||||
|
||||
public FilesContainer(MemoryContext ctx, MsvcStringReader strings)
|
||||
{
|
||||
|
|
@ -35,6 +40,205 @@ public sealed class FilesContainer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>WorldAreas.dat — lazy-loaded on first access.</summary>
|
||||
public DatFile<WorldAreaDatRow> WorldAreas
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_worldAreas is null)
|
||||
{
|
||||
_worldAreas = new DatFile<WorldAreaDatRow>("Data/Balance/WorldAreas.dat", new WorldAreaDatRowParser());
|
||||
_worldAreas.Load(Scanner, _ctx, _strings);
|
||||
}
|
||||
return _worldAreas;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a WorldArea's connected areas by following the ConnectionArrayPtr via RPM.
|
||||
/// Returns empty list if connections can't be read.
|
||||
/// </summary>
|
||||
public List<WorldAreaDatRow> GetConnections(WorldAreaDatRow area)
|
||||
{
|
||||
var result = new List<WorldAreaDatRow>();
|
||||
if (area.ConnectionCount <= 0 || area.ConnectionArrayPtr == 0) return result;
|
||||
|
||||
var arrayData = _ctx.Memory.ReadBytes(area.ConnectionArrayPtr, area.ConnectionCount * 8);
|
||||
if (arrayData is null) return result;
|
||||
|
||||
for (var i = 0; i < area.ConnectionCount; i++)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(arrayData, i * 8);
|
||||
var connected = WorldAreas.GetByAddress(ptr);
|
||||
if (connected is not null)
|
||||
result.Add(connected);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a MapPins row directly via RPM (MapPins.dat is not in FileRoot).
|
||||
/// Extracts WorldArea FK at +0x08 and resolves it to a WorldAreaDatRow.
|
||||
/// </summary>
|
||||
public WorldAreaDatRow? ReadMapPinWorldArea(nint mapPinPtr)
|
||||
{
|
||||
if (mapPinPtr == 0 || !_ctx.IsDatPtr(mapPinPtr)) return null;
|
||||
|
||||
// MapPins row: +0x08 = WorldArea FK (16 bytes: ptr(8) + meta(8))
|
||||
var data = _ctx.Memory.ReadBytes(mapPinPtr + 0x08, 8);
|
||||
if (data is null) return null;
|
||||
|
||||
var worldAreaPtr = (nint)BitConverter.ToInt64(data, 0);
|
||||
return WorldAreas.GetByAddress(worldAreaPtr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a QuestState's MapPins to WorldAreas via RPM.
|
||||
/// Follows MapPinsKeys array and single MapPinPtr, reading each MapPins row's WorldArea FK.
|
||||
/// </summary>
|
||||
public List<WorldAreaDatRow> GetMapPinWorldAreas(QuestStateDatRow questState)
|
||||
{
|
||||
var result = new List<WorldAreaDatRow>();
|
||||
var seen = new HashSet<nint>();
|
||||
|
||||
// Single MapPinPtr at +0x61
|
||||
if (questState.MapPinPtr != 0)
|
||||
{
|
||||
var wa = ReadMapPinWorldArea(questState.MapPinPtr);
|
||||
if (wa is not null && seen.Add(wa.RowAddress))
|
||||
result.Add(wa);
|
||||
}
|
||||
|
||||
// MapPinsKeys array at +0x45
|
||||
if (questState.MapPinsCount > 0 && questState.MapPinsArrayPtr != 0)
|
||||
{
|
||||
// Each element is a 16-byte TableReference: pointer(8) + metadata(8)
|
||||
var arrayData = _ctx.Memory.ReadBytes(questState.MapPinsArrayPtr, questState.MapPinsCount * 16);
|
||||
if (arrayData is not null)
|
||||
{
|
||||
for (var i = 0; i < questState.MapPinsCount; i++)
|
||||
{
|
||||
var pinPtr = (nint)BitConverter.ToInt64(arrayData, i * 16);
|
||||
var wa = ReadMapPinWorldArea(pinPtr);
|
||||
if (wa is not null && seen.Add(wa.RowAddress))
|
||||
result.Add(wa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the full area connection graph from WorldAreas.dat.
|
||||
/// One RPM per area to read its connection array. Called once, cached.
|
||||
/// </summary>
|
||||
public void EnsureConnectionGraph()
|
||||
{
|
||||
if (_graphBuilt) return;
|
||||
_graphBuilt = true;
|
||||
|
||||
if (!WorldAreas.IsLoaded) return;
|
||||
|
||||
var graph = new Dictionary<nint, List<nint>>(WorldAreas.Count);
|
||||
var resolvedEdges = 0;
|
||||
|
||||
foreach (var area in WorldAreas.Rows)
|
||||
{
|
||||
var neighbors = new List<nint>();
|
||||
graph[area.RowAddress] = neighbors;
|
||||
|
||||
if (area.ConnectionCount <= 0 || area.ConnectionArrayPtr == 0)
|
||||
continue;
|
||||
|
||||
var arrayData = _ctx.Memory.ReadBytes(area.ConnectionArrayPtr, area.ConnectionCount * 8);
|
||||
if (arrayData is null) continue;
|
||||
|
||||
for (var i = 0; i < area.ConnectionCount; i++)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(arrayData, i * 8);
|
||||
if (ptr != 0 && WorldAreas.GetByAddress(ptr) is not null)
|
||||
{
|
||||
neighbors.Add(ptr);
|
||||
resolvedEdges++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_connectionGraph = graph;
|
||||
Log.Information("FilesContainer: built connection graph — {Areas} areas, {Edges} edges",
|
||||
graph.Count, resolvedEdges);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BFS pathfinding from one WorldArea to another using the pre-built connection graph.
|
||||
/// Returns the path as a list of WorldAreaDatRows (including start and end), or null if unreachable.
|
||||
/// </summary>
|
||||
public List<WorldAreaDatRow>? FindPath(WorldAreaDatRow from, WorldAreaDatRow to)
|
||||
{
|
||||
if (from.RowAddress == to.RowAddress)
|
||||
return [from];
|
||||
|
||||
EnsureConnectionGraph();
|
||||
if (_connectionGraph is null) return null;
|
||||
|
||||
var visited = new HashSet<nint> { from.RowAddress };
|
||||
var queue = new Queue<nint>();
|
||||
var parent = new Dictionary<nint, nint>();
|
||||
|
||||
queue.Enqueue(from.RowAddress);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
|
||||
if (!_connectionGraph.TryGetValue(current, out var neighbors))
|
||||
continue;
|
||||
|
||||
foreach (var neighborAddr in neighbors)
|
||||
{
|
||||
if (!visited.Add(neighborAddr)) continue;
|
||||
|
||||
parent[neighborAddr] = current;
|
||||
|
||||
if (neighborAddr == to.RowAddress)
|
||||
{
|
||||
// Reconstruct path
|
||||
var path = new List<WorldAreaDatRow>();
|
||||
var walk = neighborAddr;
|
||||
while (walk != from.RowAddress)
|
||||
{
|
||||
path.Add(WorldAreas.GetByAddress(walk)!);
|
||||
walk = parent[walk];
|
||||
}
|
||||
path.Add(from);
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
queue.Enqueue(neighborAddr);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // unreachable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a WorldAreaDatRow by its internal ID (e.g. "G1_1_town").
|
||||
/// Linear scan — use sparingly.
|
||||
/// </summary>
|
||||
public WorldAreaDatRow? FindAreaById(string id)
|
||||
{
|
||||
if (!WorldAreas.IsLoaded) return null;
|
||||
foreach (var row in WorldAreas.Rows)
|
||||
{
|
||||
if (string.Equals(row.Id, id, StringComparison.OrdinalIgnoreCase))
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached data. Call after re-attaching to a process.
|
||||
/// </summary>
|
||||
|
|
@ -43,5 +247,9 @@ public sealed class FilesContainer
|
|||
Scanner.InvalidateCache();
|
||||
_questStates?.InvalidateCache();
|
||||
_questStates = null;
|
||||
_worldAreas?.InvalidateCache();
|
||||
_worldAreas = null;
|
||||
_connectionGraph = null;
|
||||
_graphBuilt = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
src/Roboto.Memory/Files/MapPinRow.cs
Normal file
64
src/Roboto.Memory/Files/MapPinRow.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed row from MapPins.dat.
|
||||
/// Schema (from poe-tool-dev/dat-schema poe2/MapPins):
|
||||
/// +0x00(8): Id string, +0x08(16): WorldArea FK, +0x18(16): WorldAreasKeys[],
|
||||
/// +0x28(8): Name string, +0x30(8): FlavourText, +0x38(16): QuestFlags1[],
|
||||
/// +0x48(4): Act i32, ...
|
||||
/// </summary>
|
||||
public sealed record MapPinDatRow
|
||||
{
|
||||
public nint RowAddress { get; init; }
|
||||
public string? Id { get; init; }
|
||||
/// <summary>FK pointer to WorldAreas.dat row — the primary world area for this pin.</summary>
|
||||
public nint WorldAreaPtr { get; init; }
|
||||
public string? Name { get; init; }
|
||||
public int Act { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses MapPins.dat rows. Offsets estimated from gql schema field order + type sizes.
|
||||
/// </summary>
|
||||
public sealed class MapPinDatRowParser : IDatRowParser<MapPinDatRow>
|
||||
{
|
||||
public MapPinDatRow? Parse(byte[] rowData, int offset, nint rowAddr, MemoryContext ctx, MsvcStringReader strings)
|
||||
{
|
||||
// +0x00: Id string ptr (8 bytes)
|
||||
var idPtr = (nint)BitConverter.ToInt64(rowData, offset);
|
||||
if (idPtr == 0 || !ctx.IsDatPtr(idPtr)) return null;
|
||||
var id = strings.ReadDatString(idPtr);
|
||||
|
||||
// +0x08: WorldArea FK — pointer(8) + metadata(8) = 16 bytes
|
||||
nint worldAreaPtr = 0;
|
||||
if (offset + 0x10 <= rowData.Length)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(rowData, offset + 0x08);
|
||||
if (ptr != 0 && ctx.IsDatPtr(ptr))
|
||||
worldAreaPtr = ptr;
|
||||
}
|
||||
|
||||
// +0x28: Name string ptr (8 bytes)
|
||||
string? name = null;
|
||||
if (offset + 0x30 <= rowData.Length)
|
||||
{
|
||||
var namePtr = (nint)BitConverter.ToInt64(rowData, offset + 0x28);
|
||||
if (namePtr != 0 && ctx.IsDatPtr(namePtr))
|
||||
name = strings.ReadDatString(namePtr);
|
||||
}
|
||||
|
||||
// +0x48: Act i32 (4 bytes)
|
||||
var act = 0;
|
||||
if (offset + 0x4C <= rowData.Length)
|
||||
act = BitConverter.ToInt32(rowData, offset + 0x48);
|
||||
|
||||
return new MapPinDatRow
|
||||
{
|
||||
RowAddress = rowAddr,
|
||||
Id = id,
|
||||
WorldAreaPtr = worldAreaPtr,
|
||||
Name = name,
|
||||
Act = act,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed row from QuestStates.dat.
|
||||
/// +0x00: Quest.dat FK (TableReference ptr), +0x10: Order/StateId, +0x34: Text ptr, +0x3D: Message ptr.
|
||||
/// Parsed row from QuestStates.dat (208 bytes per row).
|
||||
/// Schema (from poe-tool-dev/dat-schema poe2/QuestStates):
|
||||
/// +0x00(16): Quest FK, +0x10(4): Order, +0x14(16): FlagsPresent[], +0x24(16): FlagsMissing[],
|
||||
/// +0x34(8): Text, +0x3C(1): bool, +0x3D(8): Message,
|
||||
/// +0x45(16): MapPinsKeys1[], +0x55(4): i32, +0x59(8): MapPinsText,
|
||||
/// +0x61(16): MapPinsKey FK, ...
|
||||
/// </summary>
|
||||
public sealed record QuestStateDatRow
|
||||
{
|
||||
|
|
@ -12,6 +16,16 @@ public sealed record QuestStateDatRow
|
|||
public int StateId { get; init; }
|
||||
public string? Text { get; init; }
|
||||
public string? Message { get; init; }
|
||||
|
||||
// MapPins fields
|
||||
/// <summary>Number of MapPins entries in the array at +0x45.</summary>
|
||||
public int MapPinsCount { get; init; }
|
||||
/// <summary>Pointer to array of MapPins.dat row references in variable data.</summary>
|
||||
public nint MapPinsArrayPtr { get; init; }
|
||||
/// <summary>Localized map pin text at +0x59.</summary>
|
||||
public string? MapPinsText { get; init; }
|
||||
/// <summary>Single MapPins.dat FK pointer at +0x61.</summary>
|
||||
public nint MapPinPtr { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -23,15 +37,14 @@ public sealed class QuestStateDatRowParser : IDatRowParser<QuestStateDatRow>
|
|||
{
|
||||
var offsets = ctx.Offsets;
|
||||
|
||||
// +0x00: Quest.dat row pointer (TableReference — first 8 bytes)
|
||||
// .dat row pointers can be 2-byte aligned — use IsDatPtr instead of IsValidHeapPtr
|
||||
// +0x00: Quest.dat row pointer (TableReference — first 8 bytes of 16-byte FK)
|
||||
var questRowPtr = (nint)BitConverter.ToInt64(rowData, offset + offsets.QuestDatNameOffset);
|
||||
if (questRowPtr == 0 || !ctx.IsDatPtr(questRowPtr)) return null;
|
||||
|
||||
// +0x10: Order int32 = stateId
|
||||
var stateId = BitConverter.ToInt32(rowData, offset + offsets.QuestDatOrderOffset);
|
||||
|
||||
// +0x34: text wchar* pointer (points into .dat variable data — use IsDatPtr + ReadDatString)
|
||||
// +0x34: text string ptr (localized, points into .dat variable data)
|
||||
string? text = null;
|
||||
var textPtrOffset = offset + offsets.QuestDatTextOffset;
|
||||
if (textPtrOffset + 8 <= rowData.Length)
|
||||
|
|
@ -41,7 +54,7 @@ public sealed class QuestStateDatRowParser : IDatRowParser<QuestStateDatRow>
|
|||
text = strings.ReadDatString(textPtr);
|
||||
}
|
||||
|
||||
// +0x3D: message wchar* pointer (same — .dat variable data)
|
||||
// +0x3D: message string ptr (localized)
|
||||
string? message = null;
|
||||
var msgPtrOffset = offset + offsets.QuestDatMessageOffset;
|
||||
if (msgPtrOffset + 8 <= rowData.Length)
|
||||
|
|
@ -51,6 +64,45 @@ public sealed class QuestStateDatRowParser : IDatRowParser<QuestStateDatRow>
|
|||
message = strings.ReadDatString(msgPtr);
|
||||
}
|
||||
|
||||
// +0x45 (69): MapPinsKeys1 array — count(8) + pointer(8) = 16 bytes
|
||||
var mapPinsCount = 0;
|
||||
nint mapPinsArrayPtr = 0;
|
||||
var mapPinsOff = offset + 69;
|
||||
if (mapPinsOff + 16 <= rowData.Length)
|
||||
{
|
||||
var count = BitConverter.ToInt64(rowData, mapPinsOff);
|
||||
if (count is > 0 and <= 50)
|
||||
{
|
||||
mapPinsCount = (int)count;
|
||||
mapPinsArrayPtr = (nint)BitConverter.ToInt64(rowData, mapPinsOff + 8);
|
||||
if (!ctx.IsDatPtr(mapPinsArrayPtr))
|
||||
{
|
||||
mapPinsCount = 0;
|
||||
mapPinsArrayPtr = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +0x59 (89): MapPinsText string ptr (localized)
|
||||
string? mapPinsText = null;
|
||||
var mapPinsTextOff = offset + 89;
|
||||
if (mapPinsTextOff + 8 <= rowData.Length)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(rowData, mapPinsTextOff);
|
||||
if (ptr != 0 && ctx.IsDatPtr(ptr))
|
||||
mapPinsText = strings.ReadDatString(ptr);
|
||||
}
|
||||
|
||||
// +0x61 (97): MapPinsKey single FK — pointer(8) + metadata(8) = 16 bytes
|
||||
nint mapPinPtr = 0;
|
||||
var mapPinOff = offset + 97;
|
||||
if (mapPinOff + 8 <= rowData.Length)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(rowData, mapPinOff);
|
||||
if (ptr != 0 && ctx.IsDatPtr(ptr))
|
||||
mapPinPtr = ptr;
|
||||
}
|
||||
|
||||
return new QuestStateDatRow
|
||||
{
|
||||
RowAddress = rowAddr,
|
||||
|
|
@ -58,6 +110,10 @@ public sealed class QuestStateDatRowParser : IDatRowParser<QuestStateDatRow>
|
|||
StateId = stateId,
|
||||
Text = text,
|
||||
Message = message,
|
||||
MapPinsCount = mapPinsCount,
|
||||
MapPinsArrayPtr = mapPinsArrayPtr,
|
||||
MapPinsText = mapPinsText,
|
||||
MapPinPtr = mapPinPtr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
87
src/Roboto.Memory/Files/WorldAreaRow.cs
Normal file
87
src/Roboto.Memory/Files/WorldAreaRow.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed row from WorldAreas.dat (same layout as AreaTemplate offsets).
|
||||
/// +0x00: Id (wchar*), +0x08: Name (wchar*), +0x10: Act (int32),
|
||||
/// +0x14: IsTown (byte), +0x15: HasWaypoint (byte),
|
||||
/// +0x16: ConnectionCount (int32), +0x1E: ConnectionArrayPtr (long),
|
||||
/// +0x26: MonsterLevel (int32), +0x2A: WorldAreaId (int32).
|
||||
/// </summary>
|
||||
public sealed record WorldAreaDatRow
|
||||
{
|
||||
public nint RowAddress { get; init; }
|
||||
/// <summary>Internal area ID (e.g. "G1_1_town", "G2_6_1").</summary>
|
||||
public string? Id { get; init; }
|
||||
/// <summary>Display name (e.g. "Clearfell Encampment").</summary>
|
||||
public string? Name { get; init; }
|
||||
public int Act { get; init; }
|
||||
public bool IsTown { get; init; }
|
||||
public bool HasWaypoint { get; init; }
|
||||
public int MonsterLevel { get; init; }
|
||||
public int WorldAreaId { get; init; }
|
||||
/// <summary>Number of connected areas. Capped at 30 for safety.</summary>
|
||||
public int ConnectionCount { get; init; }
|
||||
/// <summary>Pointer to array of WorldArea row pointers in variable data (8 bytes each).</summary>
|
||||
public nint ConnectionArrayPtr { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses WorldAreas.dat rows using AreaTemplate offsets from GameOffsets.
|
||||
/// </summary>
|
||||
public sealed class WorldAreaDatRowParser : IDatRowParser<WorldAreaDatRow>
|
||||
{
|
||||
public WorldAreaDatRow? Parse(byte[] rowData, int offset, nint rowAddr, MemoryContext ctx, MsvcStringReader strings)
|
||||
{
|
||||
var o = ctx.Offsets;
|
||||
|
||||
// +0x00: Id — pointer to wchar_t* internal ID
|
||||
var idPtr = (nint)BitConverter.ToInt64(rowData, offset + o.AreaTemplateRawNameOffset);
|
||||
if (idPtr == 0 || !ctx.IsDatPtr(idPtr)) return null;
|
||||
var id = strings.ReadDatString(idPtr);
|
||||
|
||||
// +0x08: Name — pointer to wchar_t* display name
|
||||
string? name = null;
|
||||
var nameOff = offset + o.AreaTemplateNameOffset;
|
||||
if (nameOff + 8 <= rowData.Length)
|
||||
{
|
||||
var namePtr = (nint)BitConverter.ToInt64(rowData, nameOff);
|
||||
if (namePtr != 0 && ctx.IsDatPtr(namePtr))
|
||||
name = strings.ReadDatString(namePtr);
|
||||
}
|
||||
|
||||
// +0x10: Act (int32)
|
||||
var act = BitConverter.ToInt32(rowData, offset + o.AreaTemplateActOffset);
|
||||
|
||||
// +0x14: IsTown (byte), +0x15: HasWaypoint (byte)
|
||||
var isTown = rowData[offset + o.AreaTemplateIsTownOffset] == 1;
|
||||
var hasWaypoint = rowData[offset + o.AreaTemplateHasWaypointOffset] == 1;
|
||||
|
||||
// +0x16: ConnectionCount (int32), +0x1E: ConnectionArrayPtr (long)
|
||||
// ExileCore: count at +22 (0x16), array ptr at +30 (0x1E)
|
||||
var connCount = BitConverter.ToInt32(rowData, offset + 0x16);
|
||||
if (connCount < 0 || connCount > 30) connCount = 0;
|
||||
nint connArrayPtr = 0;
|
||||
if (connCount > 0)
|
||||
connArrayPtr = (nint)BitConverter.ToInt64(rowData, offset + 0x1E);
|
||||
|
||||
// +0x26: MonsterLevel (int32)
|
||||
var monsterLevel = BitConverter.ToInt32(rowData, offset + o.AreaTemplateMonsterLevelOffset);
|
||||
|
||||
// +0x2A: WorldAreaId (int32)
|
||||
var worldAreaId = BitConverter.ToInt32(rowData, offset + o.AreaTemplateWorldAreaIdOffset);
|
||||
|
||||
return new WorldAreaDatRow
|
||||
{
|
||||
RowAddress = rowAddr,
|
||||
Id = id,
|
||||
Name = name,
|
||||
Act = act,
|
||||
IsTown = isTown,
|
||||
HasWaypoint = hasWaypoint,
|
||||
MonsterLevel = monsterLevel,
|
||||
WorldAreaId = worldAreaId,
|
||||
ConnectionCount = connCount,
|
||||
ConnectionArrayPtr = connArrayPtr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -115,9 +115,10 @@ public class GameMemoryReader : IDisposable
|
|||
_questNames ??= LoadQuestNames();
|
||||
_filesContainer = new FilesContainer(_ctx, _strings);
|
||||
_questStateLookup = new QuestStateLookup(_filesContainer);
|
||||
_ = _filesContainer.WorldAreas; // eager-load WorldAreas.dat
|
||||
|
||||
// Hierarchical state tree — owns EntityList, PlayerSkills, QuestFlags, Terrain
|
||||
_gameStates = new GameStates(_ctx, _components, _strings, _questNames, _questStateLookup);
|
||||
_gameStates = new GameStates(_ctx, _components, _strings, _questNames, _questStateLookup, _filesContainer);
|
||||
|
||||
// Diagnostics uses the EntityList from the hierarchy
|
||||
var entityList = _gameStates.InGame.AreaInstance.EntityList;
|
||||
|
|
@ -234,6 +235,32 @@ public class GameMemoryReader : IDisposable
|
|||
snap.AreaHasWaypoint = at.HasWaypoint;
|
||||
snap.AreaMonsterLevel = at.MonsterLevel;
|
||||
snap.WorldAreaId = at.WorldAreaId;
|
||||
|
||||
// Resolve connected areas via WorldAreas.dat
|
||||
if (_filesContainer is not null)
|
||||
{
|
||||
var worldArea = _filesContainer.WorldAreas.GetByAddress(at.Address);
|
||||
if (worldArea is not null)
|
||||
{
|
||||
var connections = _filesContainer.GetConnections(worldArea);
|
||||
if (connections.Count > 0)
|
||||
{
|
||||
snap.ConnectedAreas = connections.Select(c => new ConnectedAreaInfo
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
Act = c.Act,
|
||||
IsTown = c.IsTown,
|
||||
HasWaypoint = c.HasWaypoint,
|
||||
MonsterLevel = c.MonsterLevel,
|
||||
WorldAreaId = c.WorldAreaId,
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass current area to UIElements for quest path resolution
|
||||
gs.InGame.UIElements.CurrentAreaRowAddress = at.Address;
|
||||
}
|
||||
|
||||
// Camera matrix from WorldData
|
||||
|
|
|
|||
|
|
@ -205,8 +205,8 @@ public sealed class GameOffsets
|
|||
public int QuestStateMaxEntries { get; set; } = 256;
|
||||
|
||||
// ── QuestStates.dat row layout (119 bytes, non-aligned fields) ──
|
||||
/// <summary>Size of each .dat row in bytes. 0x68 = 104 (confirmed via CE imul stride). 0 = name resolution disabled.</summary>
|
||||
public int QuestDatRowSize { get; set; } = 0x68;
|
||||
/// <summary>Size of each .dat row in bytes. 0xD0 = 208 (confirmed via dat-schema). 0 = name resolution disabled.</summary>
|
||||
public int QuestDatRowSize { get; set; } = 0xD0;
|
||||
/// <summary>Dat row → Quest TableReference (16 bytes: pointer to Quest.dat row at +0x00). Follow Quest.dat row → +0x00 for name wchar*.</summary>
|
||||
public int QuestDatNameOffset { get; set; } = 0x00;
|
||||
/// <summary>Dat row → Order int32 (at offset 16 / 0x10).</summary>
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ public sealed class GameStates
|
|||
/// <summary>Raw qwords from controller 0x00-0x48 (before state slots), for UI diagnostics.</summary>
|
||||
public (int Offset, nint Value, string? Match, bool Changed, string? DerefInfo)[] ControllerPreSlots { get; private set; } = [];
|
||||
|
||||
public GameStates(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames, QuestStateLookup? questStateLookup = null)
|
||||
public GameStates(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames, QuestStateLookup? questStateLookup = null, FilesContainer? filesContainer = null)
|
||||
{
|
||||
_ctx = ctx;
|
||||
AreaLoading = new AreaLoading(ctx);
|
||||
InGame = new InGameState(ctx, components, strings, questNames, questStateLookup);
|
||||
InGame = new InGameState(ctx, components, strings, questNames, questStateLookup, filesContainer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@ public sealed class InGameState : RemoteObject
|
|||
public WorldData WorldData { get; }
|
||||
public UIElements UIElements { get; }
|
||||
|
||||
public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames, QuestStateLookup? questStateLookup = null)
|
||||
public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames, QuestStateLookup? questStateLookup = null, FilesContainer? filesContainer = null)
|
||||
: base(ctx)
|
||||
{
|
||||
AreaInstance = new AreaInstance(ctx, components, strings, questNames);
|
||||
WorldData = new WorldData(ctx);
|
||||
UIElements = new UIElements(ctx, strings) { QuestStateLookup = questStateLookup };
|
||||
UIElements = new UIElements(ctx, strings)
|
||||
{
|
||||
QuestStateLookup = questStateLookup,
|
||||
FilesContainer = filesContainer,
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool ReadData()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ public sealed class UIElements : RemoteObject
|
|||
/// <summary>Optional lookup for resolving quest state IDs to human-readable text.</summary>
|
||||
public QuestStateLookup? QuestStateLookup { get; set; }
|
||||
|
||||
/// <summary>Optional files container for resolving MapPins → WorldAreas.</summary>
|
||||
public FilesContainer? FilesContainer { get; set; }
|
||||
|
||||
/// <summary>Current area's WorldAreas.dat row address — set by GameMemoryReader for path resolution.</summary>
|
||||
public nint CurrentAreaRowAddress { get; set; }
|
||||
|
||||
public UIElements(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
|
||||
{
|
||||
_strings = strings;
|
||||
|
|
@ -388,9 +394,51 @@ public sealed class UIElements : RemoteObject
|
|||
var isTracked = trackedMap.ContainsKey(questPtr);
|
||||
|
||||
string? stateText = null;
|
||||
string? mapPinsText = null;
|
||||
List<ConnectedAreaInfo>? targetAreas = null;
|
||||
List<string>? pathToTarget = null;
|
||||
|
||||
if (QuestStateLookup is not null && questPtr != 0 && stateId > 0)
|
||||
{
|
||||
QuestStateLookup.TryGetStateText(questPtr, stateId, out stateText);
|
||||
|
||||
// Resolve target areas via MapPins → WorldAreas
|
||||
if (FilesContainer is not null)
|
||||
{
|
||||
var questStateRow = QuestStateLookup.GetQuestStateRow(questPtr, stateId);
|
||||
if (questStateRow is not null)
|
||||
{
|
||||
mapPinsText = questStateRow.MapPinsText;
|
||||
var worldAreas = FilesContainer.GetMapPinWorldAreas(questStateRow);
|
||||
if (worldAreas.Count > 0)
|
||||
{
|
||||
targetAreas = worldAreas.Select(wa => new ConnectedAreaInfo
|
||||
{
|
||||
Id = wa.Id,
|
||||
Name = wa.Name,
|
||||
Act = wa.Act,
|
||||
IsTown = wa.IsTown,
|
||||
HasWaypoint = wa.HasWaypoint,
|
||||
MonsterLevel = wa.MonsterLevel,
|
||||
WorldAreaId = wa.WorldAreaId,
|
||||
}).ToList();
|
||||
|
||||
// BFS path from current area to first target area
|
||||
if (CurrentAreaRowAddress != 0)
|
||||
{
|
||||
var currentArea = FilesContainer.WorldAreas.GetByAddress(CurrentAreaRowAddress);
|
||||
if (currentArea is not null)
|
||||
{
|
||||
var path = FilesContainer.FindPath(currentArea, worldAreas[0]);
|
||||
if (path is { Count: > 1 })
|
||||
pathToTarget = path.Select(p => p.Name ?? p.Id ?? "?").ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new QuestLinkedEntry
|
||||
{
|
||||
InternalId = internalId,
|
||||
|
|
@ -400,6 +448,9 @@ public sealed class UIElements : RemoteObject
|
|||
StateText = stateText,
|
||||
IsTracked = isTracked,
|
||||
QuestDatPtr = questPtr,
|
||||
MapPinsText = mapPinsText,
|
||||
TargetAreas = targetAreas,
|
||||
PathToTarget = pathToTarget,
|
||||
});
|
||||
|
||||
walk = next;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public sealed class QuestStateLookup
|
|||
{
|
||||
private readonly FilesContainer _files;
|
||||
private Dictionary<(nint questPtr, int stateId), string>? _lookup;
|
||||
private Dictionary<(nint questPtr, int stateId), QuestStateDatRow>? _rowLookup;
|
||||
private bool _attempted;
|
||||
|
||||
public QuestStateLookup(FilesContainer files)
|
||||
|
|
@ -34,6 +35,17 @@ public sealed class QuestStateLookup
|
|||
return _lookup.TryGetValue((questDatRowPtr, stateId), out text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full QuestStateDatRow for a (questPtr, stateId) pair.
|
||||
/// Needed to access MapPins fields for target area resolution.
|
||||
/// </summary>
|
||||
public QuestStateDatRow? GetQuestStateRow(nint questDatRowPtr, int stateId)
|
||||
{
|
||||
EnsureLookup();
|
||||
if (_rowLookup is null) return null;
|
||||
return _rowLookup.GetValueOrDefault((questDatRowPtr, stateId));
|
||||
}
|
||||
|
||||
private void EnsureLookup()
|
||||
{
|
||||
if (_attempted) return;
|
||||
|
|
@ -59,15 +71,21 @@ public sealed class QuestStateLookup
|
|||
return null;
|
||||
|
||||
var result = new Dictionary<(nint, int), string>();
|
||||
var rows = new Dictionary<(nint, int), QuestStateDatRow>();
|
||||
|
||||
foreach (var row in _files.QuestStates.Rows)
|
||||
{
|
||||
if (row.Text is null || row.QuestDatRowPtr == 0)
|
||||
if (row.QuestDatRowPtr == 0)
|
||||
continue;
|
||||
|
||||
result.TryAdd((row.QuestDatRowPtr, row.StateId), row.Text);
|
||||
var key = (row.QuestDatRowPtr, row.StateId);
|
||||
rows.TryAdd(key, row);
|
||||
|
||||
if (row.Text is not null)
|
||||
result.TryAdd(key, row.Text);
|
||||
}
|
||||
|
||||
_rowLookup = rows.Count > 0 ? rows : null;
|
||||
return result.Count > 0 ? result : null;
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +95,7 @@ public sealed class QuestStateLookup
|
|||
public void InvalidateCache()
|
||||
{
|
||||
_lookup = null;
|
||||
_rowLookup = null;
|
||||
_attempted = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/Roboto.Memory/Snapshots/ConnectedAreaInfo.cs
Normal file
15
src/Roboto.Memory/Snapshots/ConnectedAreaInfo.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight connected area info from WorldAreas.dat for the snapshot.
|
||||
/// </summary>
|
||||
public sealed class ConnectedAreaInfo
|
||||
{
|
||||
public string? Id { get; init; }
|
||||
public string? Name { get; init; }
|
||||
public int Act { get; init; }
|
||||
public bool IsTown { get; init; }
|
||||
public bool HasWaypoint { get; init; }
|
||||
public int MonsterLevel { get; init; }
|
||||
public int WorldAreaId { get; init; }
|
||||
}
|
||||
|
|
@ -38,6 +38,9 @@ public class GameStateSnapshot
|
|||
public int AreaMonsterLevel;
|
||||
public int WorldAreaId;
|
||||
|
||||
// Connected areas (from WorldAreas.dat)
|
||||
public List<ConnectedAreaInfo>? ConnectedAreas;
|
||||
|
||||
// Player
|
||||
public string? CharacterName;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,4 +22,13 @@ public sealed class QuestLinkedEntry
|
|||
public bool IsTracked { get; init; }
|
||||
/// <summary>Raw Quest.dat row pointer — used as key for merging tracked info.</summary>
|
||||
public nint QuestDatPtr { get; init; }
|
||||
|
||||
/// <summary>Localized map pin text from QuestStates.dat (e.g. "Travel to the Clearfell").</summary>
|
||||
public string? MapPinsText { get; init; }
|
||||
|
||||
/// <summary>Target WorldAreas resolved from QuestStates.dat MapPins → WorldAreas.dat.</summary>
|
||||
public List<ConnectedAreaInfo>? TargetAreas { get; init; }
|
||||
|
||||
/// <summary>BFS path from current area to target area (area names in order). Null if same area or unreachable.</summary>
|
||||
public List<string>? PathToTarget { get; init; }
|
||||
}
|
||||
|
|
|
|||
17
src/Roboto.Memory/test.csv
Normal file
17
src/Roboto.Memory/test.csv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
rownum,Quest,Order,FlagsPresent,FlagsMissing,Text,Text (French),Text (German),Text (Japanese),Text (Korean),Text (Portuguese),Text (Russian),Text (Spanish),Text (Thai),Text (Traditional Chinese),bool_60,Message,Message (French),Message (German),Message (Japanese),Message (Korean),Message (Portuguese),Message (Russian),Message (Spanish),Message (Thai),Message (Traditional Chinese),MapPinsKeys,i32_85,MapPinsText,MapPinsText (French),MapPinsText (German),MapPinsText (Japanese),MapPinsText (Korean),MapPinsText (Portuguese),MapPinsText (Russian),MapPinsText (Spanish),MapPinsText (Thai),MapPinsText (Traditional Chinese),MapPinsKey,[rid]_113,bool_129,[i32]_130,[i32]_146,i32_162,SoundEffect,string_182,[rid]_190,bool_206,bool_207
|
||||
0,0,0,[2936],[],Quest Complete - You have slain the Bloated Miller and received a reward from Renly.,Quête terminée — Vous avez tué le Meunier boursouflé et avez reçu une récompense de la part de Renly.,Quest abgeschlossen: Ihr habt den Aufgedunsenen Müller getötet und von Renly eine Belohnung erhalten.,クエスト完了 - 腐乱した粉屋を倒し、レンリーから報酬を受け取った,퀘스트 완료 - 불어 터진 방아꾼을 처치하고 렌리에게 보상을 받았습니다.,Missão Concluída - Você matou o Triturador Inchado e recebeu uma recompensa do Renly.,Задание выполнено - Вы убили Раздувшегося мельника и получили награду от Ренли.,Misión completa - Has derrotado al Molinero hinchado y recibido una recompensa de Renly.,เควสต์เสร็จสิ้น - คุณได้สังหารเจ้าของโรงสีขึ้นอืดและรับรางวัลจากเรนลีย์แล้ว,任務完成——你已經殺掉了浮腫米勒,並且從倫利那裡取得你的任務獎勵。,1,Quest Complete,Quête terminée,Quest abgeschlossen,クエスト完了,퀘스트 완료,Missão Concluída,Задание выполнено,Misión completa,เควสต์เสร็จสิ้น,任務完成,[],0,,,,,,,,,,,,[],,[],[],10,,,[],,
|
||||
1,0,1,[2935],[],Renly has offered you a reward for slaying the Bloated Miller. Take it.,Renly vous offre une récompense pour avoir tué le Meunier boursouflé. Prenez-la.,"Renly hat Euch eine Belohnung dafür angeboten, dass Ihr den Aufgedunsenen Müller getötet habt. Nehmt sie an Euch.",レンリーは腐乱した粉屋を倒した報酬を提示した。受け取れ,렌리가 불어 터진 방아꾼을 처치해 준 것에 대한 보상을 준다고 합니다. 받으십시오.,Renly te ofereceu uma recompensa por matar o Triturador Inchado. Aceite.,Ренли предложил вам награду за убийство Раздувшегося мельника. Заберите её.,Renly te ha ofrecido una recompensa por derrotar al Molinero hinchado. Acéptala.,เรนลีย์ได้เสนอรางวัลให้คุณเลือกเพื่อตอบแทนการสังหารเจ้าของโรงสีขึ้นอืด รับรางวัลเสีย,倫利要給你擊殺浮腫米勒的獎勵,收下它。,,Take Renly's reward,Prenez la récompense de Renly,Nehmt Renlys Belohnung,レンリーの報酬を受け取れ,렌리의 보상 받기,Pegue a recompensa do Renly,Заберите награду у Ренли,Acepta la recompensa de Renly,รับรางวัลของเรนลีย์,領取倫利的獎勵,[8],0,Take Renly's reward,Prenez la récompense de Renly,Nehmt Renlys Belohnung.,レンリーの報酬を受け取れ,렌리의 보상 받기,Pegue a recompensa do Renly,Заберите награду у Ренли,Acepta la recompensa de Renly,รับรางวัลของเรนลีย์,領取倫利的獎勵,,[],,[],[],10,,,[],,
|
||||
2,0,2,"[2934,2890]",[],The Blacksmith appears to be in charge in this logging encampment. Talk to him.,Le Forgeron semble être le responsable de ce campement forestier. Parlez-lui.,Der Schmied scheint in diesem Holzfällerlager das Sagen zu haben. Sprecht mit ihm.,鍛冶屋が伐採の野営地を取り仕切っているようだ。彼に話しかけろ,대장장이가 이 벌목 야영지를 책임지고 있는 것 같습니다. 그와 대화하십시오.,O Ferreiro parece estar no comando deste acampamento madeireiro. Fale com ele.,"Похоже, в этом лагере лесорубов кузнец за главного. Поговорите с ним.",Parece que el herrero está al mando de este campamento maderero. Habla con él.,เหมือนว่าช่างตีเหล็กจะเป็นผู้นำค่ายตัดไม้ ลองพูดคุยกับเขาดู,鐵匠似乎是這個伐木營地的負責人。與他交談。,,Talk to the Blacksmith,Parlez au Forgeron,Sprecht mit dem Schmied,鍛冶屋に話しかけろ,대장장이와 대화하기,Fale com o Ferreiro,Поговорите с кузнецом,Habla con el herrero,พูดคุยกับช่างตีเหล็ก,與鐵匠交談,[8],0,Talk to the Blacksmith,Parler au Forgeron,Sprecht mit dem Schmied.,鍛冶屋に話しかけろ,대장장이와 대화하기,Fale com o Ferreiro,Поговорите с кузнецом,Habla con el herrero,พูดคุยกับช่างตีเหล็ก,與鐵匠交談,,[],,[],[],10,,,[],,
|
||||
3,0,3,[2934],"[281,368]",You have slain the Bloated Miller and levelled up. Open the Passive Skill Screen and spend a Passive Skill Point to upgrade your character.,Vous avez tué le Meunier boursouflé et êtes monté de niveau. Ouvrez l'arbre des Talents et dépensez un point de Talent pour améliorer votre personnage.,"Ihr habt den Aufgedunsenen Müller getötet und eine neue Stufe erreicht. Öffnet den Passiven Fertigkeitenbaum und weist einen Passiven Fertigkeitspunkt zu, um Euren Charakter zu verbessern.",腐乱した粉屋を倒しレベルが上がった。パッシブスキル画面を開いてパッシブスキルポイントを消費し、キャラクターをアップグレードしろ,불어 터진 방아꾼을 처치하고 레벨을 올렸습니다. 패시브 스킬 창을 열고 패시브 스킬 포인트를 투자해 캐릭터를 강화하십시오.,Você matou o Triturador Inchado e subiu de nível. Abra a Tela de Habilidades Passivas e gaste um Ponto de Habilidade Passiva para melhorar seu personagem.,Вы убили Раздувшегося мельника и повысили свой уровень. Откройте экран пассивных умений и потратьте очко умения на улучшение персонажа.,Has derrotado al Molinero hinchado y subido de nivel. Abre la ventana de habilidades pasivas y gasta un punto de habilidad pasiva para mejorar tu personaje.,คุณได้สังหารเจ้าของโรงสีขึ้นอืดและได้ขึ้นเลเวลใหม่แล้ว อัพเกรดตัวละครของคุณด้วยการเปิดหน้าต่างพาสซีฟ แล้วใช้แต้มพาสซีฟ 1 แต้ม,你已經擊殺浮腫米勒並升等了,開啟天賦樹畫面使用天賦點數升級你的角色。,,,,,,,,,,,,[8],0,Open the Passive Skill Screen\nSpend your Passive Skill Point,Ouvrez votre Arbre des Talents\nDépensez-y votre point de Talent,Öffnet den Passiven Fertigkeitsbaum\nWeist den Passiven Fertigkeitspunkt zu,パッシブスキル画面を開け\nパッシブスキルポイントを使用しろ,패시브 스킬 창 열기\n패시브 스킬 포인트 투자하기,Abra a Tela de Habilidades Passivas\nGaste o seu Ponto de Habilidade Passiva,Откройте экран пассивных умений\nИспользуйте очко пассивного умения,Abre la pantalla de habilidades pasivas\nGasta tu punto de habilidad pasiva,เปิดหน้าจอพาสซีฟ\nใช้แต้มพาสซีฟของคุณ,開啟天賦樹畫面\n使用天賦點,,[],,[],[],10,,,[],,
|
||||
4,0,4,[2934],[],You have slain the Bloated Miller. Enter the logging encampment.,Vous avez tué le Meunier boursouflé. Entrez dans le campement forestier.,Ihr habt den Aufgedunsenen Müller getötet. Betretet das Holzfällerlager.,腐乱した粉屋を倒した。伐採の野営地に入れ,불어 터진 방아꾼을 처치했습니다. 벌목 야영지로 들어가십시오.,Você matou o Triturador Inchado. Entre no acampamento madeireiro.,Вы убили Раздувшегося мельника. Войдите в лагерь лесорубов.,Has derrotado al Molinero hinchado. Entra al campamento maderero.,คุณได้สังหารเจ้าของโรงสีขึ้นอืดแล้ว เข้าไปในค่ายตัดไม้,你已擊殺浮腫米勒。進入伐木營地。,,Enter town,Entrez dans la ville,Betretet die Stadt,街に入れ,마을 들어가기,Entre na cidade,Войдите в лагерь,Entra al pueblo,เข้าไปในเมือง,進入城鎮,[8],0,Enter the logging encampment,Entrez dans le campement forestier,Betretet das Holzfällerlager.,伐採の野営地に入れ,벌목 야영지 들어가기,Entre no acampamento de exploração madeireira,Войдите в лагерь лесорубов,Entra al campamento maderero,เข้าไปในค่ายตัดไม้,進入伐木營地,,[],,[],[],10,,,[],,
|
||||
5,0,5,[2933],[],A logging encampment is under attack by a diseased monstrosity that was once human. Kill it.,Un campement forestier subit l'attaque d'une monstruosité malade autrefois humaine. Tuez-la.,"Ein Holzfällerlager wird von einer krankhaften Monstrosität heimgesucht, die einst ein Mensch war. Tötet sie.",伐採の野営地がかつて人間だった蝕まれた怪物の攻撃を受けている。その怪物を倒せ,벌목 야영지가 한때 인간이었던 질병 걸린 거수에게 공격받고 있습니다. 처치하십시오.,"Um acampamento madeireiro está sob ataque de algo que já foi humano, mas agora é uma monstruosidade adoecida. Mate-a.","На лагерь лесорубов напало чумное чудовище, некогда бывшее человеком. Убейте его.",Un campamento maderero está recibiendo un ataque de una monstruosidad enfermiza que una vez fue humana. Mátala.,ค่ายตัดไม้ถูกรุกรานด้วยน้ำมือของอสุรกายอาบโรคที่เคยเป็นมนุษย์มาก่อน สังหารมันเสีย,伐木營地被原為人類的染病怪物襲擊。殺死牠。,,Slay the Bloated Miller,Tuez le Meunier boursouflé,Tötet den Aufgedunsenen Müller,腐乱した粉屋を倒せ,불어 터진 방아꾼 처치하기,Mate o Triturador Inchado,Убейте Раздувшегося мельника,Derrota al Molinero hinchado,สังหารเจ้าของโรงสีขึ้นอืด,殺死浮腫米勒,[5],0,Kill the Bloated Miller and end his rage,Tuez le Meunier boursouflé et mettez fin à sa rage,Tötet den Aufgedunsenen Müller und setzt seinem Wüten ein Ende.,腐乱した粉屋を倒し、彼の怒りを終わらせろ,불어 터진 방아꾼을 처치해 그의 격노를 잠재우기,Mate o Triturador Inchado e acabe com sua raiva.,Убейте Раздувшегося мельника и покончите с его яростью,Mata al Molinero hinchado y acaba con su furia,สังหารเจ้าของโรงสีขึ้นอืดแล้วยุติความคลั่งของเขา,擊殺浮腫米勒並終止他的怒火,,[],,[],[],10,,,[],,
|
||||
6,0,6,[2801],[],The wounded man mentioned his chief Miller went to warn Clearfell about a sickness plaguing their men. Track down the Miller and find safety in Clearfell.,L'homme blessé a mentionné que son Meunier en chef était parti avertir la Clairière d'une maladie frappant leurs hommes. Suivez la piste du Meunier et trouvez refuge dans la Clairière.,"Der Verwundete erwähnte, dass sein Anführer, der Müller, Lichtfall vor einer Krankheit warnen wollte, die ihre Männer plagt. Spürt den Müller auf und bringt Euch in Lichtfall in Sicherheit.",負傷した男は、彼の親方である粉屋が彼の部下たちを苦しめている疫病についてクリアフェルに警告しに行ったことを話していた。粉屋を追い、クリアフェルで安全な場所を見つけろ,다친 남자가 말하길 수석 방아꾼이 일꾼들 사이에서 돌고 있는 병에 대해 경고하기 위해 클리어펠로 향했다고 합니다. 방아꾼을 찾고 클리어펠에 피신하십시오.,O homem ferido mencionou que o Triturador foi avisar Clearfell sobre uma doença que assola seus homens. Rastreie o Triturador e fique em segurança em Clearfell.,"Раненый мужчина упомянул, что его начальник-мельник отправился в Клирфелл предупредить о болезни. Найдите мельника и безопасное убежище в Клирфелле.",El hombre herido ha mencionado que el jefe del molino ha ido a Sierraclara para advertirles sobre una enfermedad que está azotando a sus hombres. Busca al Molinero y encuentra refugio en Sierraclara.,ชายที่บาดเจ็บบอกว่าเจ้าของโรงสีออกไปเตือนเคลียร์เฟลเกี่ยวกับโรคภัยที่ระบาดไปตามคนของพวกเขา ตามหาเจ้าของโรงสีแล้วหาที่ปลอดภัยในเคลียร์เฟลเสีย,找到皆伐。,,Find Clearfell,Trouvez la Clairière,Findet Lichtfall,クリアフェルを見つけろ,클리어펠 찾기,Encontre Clearfell.,Найдите Клирфелл,Encuentra Sierraclara,ค้นหาเคลียร์เฟล,找到皆伐,[5],0,Search for the Miller and find safety in Clearfell,Cherchez le Meunier et trouvez refuge dans la Clairière,Sucht nach dem Müller und findet Sicherheit in Lichtfall.,粉屋を探し、クリアフェルで安全な場所を見つけろ,방아꾼을 찾고 클리어펠에 피신하기,Procure pelo Triturador e fique em segurança em Clearfell,Найдите мельника и безопасное убежище в Клирфелле,Busca al Molinero y encuentra refugio en Sierraclara,ตามหาเจ้าของโรงสี แล้วหาที่ปลอดภัยในเคลียร์เฟล,尋找米勒,並在皆伐尋求庇護,,[],,[],[],10,,,[],,
|
||||
7,1,0,[2929],[],Quest Complete - You have slain the Devourer and have received a reward from Renly.,Quête terminée — Vous avez tué le Dévoreur et avez reçu une récompense de la part de Renly.,Quest abgeschlossen: Ihr habt den Verschlinger getötet und von Renly eine Belohnung erhalten.,クエスト完了 - デヴァウラーを倒し、レンリーから報酬を受け取った,퀘스트 완료 - 포식자를 처치하고 렌리에게 보상을 받았습니다.,Missão Cumprida - Você matou o Devorador e recebeu uma recompensa do Renly.,Задание выполнено - Вы убили Пожирателя и получили награду от Ренли.,Misión completa - Has derrotado al Devorador y recibido una recompensa de Renly.,เควสต์เสร็จสิ้น - คุณได้สังหารตัวสวาปามและรับรางวัลจากเรนลีย์แล้ว,任務完成——你已經殺掉了吞噬獸,並且從倫利那裡取得你的任務獎勵。,1,Quest Complete,Quête terminée,Quest abgeschlossen,クエスト完了,퀘스트 완료,Missão Concluída,Задание выполнено,Misión completa,เควสต์เสร็จสิ้น,任務完成,[],0,,,,,,,,,,,,[],,[],[],10,,,[],,
|
||||
8,1,1,[2931],[],You have slain the Devourer. Talk to Renly in Clearfell for your reward.,Vous avez tué le Dévoreur. Parlez à Renly à la Clairière pour obtenir votre récompense.,Ihr habt den Verschlinger getötet. Sprecht Renly in Lichtfall auf Eure Belohnung an.,デヴァウラーを倒した。クリアフェルのレンリーに話しかけて報酬を受け取れ,포식자를 처치했습니다. 클리어펠에 있는 렌리와 대화해서 보상을 받으십시오.,Você matou o Devorador. Fale com Renly em Clearfell para receber sua recompensa.,Вы убили Пожирателя. Поговорите с Ренли в Клирфелле по поводу награды.,Has derrotado al Devorador. Habla con Renly en Sierraclara para recibir tu recompensa.,คุณได้สังหารตัวสวาปามแล้ว พูดคุยกับเรนลีย์ภายในค่ายเคลียร์เฟลเพื่อรับรางวัล,你已殺掉吞噬獸,在皆伐與倫利交談並領取你的獎勵。,,Talk to Renly for your reward,Parlez à Renly pour obtenir votre récompense,Sprecht Renly auf Eure Belohnung an,レンリーに話しかけて報酬を受け取れ,렌리와 대화해서 보상 받기,Fale com Renly para pegar sua recompensa,Поговорите с Ренли о награде,Habla con Renly para recibir tu recompensa,พูดคุยกับเรนลีย์เพื่อรับรางวัล,與倫利交談以獲得獎勵,[8],0,Talk to Renly for your reward,Parlez à Renly pour obtenir votre récompense,Sprecht Renly auf Eure Belohnung an.,レンリーに話しかけて報酬を受け取れ,렌리와 대화해서 보상 받기,Fale com Renly para receber sua recompensa,Поговорите с Ренли о награде,Habla con Renly para recibir tu recompensa,พูดคุยกับเรนลีย์เพื่อรับรางวัล,與倫利交談以獲得獎勵,,[],,[],[],10,,,[],,
|
||||
9,1,2,[2932],[],You have cornered the Devourer. Kill it.,Vous avez acculé le Dévoreur. Tuez-le.,Ihr habt den Verschlinger aufgespürt. Tötet ihn.,デヴァウラーを追い詰めた。やつを倒せ,포식자를 막다른 길로 몰았습니다. 처치하십시오.,Você encurralou o Devorador. Mate-o.,Вы загнали Пожирателя в угол. Убейте его.,Has acorralado al Devorador. Mátalo.,คุณต้อนตัวสวาปามให้จนมุมแล้ว สังหารมันเสีย,你已將吞噬獸逼入死角。殺死牠。,,Kill the Devourer,Tuez le Dévoreur,Tötet den Verschlinger,デヴァウラーを倒せ,포식자 처치하기,Mate o Devorador,Убейте Пожирателя,Mata al Devorador,สังหารตัวสวาปาม,殺死吞噬獸,[14],0,Kill the Devourer,Tuez le Dévoreur,Tötet den Verschlinger.,デヴァウラーを倒せ,포식자 처치하기,Mate o Devorador,Убейте Пожирателя,Mata al Devorador,สังหารตัวสวาปาม,殺死吞噬獸,14,[],,[],[],10,,,[],,
|
||||
10,1,3,"[2925,2892]",[],You have found the Mud Burrow. Search the tunnels for the Devourer.,Vous avez trouvé la Tanière boueuse. Fouillez les tunnels à la recherche du Dévoreur.,Ihr habt die Schlammgrube gefunden. Durchsucht die Tunnel nach dem Verschlinger.,泥の巣穴を見つけたトンネルを探索しデヴァウラーを見つけろ,진흙 토굴을 찾았습니다. 굴을 수색해서 포식자를 찾으십시오.,Você encontrou a Toca Lamacenta. Procure pelo Devorador nos túneis.,"Вы нашли Грязевую нору. Обыщите туннели, чтобы найти Пожирателя.",Has encontrado el Lodazal. Registra los túneles para encontrar al Devorador.,คุณได้พบโพรงโคลนแล้ว ตามหาตัวสวาปามภายในโพรงเหล่านี้,你已找到泥沼陋居,在坑道中搜尋吞噬獸的蹤跡。,,Search the Mud Burrow,Fouillez la Tanière Boueuse,Durchsucht die Schlammgrube,泥の巣穴を探索しろ,진흙 토굴 수색하기,Procure na Toca Lamacenta,Обыщите Грязевую нору,Investiga el Lodazal,ค้นหาภายในโพรงโคลน,在泥沼陋居進行搜索,[14],0,Find the Devourer and slay it,Trouvez le Dévoreur et tuez-le,Findet den Verschlinger und tötet ihn.,デヴァウラーを見つけて倒せ,포식자를 찾아서 처치하기,Encontre o Devorador e mate-o,Найдите Пожирателя и убейте его,Encuentra al Devorador y mátalo,ตามหาและสังหารตัวสวาปาม,找出吞噬獸並加以消滅,14,[],,[],[],10,,,[],,
|
||||
11,1,4,[2925],[],The Devourer lives underground in a Mud Burrow. Find it.,Le Dévoreur vit sous terre dans une Tanière boueuse. Trouvez-la.,Der Verschlinger verweilt in einer Schlammgrube unter der Erde. Findet sie.,デヴァウラーは泥の巣穴の地下に棲んでいる。見つけ出せ,포식자는 진흙 토굴 지하에 살고 있습니다. 찾으십시오.,O Devorador vive no subterrâneo em uma Toca Lamacenta. Encontre-o.,Пожиратель живёт под землёй в Грязевой норе. Найдите её.,El Devorador vive bajo tierra en un Lodazal. Encuéntralo.,ตัวสวาปามอาศัยอยู่ใต้ดินในโพรงโคลน ตามหามันให้เจอ,吞噬獸住在地底下的泥沼陋居,想辦法找到牠。,,Find the Mud Burrow,Trouvez la Tanière boueuse,Findet die Schlammgrube,泥の巣穴を見つけろ,진흙 토굴 찾기,Encontre a Toca Lamacenta,Найдите Грязевую нору,Encuentra el Lodazal,ค้นหาโพรงโคลน,尋找泥沼陋居,[9],0,Search Clearfell to find the Mud Burrow entrance\nSlay the Devourer in its lair,Fouillez la Clairière pour trouver l'entrée de la Tanière boueuse\nTuez le Dévoreur dans son antre,Durchsucht Lichtfall nach dem Eingang zur Schlammgrube\nTötet den Verschlinger in seinem Versteck.,クリアフェルを探索し泥の巣穴の入口を見つけろ\nデヴァウラーをその巣で倒せ,클리어펠을 수색해서 진흙 토굴 입구 찾기\n소굴에 있는 포식자 처치하기,Procure em Clearfell para encontrar a entrada da Toca Lamacenta\nMate o Devorador em seu covil,Найдите на Вырубке вход в Грязевую нору\nУбейте Пожирателя в его логове,Registra Sierraclara para encontrar la entrada al Lodazal\nDerrota al Devorador en su guarida,ค้นหาทางเข้าโพรงโคลนภายในเคลียร์เฟล\nสังหารตัวสวาปามในรังของมัน,搜尋皆伐,找出泥沼陋居的入口\n在吞噬獸的巢穴擊殺吞噬獸,,[94],,[],[],10,,,[],,
|
||||
12,1,5,[2925],[],Find the Devourer in its Mud Burrow and slay it so that the Ezomytes can safely leave the walls of Clearfell once more.,Trouvez le Dévoreur dans sa Tanière boueuse et tuez-le pour que les Ézomytes puissent à nouveau quitter les murs de la Clairière en toute sécurité.,"Spürt den Verschlinger in seiner Schlammgrube auf und tötet ihn, damit die Ezomyten die Mauern von Lichtfall endlich wieder sicher verlassen können.",エゾマイト人が再び安全にクリアフェルの壁から離れることができるように、泥の巣穴でデヴァウラーを見つけて倒せ,에조미어인들이 다시 안전하게 클리어펠 밖으로 떠날 수 있도록 진흙 토굴에 있는 포식자를 찾아 처치하십시오.,Encontre o Devorador em sua Toca Lamacenta e mate-o para que os Ezomitas possam sair de Clearfell em segurança.,"Найдите Пожирателя в его Грязевой норе и убейте его, чтобы эзомиты могли вновь без опаски выходить за стены Клирфелла.",Encuentra al Devorador en el Lodazal y mátalo para que los ezomitas puedan salir con seguridad de los muros de Sierraclara.,ตามหาและสังหารตัวสวาปามภายในโพรงโคลน เพื่อให้เหล่าเอโซไมต์ได้ออกจากเคลียร์เฟลอย่างปลอดภัยอีกครั้ง,在泥沼陋居中找到吞噬獸並加以擊殺,讓艾茲麥人能再次安全地踏出皆伐城牆的保護範圍。,,Slay the Devourer,Tuez le Dévoreur,Tötet den Verschlinger,デヴァウラーを倒せ,포식자 처치하기,Mate o Devorador,Убейте Пожирателя,Derrota al Devorador,สังหารตัวสวาปาม,擊殺吞噬獸,[9],0,Search Clearfell for the entrance to the Mud Burrow\nSlay the Devourer in its lair,Fouillez la Clairière à la recherche de l'entrée de la Tanière boueuse\nTuez le Dévoreur dans son antre,Sucht in Lichtfall nach dem Eingang zur Schlammgrube\nTötet den Verschlinger in seinem Versteck.,クリアフェルを探索し泥の巣穴の入口を見つけろ\nデヴァウラーをその巣で倒せ,클리어펠을 수색해서 진흙 토굴 입구 찾기\n소굴에 있는 포식자 처치하기,Procure em Clearfell para encontrar a entrada da Toca Lamacenta\nMate o Devorador em seu covil,Найдите на Вырубке вход в Грязевую нору\nУбейте Пожирателя в его логове,Registra Sierraclara para encontrar la entrada al Lodazal\nDerrota al Devorador en su guarida,ค้นหาทางเข้าโพรงโคลนภายในเคลียร์เฟล\nสังหารตัวสวาปามในรังของมัน,在伐木場尋找泥沼陋居的入口\n在吞噬獸的巢穴擊殺吞噬獸,,[],,[],[],10,,,[],,
|
||||
13,2,0,[4701],[],Quest Complete - You have released a dark entity called the Hooded One from the Tree of Souls.,Quête terminée — Vous avez libéré de l'Arbre des âmes une entité sombre appelée l'Encapuchonné.,Quest abgeschlossen: Ihr habt ein dunkles Wesen namens der Verhüllte aus dem Baum der Seelen befreit.,クエスト完了 - フードをかぶった者と呼ばれる闇の存在を魂の木から解放した,퀘스트 완료 - 영혼의 나무에서 두건 쓴 자라는 어둠의 존재를 풀어줬습니다.,"Missão Concluída - Você libertou uma entidade sombria da Árvore das Almas chamada O Encapuzado ",Задание выполнено - Вы освободили тёмную сущность по имений Скрытный от Дерева Душ.,Misión completa - Has liberado a una entidad oscura llamada el Encapuchado del Árbol de las almas.,เควสต์เสร็จสิ้น - คุณได้ปลดปล่อยบุคคลมืดมนที่มีชื่อว่าผู้คลุมกายออกมาจากต้นตรึงวิญญาณแล้ว,任務完成——你從攝魂之樹釋放出一個名為黑衣幽魂的黑暗生物。,1,Quest Complete,Quête terminée,Quest abgeschlossen,クエスト完了,퀘스트 완료,Missão Concluída,Задание выполнено,Misión completa,เควสต์เสร็จสิ้น,任務完成,[],0,,,,,,,,,,,,[],,[],[],10,,,[],,
|
||||
14,2,1,[4696],[],You have released a dark entity called the Hooded One from the Tree of Souls. Return to Clearfell Encampment and speak to Una.,Vous avez libéré de l'Arbre des âmes une entité sombre appelée l'Encapuchonné. Retournez au Campement de la Clairière et parlez à Una.,Ihr habt ein dunkles Wesen namens der Verhüllte aus dem Baum der Seelen befreit. Kehrt zum Lichtfall-Lager zurück und sprecht mit Una.,フードをかぶった者と呼ばれる闇の存在を魂の木から解放した。クリアフェルの野営地に戻りウーナと話せ,영혼의 나무에서 두건 쓴 자라는 어둠의 존재를 풀어줬습니다. 클리어펠 야영지로 돌아가서 우나와 대화하십시오.,Você libertou uma entidade sombria da Árvore das Almas chamada O Encapuzado. Volte ao Acampamento Clearfell e fale com a Una.,Вы освободили тёмную сущность по имений Скрытный от Дерева Душ. Вернитесь в Лагерь Клирфелл и поговорите с Уной.,Has liberado a una entidad oscura llamada el Encapuchado en el Árbol de las almas. Regresa al Campamento de Sierraclara y habla con Una.,คุณได้ปลดปล่อยบุคคลมืดมนที่มีชื่อว่าผู้คลุมกายออกมาจากต้นตรึงวิญญาณแล้ว กลับไปพูดคุยกับอูน่าที่ค่ายเคลียร์เฟล,你從攝魂之樹釋放出一個名為黑衣幽魂的黑暗生物。返回皆伐營地與烏娜交談。,,Meet Una in Clearfell,Retrouvez Una à la Clairière,Trefft Una in Lichtfall,クリアフェルでウーナに会え,클리어펠에 있는 우나 만나기,Encontre Una em Clearfell,Встретьтесь с Уной в Клирфелле,Reúnete con Una en Sierraclara,ไปพบกับอูน่าในเคลียร์เฟล,在皆伐與烏娜碰面,[8],0,Return to Clearfell Encampment and speak to Una,Retournez au Campement de la Clairière et parlez à Una,Kehrt zum Lichtfall-Lager zurück und sprecht mit Una.,クリアフェルの野営地に戻りウーナと話せ,클리어펠 야영지로 돌아가서 우나와 대화하기,Volte ao Acampamento Clearfell e fale com Una,Вернитесь в Лагерь Клирфелл и поговорите с Уной,Regresa al Campamento de Sierraclara y habla con Una,กลับไปยังค่ายเคลียร์เฟลแล้วพูดคุยกับอูน่า,返回皆伐營地與烏娜交談,,[],,[],[],10,,,[],,
|
||||
38,2,25,[2894],[2943],This vale is littered with the debris of countless battles. Search it for anything that may still be useful.,Cette vallée est jonchée des débris d'innombrables batailles. Cherchez-y tout ce qui pourrait encore être utile.,"\r\nDieses Tal ist mit den Trümmern unzähliger Schlachten übersät. Durchsucht es nach allem, was noch nützlich sein könnte.",この谷には数え切れない戦いの残骸が散らばっている。まだ役に立ちそうなものを探せ,이 계곡에는 헤아릴 수 없이 많은 전투의 흔적이 흩어져 있습니다. 잔해를 수색해서 아직 쓸 수 있는 걸 뭐든 찾아내십시오.,Este vale é coberto por detritos de inúmeras batalhas. Procure coisas que possam ser úteis,"Эта долина щетинится останками бесчисленных битв. Обыщите её на предмет того, что ещё может быть полезно.",El valle está lleno de deshechos de innumerables batallas. Regístralo en busca de objetos que puedan resultar útiles.,ห้วยนี้เกลื่อนกลาดไปด้วยซากของศึกเหนือคณานับ ค้นหาสิ่งที่ยังพอมีประโยชน์ภายในนี้,殘餘的魔法能量還在赤谷中縈繞。調查鐵鏽方尖碑,尋找有關力量魔符的情報。,,Search the Red Vale,Inspectez la Vallée rouge,Durchsucht das Rote Tal,赤き谷を探索しろ,붉은 계곡 수색하기,Procure no Vale Vermelho,Обыщите Красную Долину,Registra el Valle rojo,ค้นหาภายในห้วยสีชาด,調查鐵鏽方尖碑,[27],0,Search the Red Vale for Obelisks of Rust containing Runes of Power,Cherchez dans la Vallée rouge les Obélisques de Rouille qui contiennent les Runes de pouvoir,Durchsucht das Rote Tal nach Obelisken aus Rost,die Runen der Macht enthalten.,赤き谷で力のルーンが含まれる錆びたオベリスクを探せ,붉은 계곡을 수색해서 힘의 룬을 지닌 녹의 오벨리스크 찾기,Procure no Vale Vermelho por Obeliscos da Ferrugem que contenham Runas de Poder,Отыщите в Красной Долине ржавые обелиски с Рунами силы,Registra el Valle rojo para encontrar los obeliscos oxidados que contienen las runas de poder,ค้นหาเสาหินสนิมที่มีอักขระแห่งพลังภายในห้วยสีชาด,調查鐵鏽方尖碑\n收集力量魔符,27,[],,[],[],10,,,"[2974,2975,2976]"
|
||||
|
Can't render this file because it has a wrong number of fields in line 17.
|
Loading…
Add table
Add a link
Reference in a new issue