diff --git a/pkNX.Structures.FlatBuffers/Gen9/World/Area/AreaInfo.cs b/pkNX.Structures.FlatBuffers/Gen9/World/Area/AreaInfo.cs index 601412ba..4bb3092b 100644 --- a/pkNX.Structures.FlatBuffers/Gen9/World/Area/AreaInfo.cs +++ b/pkNX.Structures.FlatBuffers/Gen9/World/Area/AreaInfo.cs @@ -35,4 +35,7 @@ public class AreaInfo [FlatBufferItem(22)] public EncBiome BaseBiome { get; set; } [FlatBufferItem(23)] public AreaTag Tag { get; set; } [FlatBufferItem(24)] public OverrideBiome OverrideBiome { get; set; } + + public int ActualMinLevel => MinEncLv != 0 ? MinEncLv : 1; + public int ActualMaxLevel => MaxEncLv != 0 ? MaxEncLv : 100; } diff --git a/pkNX.Structures/Havok/HavokCollision.cs b/pkNX.Structures/Havok/HavokCollision.cs index 7db02cfc..147b00b7 100644 --- a/pkNX.Structures/Havok/HavokCollision.cs +++ b/pkNX.Structures/Havok/HavokCollision.cs @@ -425,7 +425,7 @@ private static (int, uint) ReadVar32(ReadOnlySpan buffer) }; } - public class AABBTree + public class AABBTree : IContainsV3f { private readonly List _nodes; private int _numLeafRects; @@ -471,8 +471,10 @@ public AABBTree(List nodes) // Official logic for area-containment checks for y intersection between y+1 and y-10000.0 public bool ContainsPoint(float x, float y, float z) => ContainsPointInNode(1, x, y - 10000, y + 1, z); + public bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ) + => ContainsPointInNode(1, x, y - 10000, y + 1, z, toleranceX, toleranceY, toleranceZ); - private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, float z) + private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, float z, float tx = 0f, float ty = 0f, float tz = 0f) { if (nodeIndex == 0) return false; @@ -480,16 +482,17 @@ private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, flo var node = _nodes[nodeIndex]; for (var i = 0; i < 4; i++) { - if (node.lx[i] > node.hx[i] || !(node.lx[i] <= x && x <= node.hx[i])) + if (node.lx[i] > node.hx[i] || !(node.lx[i] - tx <= x) || !(node.hx[i] + tx >= x)) continue; - if (node.lz[i] > node.hz[i] || !(node.lz[i] <= z && z <= node.hz[i])) + if (node.lz[i] > node.hz[i] || !(node.lz[i] - tz <= z) || !(node.hz[i] + tz >= z)) continue; - if (node.ly[i] > node.hy[i] || !(node.ly[i] <= hy && ly <= node.hy[i])) + if (node.ly[i] > node.hy[i] || !(node.ly[i] - ty <= hy) || !(node.hy[i] + ty >= ly)) continue; - if (node.IsLeaf || ContainsPointInNode((int)node.data[i], x, ly, hy, z)) + if (node.IsLeaf) + return true; + if (ContainsPointInNode((int)node.data[i], x, ly, hy, z, tx, ty, tz)) return true; } - return false; } } @@ -585,3 +588,9 @@ private struct HavokItem public uint Count; } } + +public interface IContainsV3f +{ + bool ContainsPoint(float x, float y, float z); + bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ); +} diff --git a/pkNX.WinForms/Dumping/EncounterDumperSV.cs b/pkNX.WinForms/Dumping/EncounterDumperSV.cs deleted file mode 100644 index 7d7c6dbd..00000000 --- a/pkNX.WinForms/Dumping/EncounterDumperSV.cs +++ /dev/null @@ -1,1015 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using pkNX.Containers; -using pkNX.Structures; -using pkNX.Structures.FlatBuffers; -using pkNX.WinForms.Subforms; - -namespace pkNX.WinForms; - -public class EncounterDumperSV -{ - private readonly IFileInternal ROM; - - public EncounterDumperSV(IFileInternal rom) => ROM = rom; - - public void DumpTo(string path, IReadOnlyList specNames, IReadOnlyList moveNames, - Dictionary placeNameMap, - bool writeText = true, bool writePickle = true) - { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - - var field = new PaldeaFieldModel(ROM); - var scene = new PaldeaSceneModel(ROM, field); - var fsym = new PaldeaFixedSymbolModel(ROM); - var csym = new PaldeaCoinSymbolModel(ROM); - var mlEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_100000.bin")); - var alEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_atlantis.bin")); - var pokeData = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin")); - var fsymData = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/fixed_symbol/fixed_symbol_table/fixed_symbol_table_array.bin")); - var eventBattle = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin")); - - var result = new Dictionary>(); - using var sw = File.CreateText(Path.Combine(path, "titan_enc.txt")); - foreach (var areaName in scene.areaNames) - { - var areaInfo = scene.AreaInfos[areaName]; - var name = areaInfo.LocationNameMain; - if (string.IsNullOrEmpty(name)) - continue; - if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) - continue; - - // Determine potential spawners - var areaEncPoints = GetAreaPoints(scene, areaName, alEncPoints, mlEncPoints, areaInfo); - var slots = GetEncounters(areaEncPoints, pokeData, areaName, scene); - - // Consolidate encounters - ConsolidateEncounters(slots); - - // Add to loc id - var locationID = placeNameMap[name].Index; - AddSlots(result, locationID, slots); - - // Output to stream if available. - if (writeText) - WriteLocation(sw, specNames, placeNameMap, areaName, areaInfo, slots); - } - - if (writeText) - { - using var tw = File.CreateText(Path.Combine(path, "titan_loc_enc.txt")); - WriteLocEncList(tw, specNames, placeNameMap, result); - } - if (writePickle) - { - string binPath = Path.Combine(path, "encounter_wild_paldea.pkl"); - SerializeEncounters(binPath, result); - } - - // Fixed symbols - List serialized = new(); - int[] bannedIndexes = - { - 31, // Lighthouse Wingull - }; - foreach (var (game, gamePoints) in new[] { ("sl", fsym.scarletPoints), ("vl", fsym.violetPoints) }) - { - using var gw = File.CreateText(Path.Combine(path, $"titan_fixed_{game}.txt")); - for (var i = 0; i < fsymData.Table.Length; i++) - { - FixedSymbolTable? entry = fsymData.Table[i]; - var tableKey = entry.TableKey; - - var points = gamePoints.Where(p => p.TableKey == tableKey).ToList(); - if (points.Count == 0) - continue; - - var areas = new List(); - foreach (var areaName in scene.areaNames) - { - if (scene.isAtlantis[areaName]) - continue; - - var areaInfo = scene.AreaInfos[areaName]; - var name = areaInfo.LocationNameMain; - if (string.IsNullOrEmpty(name)) - continue; - if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) - continue; - - if (points.Any(p => scene.IsPointContained(areaName, p.Position.X, p.Position.Y, p.Position.Z))) - areas.Add(areaName); - } - - var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList(); - - gw.WriteLine("==="); - gw.WriteLine(entry.TableKey); - gw.WriteLine("==="); - gw.WriteLine(" PokeData:"); - var pd = entry.PokeDataSymbol; - gw.WriteLine($" Species: {specNames[(int)pd.DevId]}"); - gw.WriteLine($" Form: {pd.FormId}"); - gw.WriteLine($" Level: {pd.Level}"); - gw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}"); - gw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}"); - - var talentStr = pd.TalentType switch - { - TalentType.RANDOM => "Random", - TalentType.V_NUM => $"{pd.TalentVNum} Perfect", - TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}", - _ => "Invalid", - }; - gw.WriteLine($" IVs: {talentStr}"); - gw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.TokuseiIndex]}"); - switch (pd.WazaType) - { - case WazaType.DEFAULT: - gw.WriteLine($" Moves: Random"); - break; - case WazaType.MANUAL: - gw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}"); - break; - } - - gw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}"); - gw.WriteLine($" GemType: {(int)pd.GemType}"); - - - gw.WriteLine(" Points:"); - foreach (var point in points) - { - gw.WriteLine($" - ({point.Position.X}, {point.Position.Y}, {point.Position.Z})"); - } - gw.WriteLine(" Areas:"); - foreach (var areaName in areas) - { - var areaInfo = scene.AreaInfos[areaName]; - var loc = areaInfo.LocationNameMain; - (string name, int index) = placeNameMap[loc]; - gw.WriteLine($" - {areaName} - {loc} - {name} ({index})"); - } - - // Serialize - if (locs.Count == 0) - continue; - if (bannedIndexes.Contains(i)) - continue; - WriteFixedSymbol(serialized, entry, locs); - } - } - - using var cw = File.CreateText(Path.Combine(path, $"titan_coin_symbol.txt")); - foreach (var entry in csym.Points) - { - var areas = new List(); - foreach (var areaName in scene.areaNames) - { - if (scene.isAtlantis[areaName]) - continue; - - var areaInfo = scene.AreaInfos[areaName]; - var name = areaInfo.LocationNameMain; - if (string.IsNullOrEmpty(name)) - continue; - if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) - continue; - - if (scene.IsPointContained(areaName, entry.Position.X, entry.Position.Y, entry.Position.Z)) - areas.Add(areaName); - } - - var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList(); - - cw.WriteLine("==="); - cw.WriteLine(entry.Name); - cw.WriteLine("==="); - cw.WriteLine($" First Num: {entry.FirstNum}"); - cw.WriteLine($" Coordinates: ({entry.Position.X}, {entry.Position.Y}, {entry.Position.Z})"); - - if (entry.IsBox) - { - cw.WriteLine($" Box Label: {entry.BoxLabel}"); - cw.WriteLine(" PokeData:"); - var pd = Array.Find(eventBattle.Table, e => e.Label == entry.BoxLabel)!.PokeData; - - cw.WriteLine($" Species: {specNames[(int)pd.DevId]}"); - cw.WriteLine($" Form: {pd.FormId}"); - cw.WriteLine($" Level: {pd.Level}"); - cw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}"); - cw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}"); - - var talentStr = pd.TalentType switch - { - TalentType.RANDOM => "Random", - TalentType.V_NUM => $"{pd.TalentVnum} Perfect", - TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}", - _ => "Invalid", - }; - cw.WriteLine($" IVs: {talentStr}"); - cw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.Tokusei]}"); - switch (pd.WazaType) - { - case WazaType.DEFAULT: - cw.WriteLine($" Moves: Random"); - break; - case WazaType.MANUAL: - cw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}"); - break; - } - - cw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}"); - cw.WriteLine($" GemType: {(int)pd.GemType}"); - } - - cw.WriteLine(" Areas:"); - foreach (var areaName in areas) - { - var areaInfo = scene.AreaInfos[areaName]; - var loc = areaInfo.LocationNameMain; - (string name, int index) = placeNameMap[loc]; - cw.WriteLine($" - {areaName} - {loc} - {name} ({index})"); - } - } - - var pathPickle = Path.Combine(path, "encounter_fixed_paldea.pkl"); - var ordered = serialized - .OrderBy(z => BinaryPrimitives.ReadUInt16LittleEndian(z)) // Species - .ThenBy(z => z[2]) // Form - .ThenBy(z => z[3]) // Level - ; - File.WriteAllBytes(pathPickle, ordered.SelectMany(z => z).ToArray()); - } - - private static void WriteFixedSymbol(ICollection exist, FixedSymbolTable entry, IReadOnlyList locs) - { - var enc = entry.PokeDataSymbol; - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); - - bw.Write((ushort)enc.DevId); - bw.Write((byte)enc.FormId); - bw.Write((byte)enc.Level); - - bw.Write((byte)enc.TalentVNum); - bw.Write((byte)enc.GemType); - bw.Write((ushort)0); // reserved - - bw.Write((ushort)enc.Waza1.WazaId); - bw.Write((ushort)enc.Waza2.WazaId); - - bw.Write((ushort)enc.Waza3.WazaId); - bw.Write((ushort)enc.Waza4.WazaId); - - // At most 3 locations, but just use 4 bytes. - Span temp = stackalloc byte[4]; - if (locs.Count > temp.Length) - throw new ArgumentException("Too many locations??", nameof(locs)); - for (int i = 0; i < locs.Count; i++) - temp[i] = (byte)locs[i]; - bw.Write(temp); - - var result = ms.ToArray(); - if (!exist.Any(x => x.SequenceEqual(result))) - exist.Add(result); - } - - private static void AddSlots(IDictionary> encounters, int locationID, List slots) - { - if (encounters.TryGetValue(locationID, out var existingSlots)) - { - existingSlots.AddRange(slots); - ConsolidateEncounters(existingSlots); - } - else - { - encounters[locationID] = slots; - } - } - - private static void SerializeEncounters(string binPath, Dictionary> tables) - { - int ctr = 0; - var result = new byte[tables.Count][]; - foreach (var (loc, slots) in tables) - { - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); - bw.Write((ushort)loc); - foreach (var slot in slots) - { - bw.Write((ushort)slot.Species); - bw.Write((byte)slot.Form); - bw.Write((byte)slot.Gender); - bw.Write((byte)slot.MinLevel); - bw.Write((byte)slot.MaxLevel); - } - result[ctr++] = ms.ToArray(); - } - var mini = MiniUtil.PackMini(result, "sv"); - File.WriteAllBytes(binPath, mini); - } - - private static void ConsolidateEncounters(List encounters) - { - // Merge and remove future indexes if they can be combined with current - // Pre-sort so we can just iterate. - encounters.Sort(); - - for (int i = 0; i < encounters.Count;) - { - var enc = encounters[i]; - int count = 1; - for (int j = i + 1; j < encounters.Count; j++) - { - if (!encounters[j].IsSameSpecFormGender(enc)) - break; - count++; - } - - // For i to i+count, merge within - // Remove all indexes that have Absorb return true with a later element - while (true) - { - if (TryAbsorbFromRange(encounters, i, count)) - count--; - else - break; - } - i += count; - } - } - - private static bool TryAbsorbFromRange(List encounters, int start, int count) - { - // Need to compare all elements with each-other; return true if any absorb. - var end = start + count - 1; - for (int i = start; i < end; i++) - { - for (int j = i + 1; j <= end; j++) - { - if (!encounters[i].Absorb(encounters[j])) - continue; - encounters.RemoveAt(j); - return true; - } - } - return false; - } - - private static List GetEncounters(List areaEncPoints, EncountPokeDataArray pokeData, string areaName, PaldeaSceneModel scene) - { - var added = new HashSet(); - var result = new List(); - foreach (var ep in areaEncPoints) - { - foreach (var pd in pokeData.Table) - { - // Check area - if (!string.IsNullOrEmpty(pd.Area) && !IsInArea(pd.Area, ep.AreaNo)) - continue; - - // Check loc - if (!string.IsNullOrEmpty(pd.LocationName) && !IsInArea(pd.LocationName, areaName, scene, ep)) - continue; - - // Check biome - if (!HasBiome(pd, (Biome)(int)ep.Biome)) - continue; - - // check level range overlap - if (!LevelWithinRange(pd, ep)) - continue; - - // check area level range overlap - // Assume flag, enable table, timetable are fine - // Assume version is fine -- union wireless sessions can share encounters. - - // Add encount - var e = PaldeaEncounter.GetNew(pd, ep); - if (added.Add(e.GetHash())) - result.Add(e); - - if (pd.BandPoke != 0) // Add band encount - { - var b = PaldeaEncounter.GetBand(pd, ep); - if (added.Add(b.GetHash())) - result.Add(b); - } - } - } - return result; - } - - private static bool LevelWithinRange(EncountPokeData pd, PointData clamp) - { - return clamp.LevelRange.X <= pd.MaxLevel && pd.MinLevel <= clamp.LevelRange.Y; - } - - private static bool HasBiome(EncountPokeData pd, Biome biome) - { - if (biome == pd.Biome1) - return true; - if (biome == pd.Biome2) - return true; - if (biome == pd.Biome3) - return true; - if (biome == pd.Biome4) - return true; - return false; - } - - private static bool IsInArea(ReadOnlySpan areaName, int areaNo) - { - int start = 0; - for (int i = 0; i < areaName.Length; i++) - { - if (areaName[i] != ',') - continue; - var name = areaName[start..i]; - if (int.TryParse(name, out var tmp) && areaNo == tmp) - return true; - start = i + 1; - } - return int.TryParse(areaName[start..], out var x) && areaNo == x; - } - - private static bool IsInArea(string locName, string areaName, PaldeaSceneModel scene, PointData ep) - { - var split = locName.Split(","); - foreach (string a in split) - { - if (a == areaName) - return true; - - if (!scene.IsPointContained(a, ep.Position.X, ep.Position.Y, ep.Position.Z)) - continue; - - return true; - } - return false; - } - - private static List GetAreaPoints(PaldeaSceneModel scene, string areaName, PointDataArray alEncPoints, - PointDataArray mlEncPoints, AreaInfo areaInfo) - { - var areaEncPoints = new List(); - foreach (var ep in scene.isAtlantis[areaName] ? alEncPoints.Table : mlEncPoints.Table) - { - var areaMin = areaInfo.MinEncLv != 0 ? areaInfo.MinEncLv : 1; - var areaMax = areaInfo.MaxEncLv != 0 ? areaInfo.MaxEncLv : 100; - if (!(ep.LevelRange.X <= areaMax && areaMin <= ep.LevelRange.Y)) - continue; - if (!scene.IsPointContained(areaName, ep.Position.X, ep.Position.Y, ep.Position.Z)) - continue; - var composite = GetCompositePoint(ep, areaMin, areaMax); - areaEncPoints.Add(composite); - } - - return areaEncPoints; - } - - private static List GetFixedSymbols(PaldeaSceneModel scene, PaldeaFixedSymbolModel fsym, FixedSymbolTableArray fsymTable, string areaName) - { - var symbols = new List(); - var added = new HashSet(); - // add Scarlet - foreach (var point in fsym.scarletPoints.Concat(fsym.violetPoints)) - { - if (!added.Contains(point.TableKey) && scene.IsPointContained(areaName, point.Position.X, point.Position.Y, point.Position.Z)) - { - added.Add(point.TableKey); - symbols.Add(GetPokeDataSymbol(fsymTable, point.TableKey)); - } - } - - return symbols; - } - - private static PokeDataSymbol GetPokeDataSymbol(FixedSymbolTableArray fsymTable, string tableKey) - { - foreach (var entry in fsymTable.Table) - { - if (entry.TableKey == tableKey) - return entry.PokeDataSymbol; - } - throw new ArgumentException($"TableKey not found ({tableKey})"); - } - - private static PointData GetCompositePoint(PointData ep, int areaMin, int areaMax) - { - var newX = Math.Max(ep.LevelRange.X, areaMin); - var newY = Math.Min(ep.LevelRange.Y, areaMax); - var composite = new PointData - { - Position = ep.Position, - LevelRange = new PackedVec2f { X = newX, Y = newY }, - Biome = ep.Biome, - Substance = ep.Substance, - AreaNo = ep.AreaNo, - }; - return composite; - } - - private static void WriteLocation(TextWriter tw, IReadOnlyList specNames, - IReadOnlyDictionary placeNameMap, - string areaName, AreaInfo areaInfo, List encounts) - { - var loc = areaInfo.LocationNameMain; - (string name, int index) = placeNameMap[loc]; - var heading = $"{areaName} - {loc} - {name} ({index})"; - WriteEncounts(tw, specNames, heading, encounts); - } - - private static void WriteLocEncList(TextWriter tw, IReadOnlyList specNames, - IReadOnlyDictionary placeNameMap, - IReadOnlyDictionary> locIdEncs) - { - foreach (var place in placeNameMap.Keys.OrderBy(p => placeNameMap[p].Index)) - { - (string name, int index) = placeNameMap[place]; - if (!locIdEncs.TryGetValue(index, out var encounts)) - continue; - - var heading = $"{name} ({index})"; - WriteEncounts(tw, specNames, heading, encounts); - } - } - - private static void WriteEncounts(TextWriter tw, IReadOnlyList specNames, string heading, List encounts) - { - tw.WriteLine("==="); - tw.WriteLine(heading); - tw.WriteLine("==="); - foreach (var e in encounts) - tw.WriteLine($" - {e.GetEncountString(specNames)}"); - tw.WriteLine(); - } - - public static Dictionary GetPlaceNameMap(string[] text, AHTB ahtb) - { - var result = new Dictionary(); - for (var i = 0; i < text.Length; i++) - result[ahtb.Entries[i].Name] = (text[i], i); - return result; - } -} - -public record PaldeaEncounter(int Species, int Form, int Sex, int MinLevel, int MaxLevel) : IComparable -{ - public int MinLevel { get; private set; } = MinLevel; - public int MaxLevel { get; private set; } = MaxLevel; - - public int Gender => Sex switch - { - 1 => 0, - 2 => 1, - _ => -1, - }; - - - public static PaldeaEncounter GetNew(EncountPokeData pd, PointData ep) - { - var min = (int)Math.Max(ep.LevelRange.X, pd.MinLevel); - var max = (int)Math.Min(ep.LevelRange.Y, pd.MaxLevel); - return new((int)pd.DevId, pd.Form, (int)pd.Sex, min, max); - } - - public static PaldeaEncounter GetBand(EncountPokeData pd, PointData ep) - { - var min = (int)Math.Max(ep.LevelRange.X, pd.MinLevel); - var max = (int)Math.Min(ep.LevelRange.Y, pd.MaxLevel); - return new((int)pd.BandPoke, pd.BandForm, (int)pd.BandSex, min, max); - } - - public string GetEncountString(IReadOnlyList specNames) - { - var species = specNames[Species]; - return GetString(species); - } - - public override string ToString() - { - return GetString(((PKHeX.Core.Species)Species).ToString()); - } - - private string GetString(string species) - { - var form = Form == 0 ? "" : $"-{Form}"; - var sex = Sex == 0 ? "" : $" (sex={Sex})"; - return $"{species}{form}{sex} Lv. {MinLevel}-{MaxLevel}"; - } - - public bool Absorb(PaldeaEncounter other) - { - if (other.MinLevel == MinLevel && other.MaxLevel == MaxLevel) - return true; - - if (!IsLevelRangeOverlap(other) && !other.IsLevelRangeOverlap(this)) - return false; - - MinLevel = Math.Min(MinLevel, other.MinLevel); - MaxLevel = Math.Max(MaxLevel, other.MaxLevel); - return true; - } - - public bool IsSameSpecFormGender(PaldeaEncounter other) - { - if (Species != other.Species || Form != other.Form) - return false; - if (Sex != other.Sex) - return false; - return true; - } - - private bool IsLevelRangeOverlap(PaldeaEncounter other) - { - // If our level range overlaps with the other (with +/- 1 tolerance), return true. - return MaxLevel + 1 >= other.MinLevel && MinLevel + 1 <= other.MaxLevel; - } - - public int CompareTo(PaldeaEncounter? other) - { - if (ReferenceEquals(this, other)) return 0; - if (ReferenceEquals(null, other)) return 1; - int speciesComparison = Species.CompareTo(other.Species); - if (speciesComparison != 0) return speciesComparison; - int formComparison = Form.CompareTo(other.Form); - if (formComparison != 0) return formComparison; - int sexComparison = Sex.CompareTo(other.Sex); - if (sexComparison != 0) return sexComparison; - int minLevelComparison = MinLevel.CompareTo(other.MinLevel); - if (minLevelComparison != 0) return minLevelComparison; - return MaxLevel.CompareTo(other.MaxLevel); - } - - public ulong GetHash() - { - ulong result = (ulong)Species; - result = (result << 16) | (byte)Form; - result = (result << 8) | (byte)Sex; - result = (result << 8) | (byte)MinLevel; - result = (result << 8) | (byte)MaxLevel; - return result; - } -} - -public class PaldeaFixedSymbolPoint -{ - public string TableKey; - public PackedVec3f Position; - - public PaldeaFixedSymbolPoint(string key, PackedVec3f pos) - { - TableKey = key; - Position = new PackedVec3f - { - X = pos.X, - Y = pos.Y, - Z = pos.Z, - }; - } -} -public class PaldeaCoinSymbolPoint -{ - public string Name; - public ulong FirstNum; - public string BoxLabel; - public PackedVec3f Position; - - public PaldeaCoinSymbolPoint(string name, ulong num, string boxLabel, PackedVec3f pos) - { - Name = name; - FirstNum = num; - BoxLabel = boxLabel; - Position = new PackedVec3f - { - X = pos.X, - Y = pos.Y, - Z = pos.Z, - }; - } - - public bool IsBox => !string.IsNullOrEmpty(BoxLabel); -} - -public class PaldeaSceneModel -{ - public readonly List areaNames; - public readonly Dictionary AreaInfos; - public readonly Dictionary areaColTrees; - public readonly Dictionary areaColBoxes; - public readonly Dictionary isAtlantis; - - public PaldeaSceneModel(IFileInternal ROM, PaldeaFieldModel field) - { - // NOTE: Safe to only use _0 because _1 is identical. - var area_management = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/scene/parts/field/field_system/area_management_/area_management_0.trscn")); - Debug.Assert(area_management.ObjectTemplateName == "area_management"); - - // NOTE: Safe to only use _0 because _1 is identical. - var a_w23_field_area_col = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/scene/parts/field/room/a_w23_field/a_w23_field_area_col_/a_w23_field_area_col_0.trscn")); - Debug.Assert(a_w23_field_area_col.ObjectTemplateName == "a_w23_field_area_col"); - - areaNames = new List(); - AreaInfos = new Dictionary(); - areaColTrees = new Dictionary(); - areaColBoxes = new Dictionary(); - isAtlantis = new Dictionary(); - - void AddSceneObject(string name, TrinitySceneObjectSV sceneObject, TrinityCollisionComponent1SV collisionComponent) - { - areaNames.Add(name); - AreaInfos[name] = field.FindAreaInfo(name); - - Debug.Assert(collisionComponent.CollisionShape.Discriminator is 2 or 4); - - if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeBoxSV? box)) - { - // Box collision, obj.ObjectPosition.Field_02 is pos, box.Field_01 is size of box - areaColBoxes[name] = new BoxCollision9 - { - Position = sceneObject.ObjectPosition.Field_02, - Size = box.Field_01, - }; - } - else if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeHavokSV? havok)) - { - var havokData = ROM.GetPackedFile(havok.TrcolFilePath); - areaColTrees[name] = HavokCollision.ParseAABBTree(havokData); - } - } - - foreach (var obj in area_management.Objects.Concat(a_w23_field_area_col.Objects)) - { - var isAtlantisObj = a_w23_field_area_col.Objects.Contains(obj); - if (!(obj.SubObjects.Length > 0 && obj.SubObjects[0].Type == "trinity_CollisionComponent")) - continue; - var collisionComponent = FlatBufferConverter.DeserializeFrom(obj.SubObjects[0].Data).Component.Item1; - - switch (obj.Type) - { - case "trinity_ObjectTemplate": - { - var sObj = FlatBufferConverter.DeserializeFrom(obj.Data); - if (sObj.Type != "trinity_SceneObject") - continue; - var sceneObject = FlatBufferConverter.DeserializeFrom(sObj.Data); - Debug.Assert(sceneObject.ObjectName == sObj.ObjectTemplateExtra); - - AddSceneObject(sObj.ObjectTemplateName, sceneObject, collisionComponent); - isAtlantis[sObj.ObjectTemplateName] = isAtlantisObj; - break; - } - case "trinity_SceneObject": - { - var sceneObject = FlatBufferConverter.DeserializeFrom(obj.Data); - AddSceneObject(sceneObject.ObjectName, sceneObject, collisionComponent); - isAtlantis[sceneObject.ObjectName] = isAtlantisObj; - break; - } - } - } - } - - public bool IsPointContained(string areaName, float x, float y, float z) - { - if (areaColTrees.TryGetValue(areaName, out var tree)) - return tree.ContainsPoint(x, y, z); - if (areaColBoxes.TryGetValue(areaName, out var box)) - return box.ContainsPoint(x, y, z); - return false; - } -} - -public class PaldeaFieldModel -{ - private readonly FieldMainArea[] mainAreas; - private readonly FieldSubArea[] subAreas; - private readonly FieldInsideArea[] insideAreas; - private readonly FieldDungeonArea[] dungeonAreas; - private readonly FieldLocation[] fieldLocations; - - public PaldeaFieldModel(IFileInternal ROM) - { - mainAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_main_area/field_main_area_array.bin")).Table; - subAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_sub_area/field_sub_area_array.bin")).Table; - insideAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_inside_area/field_inside_area_array.bin")).Table; - dungeonAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_dungeon_area/field_dungeon_area_array.bin")).Table; - fieldLocations = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_location/field_location_array.bin")).Table; - } - - public AreaInfo FindAreaInfo(string name) - { - foreach (var area in mainAreas) - { - if (area.Name == name) - return area.AreaInfo; - } - foreach (var area in subAreas) - { - if (area.Name == name) - return area.AreaInfo; - } - foreach (var area in insideAreas) - { - if (area.Name == name) - return area.AreaInfo; - } - foreach (var area in dungeonAreas) - { - if (area.Name == name) - return area.AreaInfo; - } - foreach (var area in fieldLocations) - { - if (area.Name == name) - return area.AreaInfo; - } - throw new ArgumentException($"Unknown area {name}"); - } -} - -public class PaldeaFixedSymbolModel -{ - public readonly List scarletPoints; - public readonly List violetPoints; - - public PaldeaFixedSymbolModel(IFileInternal ROM) - { - scarletPoints = new List(); - violetPoints = new List(); - - var p0Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_fixed_placement_symbol_/world_fixed_placement_symbol_0.trscn"); - var p1Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_fixed_placement_symbol_/world_fixed_placement_symbol_1.trscn"); - - var p0 = FlatBufferConverter.DeserializeFrom(p0Data); - var p1 = FlatBufferConverter.DeserializeFrom(p1Data); - - scarletPoints.AddRange(GetObjectTemplateSymbolPoints(p0)); - violetPoints.AddRange(GetObjectTemplateSymbolPoints(p1)); - } - - private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template) - { - foreach (var obj in template.Objects) - { - switch (obj.Type) - { - case "trinity_ObjectTemplate": - { - var sObj = FlatBufferConverter.DeserializeFrom(obj.Data); - if (sObj.Type != "trinity_ScenePoint") - continue; - var scenePoint = FlatBufferConverter.DeserializeFrom(sObj.Data); - - foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) - yield return f; - break; - } - case "trinity_ScenePoint": - { - var scenePoint = FlatBufferConverter.DeserializeFrom(obj.Data); - - foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) - yield return f; - break; - } - } - } - } - - private IEnumerable GetScenePointSymbolPoints(TrinityScenePointSV scenePoint, TrinitySceneObjectTemplateEntrySV[] subObjects) - { - // Handle SubObjects - for (var i = 0; i < subObjects.Length; i++) - { - var sobj = subObjects[i]; - switch (sobj.Type) - { - case "trinity_PropertySheet": - { - var propSheet = FlatBufferConverter.DeserializeFrom(sobj.Data); - if (propSheet.Name == "fixed_symbol_point") - { - var tableKey = GetTableKey(propSheet); - if (!string.IsNullOrEmpty(tableKey)) - { - yield return new PaldeaFixedSymbolPoint(tableKey, scenePoint.Position); - } - } - break; - } - case "trinity_ScenePoint": - { - var subScenePoint = FlatBufferConverter.DeserializeFrom(sobj.Data); - foreach (var f in GetScenePointSymbolPoints(subScenePoint, sobj.SubObjects)) - yield return f; - break; - } - default: - throw new ArgumentException($"Unknown SubObject {sobj.Type}"); - } - } - } - - public static string GetTableKey(TrinityPropertySheetSV propSheet) - { - if (propSheet.Name != "fixed_symbol_point") - throw new ArgumentException($"Invalid PropertySheet {propSheet.Name}"); - - if (propSheet.Properties[0].Fields[1].Name != "tableKey") - throw new ArgumentException("Invalid PropertySheet field layout"); - - if (!propSheet.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv)) - throw new ArgumentException("Could not get PropertySheet Table Key"); - - return sv.Value; - } -} - -public class PaldeaCoinSymbolModel -{ - public readonly List Points; - - public PaldeaCoinSymbolModel(IFileInternal ROM) - { - Points = new List(); - - var cData = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_coin_placement_symbol_/world_coin_placement_symbol_0.trscn"); - // NOTE: Fine to only use Scarlet, Violet data is identical. - - var c = FlatBufferConverter.DeserializeFrom(cData); - - Points.AddRange(GetObjectTemplateSymbolPoints(c)); - } - - private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template) - { - foreach (var obj in template.Objects) - { - switch (obj.Type) - { - case "trinity_ScenePoint": - { - var scenePoint = FlatBufferConverter.DeserializeFrom(obj.Data); - - if (obj.SubObjects.Length != 1) - throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject Count {obj.SubObjects.Length}"); - - if (obj.SubObjects[0].Type != "trinity_PropertySheet") - throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject ({obj.SubObjects[0].Type})"); - - var propSheet = FlatBufferConverter.DeserializeFrom(obj.SubObjects[0].Data); - - yield return ParseCoinSymbolPoint(scenePoint, propSheet); - break; - } - default: - throw new ArgumentException($"Unsupported CoinPlacement Object Type {obj.Type}"); - } - } - } - - private PaldeaCoinSymbolPoint ParseCoinSymbolPoint(TrinityScenePointSV sp, TrinityPropertySheetSV ps) - { - switch (ps.Name) - { - case "coin_walk_symbol_point": - return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), string.Empty, sp.Position); - case "coin_box_symbol_point": - return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), GetBoxLabel(ps), sp.Position); - default: - throw new ArgumentException($"Unknown CoinSymbol PropertySheet {ps.Name}"); - } - } - - private ulong GetFirstNum(TrinityPropertySheetSV ps) - { - if (ps.Properties[0].Fields[0].Name != "firstNum") - throw new ArgumentException("Invalid PropertySheet field layout"); - - if (!ps.Properties[0].Fields[0].Data.TryGet(out TrinityPropertySheetField1SV? sv)) - throw new ArgumentException("Could not get PropertySheet Table Key"); - - return sv.Value; - } - - private string GetBoxLabel(TrinityPropertySheetSV ps) - { - if (ps.Properties[0].Fields[1].Name != "label") - throw new ArgumentException("Invalid PropertySheet field layout"); - - if (!ps.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv)) - throw new ArgumentException("Could not get PropertySheet Table Key"); - - return sv.Value; - } -} diff --git a/pkNX.WinForms/Dumping/GameDumperSV.cs b/pkNX.WinForms/Dumping/GameDumperSV.cs index f7286f01..c982c324 100644 --- a/pkNX.WinForms/Dumping/GameDumperSV.cs +++ b/pkNX.WinForms/Dumping/GameDumperSV.cs @@ -353,14 +353,19 @@ static byte[] GetPickle(PersonalInfo9SV e) { if (!e.IsPresentInGame) return Array.Empty(); - return Write(e.FB.Evolutions); + return Write(e.FB.Info.SpeciesCopy, e.FB.Evolutions); } - static byte[] Write(PersonalInfo9SVEvolutions[] evos) + static byte[] Write(int species, PersonalInfo9SVEvolutions[] evos) { using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms); - foreach (var m in evos) // just in case + + var list = evos.ToArray(); + if (species == (int)Species.Crabrawler) + Array.Reverse(list); // put the levelup evo last. + + foreach (var m in list) // just in case { var method = ((EvolutionType)m.Method); if (method == EvolutionType.Hisui) @@ -533,6 +538,7 @@ private void DumpAjito() private void DumpEncount() { + Dump("world/data/event/treeshake/treeshake_pokemon/treeshake_pokemon_array.bfbs"); Dump("world/data/encount/point_data/point_data/encount_data_atlantis.bfbs"); Dump("world/data/encount/point_data/point_data/encount_data_100000.bfbs"); Dump("world/data/encount/pokedata/pokedata/pokedata_array.bfbs"); @@ -907,6 +913,7 @@ public void DumpSpecific() "world/data/encount/setting/setting/data.bfbs", "world/data/encount/setting/raid_difficulty_lottery/raid_difficulty_lottery_array.bfbs", "world/data/encount/setting/raid_gem_setting/raid_gem_setting.bfbs", + "world/data/event/treeshake/treeshake_pokemon/treeshake_pokemon_array.bfbs", // Field "world/data/field/area/field_dungeon_area/field_dungeon_area_array.bfbs", "world/data/field/area/field_inside_area/field_inside_area_array.bfbs", diff --git a/pkNX.WinForms/Dumping/SV/BoxCollision9.cs b/pkNX.WinForms/Dumping/SV/BoxCollision9.cs new file mode 100644 index 00000000..ac628835 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/BoxCollision9.cs @@ -0,0 +1,31 @@ +namespace pkNX.Structures.FlatBuffers; + +public class BoxCollision9 : IContainsV3f +{ + public PackedVec3f Position { get; init; } + public PackedVec3f Size { get; init; } + + public bool ContainsPoint(float x, float y, float z) => ContainsPoint(x, y, z, 0, 0, 0); + + public bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ) + { + var box_lx = Position.X - (Size.X / 2.0f) - toleranceX; + var box_hx = Position.X + (Size.X / 2.0f) + toleranceX; + if (box_lx > box_hx || !(box_lx <= x && x <= box_hx)) + return false; + + var box_lz = Position.Z - (Size.Z / 2.0f) - toleranceZ; + var box_hz = Position.Z + (Size.Z / 2.0f) + toleranceZ; + if (box_lz > box_hz || !(box_lz <= z && z <= box_hz)) + return false; + + var ly = y - 10000; + var hy = y + 1; + var box_ly = Position.Y - (Size.Y / 2.0f) - toleranceY; + var box_hy = Position.Y + (Size.Y / 2.0f) + toleranceY; + if (box_ly > box_hy || !(box_ly <= hy && ly <= box_hy)) + return false; + + return true; + } +} diff --git a/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs b/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs new file mode 100644 index 00000000..62713820 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs @@ -0,0 +1,520 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers; + +namespace pkNX.WinForms; + +public class EncounterDumperSV +{ + private readonly IFileInternal ROM; + private const float tolX = 30f; + private const float tolY = 30f; + private const float tolZ = 30f; + + public EncounterDumperSV(IFileInternal rom) => ROM = rom; + + public void DumpTo(string path, IReadOnlyList specNames, IReadOnlyList moveNames, + Dictionary placeNameMap, + bool writeText = true, bool writePickle = true) + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + + var field = new PaldeaFieldModel(ROM); + var scene = new PaldeaSceneModel(ROM, field); + var fsym = new PaldeaFixedSymbolModel(ROM); + var csym = new PaldeaCoinSymbolModel(ROM); + var mlEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_100000.bin")); + var alEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_atlantis.bin")); + var pokeData = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin")); + + var db = new LocationDatabase(); + + // Overall Logic Flow: + // 1 - At every point, spawn everything that can exist at that point. + // 2 - At each area, absorb native points into local points. + // 3 - At each area, absorb crossover points into crossover points. + // 4 - Consolidate the encounters from both point lists. + // Just compute everything (big memory!) then crunch it all down. + + // Points can be used by multiple areas as crossover sources. Need to be able to "belong" to multiple areas, and indicate their parent area. + var pointMain = ReformatPoints(mlEncPoints); + var pointAtlantis = ReformatPoints(alEncPoints); + + // Fill the point lists for each area, then spawn everything into those points. + foreach (var areaName in scene.areaNames) + { + var areaInfo = scene.AreaInfos[areaName]; + + // Determine potential spawners + if (!scene.TryGetContainsCheck(areaName, out var collider)) + { + Console.WriteLine($"No collider for {areaName}"); + continue; + } + + // Locations that do not spawn encounters can still have crossovers bleed into them. + // We'll have empty local encounter lists for them. + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) + continue; + var storage = db.Get(placeNameMap[name].Index, areaName, areaInfo); + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + var points = scene.isAtlantis[areaName] ? pointAtlantis : pointMain; + storage.LoadPoints(points, collider, areaInfo.ActualMinLevel, areaInfo.ActualMaxLevel); + storage.GetEncounters(pokeData, scene); + } + + // For each area, we need to peek at the other areas to see if they have any crossover points. + // For each of those crossover points, we need to see if they are in the current area's collider. + // If they are, we need to add them to the current area's list of crossover points. + foreach (var areaName in scene.areaNames) + { + // Same sanity checking as above iteration. + var areaInfo = scene.AreaInfos[areaName]; + + // Determine potential spawners + if (!scene.TryGetContainsCheck(areaName, out var collider)) + { + Console.WriteLine($"No collider for {areaName}"); + continue; + } + + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) + continue; + //if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + // continue; + + var storage = db.Get(placeNameMap[name].Index, areaName, areaInfo); + // Here's where the fun begins. Iterate over areas inside this loop so we can look for all possible adjacent areas. + foreach (var otherName in scene.areaNames) + { + // Skip self + if (otherName == areaName) + continue; + // Skip areas that don't have a location name + var otherAreaInfo = scene.AreaInfos[otherName]; + var otherNameMain = otherAreaInfo.LocationNameMain; + if (string.IsNullOrEmpty(otherNameMain)) + continue; + + // Skip areas that don't have a collider + if (!scene.TryGetContainsCheck(otherName, out _)) + continue; + + // Iterate over all crossover points in the other area. + var cross = db.Get(placeNameMap[otherNameMain].Index, otherName, otherAreaInfo); + foreach (var point in cross.Local) + { + // If the crossover point is close enough to the current area's collider, add it to the current area's list of crossover points. + if (collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ)) + storage.Nearby.Add(point); + } + } + } + + // Each area and their local / crossover points have been aggregated. + // Integrate the points' slots into a single list, and consolidate entries with same level ranges to as few objects as possible. + foreach (var storage in db.Locations.Values) + { + // Consolidate encounters + storage.Integrate(); + storage.Consolidate(); + } + + // Output to stream if available. + if (writeText) + { + using var sw = File.CreateText(Path.Combine(path, "titan_enc.txt")); + foreach (var s in db.Locations.Values) + WriteLocation(sw, specNames, placeNameMap, s.AreaName, s.AreaInfo, s.Slots); + using var tw = File.CreateText(Path.Combine(path, "titan_loc_enc.txt")); + WriteLocEncList(tw, specNames, placeNameMap, db); + + using var pw = File.CreateText(Path.Combine(path, "titan_loc_point.txt")); + WriteLocPointList(pw, placeNameMap, db); + } + if (writePickle) + { + string binPath = Path.Combine(path, "encounter_wild_paldea.pkl"); + SerializeEncounters(binPath, db); + } + + // Fixed symbols + List serialized = new(); + int[] bannedIndexes = + { + 31, // Lighthouse Wingull + }; + var fsymData = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/fixed_symbol/fixed_symbol_table/fixed_symbol_table_array.bin")); + var eventBattle = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin")); + foreach (var (game, gamePoints) in new[] { ("sl", fsym.scarletPoints), ("vl", fsym.violetPoints)}) + { + using var gw = File.CreateText(Path.Combine(path, $"titan_fixed_{game}.txt")); + for (var i = 0; i < fsymData.Table.Length; i++) + { + FixedSymbolTable? entry = fsymData.Table[i]; + var tableKey = entry.TableKey; + + var points = gamePoints.Where(p => p.TableKey == tableKey).ToList(); + if (points.Count == 0) + continue; + + var areas = new List(); + foreach (var areaName in scene.areaNames) + { + if (scene.isAtlantis[areaName]) + continue; + + var areaInfo = scene.AreaInfos[areaName]; + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) + continue; + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + if (points.Any(p => scene.IsPointContained(areaName, p.Position.X, p.Position.Y, p.Position.Z))) + areas.Add(areaName); + } + + var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList(); + + gw.WriteLine("==="); + gw.WriteLine(entry.TableKey); + gw.WriteLine("==="); + gw.WriteLine(" PokeData:"); + var pd = entry.PokeDataSymbol; + gw.WriteLine($" Species: {specNames[(int)pd.DevId]}"); + gw.WriteLine($" Form: {pd.FormId}"); + gw.WriteLine($" Level: {pd.Level}"); + gw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}"); + gw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}"); + + var talentStr = pd.TalentType switch + { + TalentType.RANDOM => "Random", + TalentType.V_NUM => $"{pd.TalentVNum} Perfect", + TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}", + _ => "Invalid", + }; + gw.WriteLine($" IVs: {talentStr}"); + gw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.TokuseiIndex]}"); + switch (pd.WazaType) + { + case WazaType.DEFAULT: + gw.WriteLine(" Moves: Random"); + break; + case WazaType.MANUAL: + gw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}"); + break; + } + + gw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}"); + gw.WriteLine($" GemType: {(int)pd.GemType}"); + + gw.WriteLine(" Points:"); + foreach (var point in points) + { + gw.WriteLine($" - ({point.Position.X}, {point.Position.Y}, {point.Position.Z})"); + } + gw.WriteLine(" Areas:"); + foreach (var areaName in areas) + { + var areaInfo = scene.AreaInfos[areaName]; + var loc = areaInfo.LocationNameMain; + (string name, int index) = placeNameMap[loc]; + gw.WriteLine($" - {areaName} - {loc} - {name} ({index})"); + } + + // Serialize + if (locs.Count == 0) + continue; + if (bannedIndexes.Contains(i)) + continue; + + areas.Clear(); + foreach (var areaName in scene.areaNames) + { + if (scene.isAtlantis[areaName]) + continue; + + var areaInfo = scene.AreaInfos[areaName]; + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) + continue; + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + if (!scene.TryGetContainsCheck(areaName, out var collider)) + continue; + + if (points.Any(p => collider.ContainsPoint(p.Position.X, p.Position.Y, p.Position.Z, tolX, tolY, tolZ))) + areas.Add(areaName); + } + + locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList(); + WriteFixedSymbol(serialized, entry, locs); + } + } + + using var cw = File.CreateText(Path.Combine(path, "titan_coin_symbol.txt")); + foreach (var entry in csym.Points) + { + var areas = new List(); + foreach (var areaName in scene.areaNames) + { + if (scene.isAtlantis[areaName]) + continue; + + var areaInfo = scene.AreaInfos[areaName]; + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) + continue; + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + if (scene.IsPointContained(areaName, entry.Position.X, entry.Position.Y, entry.Position.Z)) + areas.Add(areaName); + } + + var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList(); + + cw.WriteLine("==="); + cw.WriteLine(entry.Name); + cw.WriteLine("==="); + cw.WriteLine($" First Num: {entry.FirstNum}"); + cw.WriteLine($" Coordinates: ({entry.Position.X}, {entry.Position.Y}, {entry.Position.Z})"); + + if (entry.IsBox) + { + cw.WriteLine($" Box Label: {entry.BoxLabel}"); + cw.WriteLine(" PokeData:"); + var pd = Array.Find(eventBattle.Table, e => e.Label == entry.BoxLabel)!.PokeData; + + cw.WriteLine($" Species: {specNames[(int)pd.DevId]}"); + cw.WriteLine($" Form: {pd.FormId}"); + cw.WriteLine($" Level: {pd.Level}"); + cw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}"); + cw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}"); + + var talentStr = pd.TalentType switch + { + TalentType.RANDOM => "Random", + TalentType.V_NUM => $"{pd.TalentVnum} Perfect", + TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}", + _ => "Invalid", + }; + cw.WriteLine($" IVs: {talentStr}"); + cw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.Tokusei]}"); + switch (pd.WazaType) + { + case WazaType.DEFAULT: + cw.WriteLine($" Moves: Random"); + break; + case WazaType.MANUAL: + cw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}"); + break; + } + + cw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}"); + cw.WriteLine($" GemType: {(int)pd.GemType}"); + } + + cw.WriteLine(" Areas:"); + foreach (var areaName in areas) + { + var areaInfo = scene.AreaInfos[areaName]; + var loc = areaInfo.LocationNameMain; + (string name, int index) = placeNameMap[loc]; + cw.WriteLine($" - {areaName} - {loc} - {name} ({index})"); + } + } + var pathPickle = Path.Combine(path, "encounter_fixed_paldea.pkl"); + var ordered = serialized + .OrderBy(z => BinaryPrimitives.ReadUInt16LittleEndian(z)) // Species + .ThenBy(z => z[2]) // Form + .ThenBy(z => z[3]) // Level + ; + File.WriteAllBytes(pathPickle, ordered.SelectMany(z => z).ToArray()); + } + + private static LocationPointDetail[] ReformatPoints(PointDataArray all) + { + var arr = all.Table; + var result = new LocationPointDetail[arr.Length]; + for (int i = 0; i < arr.Length; i++) + result[i] = new LocationPointDetail(arr[i]); + return result; + } + + private static void WriteFixedSymbol(ICollection exist, FixedSymbolTable entry, IReadOnlyList locs) + { + var enc = entry.PokeDataSymbol; + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + + bw.Write((ushort)enc.DevId); + bw.Write((byte)enc.FormId); + bw.Write((byte)enc.Level); + + bw.Write((byte)enc.TalentVNum); + bw.Write((byte)enc.GemType); + bw.Write((byte)(enc.Sex - 1)); + bw.Write((byte)0); // reserved + + bw.Write((ushort)enc.Waza1.WazaId); + bw.Write((ushort)enc.Waza2.WazaId); + + bw.Write((ushort)enc.Waza3.WazaId); + bw.Write((ushort)enc.Waza4.WazaId); + + // At most 3 locations, but just use 4 bytes. + Span temp = stackalloc byte[4]; + if (locs.Count > temp.Length) + throw new ArgumentException("Too many locations??", nameof(locs)); + for (int i = 0; i < locs.Count; i++) + temp[i] = (byte)locs[i]; + bw.Write(temp); + + var result = ms.ToArray(); + if (!exist.Any(x => x.SequenceEqual(result))) + exist.Add(result); + } + + private static void SerializeEncounters(string binPath, LocationDatabase db) + { + var tables = db.Locations.Values; + int ctr = 0; + var result = new byte[tables.Count + tables.Sum(z => z.SlotsCrossover.Count)][]; + foreach (var x in tables) + { + result[ctr++] = SerializeLocationSet((ushort)x.Location, 0, x.Slots); + foreach (var sub in x.SlotsCrossover) + result[ctr++] = SerializeLocationSet((ushort)x.Location, (ushort)sub.Key, sub.Value.Slots); + } + var mini = MiniUtil.PackMini(result, "sv"); + File.WriteAllBytes(binPath, mini); + } + + private static byte[] SerializeLocationSet(ushort loc, ushort crossover, IReadOnlyList slots) + { + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + bw.Write(loc); + bw.Write(crossover); + foreach (var slot in slots) + { + bw.Write((ushort)slot.Species); + bw.Write((byte)slot.Form); + bw.Write((byte)slot.Gender); + + bw.Write((byte)slot.MinLevel); + bw.Write((byte)slot.MaxLevel); + bw.Write((byte)slot.Time); + bw.Write((byte)0); + } + return ms.ToArray(); + } + + private static PokeDataSymbol GetPokeDataSymbol(FixedSymbolTableArray fsymTable, string tableKey) + { + foreach (var entry in fsymTable.Table) + { + if (entry.TableKey == tableKey) + return entry.PokeDataSymbol; + } + throw new ArgumentException($"TableKey not found ({tableKey})"); + } + + private static void WriteLocation(TextWriter tw, IReadOnlyList specNames, + IReadOnlyDictionary placeNameMap, + string areaName, AreaInfo areaInfo, List encounts) + { + var loc = areaInfo.LocationNameMain; + (string name, int index) = placeNameMap[loc]; + var heading = $"{areaName} - {loc} - {name} ({index})"; + WriteEncounts(tw, specNames, heading, encounts); + } + + private static void WriteLocEncList(TextWriter tw, IReadOnlyList specNames, + IReadOnlyDictionary placeNameMap, + LocationDatabase db) + { + foreach (var place in placeNameMap.Keys.OrderBy(p => placeNameMap[p].Index)) + { + (string name, int index) = placeNameMap[place]; + if (!db.Locations.TryGetValue(index, out var encounts)) + continue; + + var heading = $"{name} ({index})"; + WriteEncounts(tw, specNames, heading, encounts.Slots); + } + } + + private static void WriteLocPointList(TextWriter tw, + IReadOnlyDictionary placeNameMap, + LocationDatabase db) + { + foreach (var place in placeNameMap.Keys.OrderBy(p => placeNameMap[p].Index)) + { + (string name, int index) = placeNameMap[place]; + if (!db.Locations.TryGetValue(index, out var loc)) + continue; + + var heading = $"{name} ({index})"; + WriteBiomes(tw, heading, loc.Local); + } + + foreach (var place in placeNameMap.Keys.OrderBy(p => placeNameMap[p].Index)) + { + (string name, int index) = placeNameMap[place]; + if (!db.Locations.TryGetValue(index, out var loc)) + continue; + + var heading = $"{name} ({index})"; + WritePoints(tw, heading, loc.Local); + } + } + + private static void WriteBiomes(TextWriter tw, string heading, List points) + { + var biomes = points.Select(z => z.Point.Biome).Distinct().Select(z => z.ToString()).OrderBy(z => z); + var btext = string.Join(',', biomes); + tw.WriteLine($"{heading}\t{btext}"); + } + + private static void WritePoints(TextWriter tw, string heading, List points) + { + tw.WriteLine("==="); + tw.WriteLine(heading); + tw.WriteLine("==="); + foreach (var e in points) + tw.WriteLine($" - {e.GetString()}"); + tw.WriteLine(); + } + + private static void WriteEncounts(TextWriter tw, IReadOnlyList specNames, string heading, IEnumerable encounts) + { + tw.WriteLine("==="); + tw.WriteLine(heading); + tw.WriteLine("==="); + foreach (var e in encounts) + tw.WriteLine($" - {e.GetEncountString(specNames)}"); + tw.WriteLine(); + } + + public static Dictionary GetPlaceNameMap(string[] text, AHTB ahtb) + { + var result = new Dictionary(); + for (var i = 0; i < text.Length; i++) + result[ahtb.Entries[i].Name] = (text[i], i); + return result; + } +} diff --git a/pkNX.WinForms/Dumping/SV/LocationDatabase.cs b/pkNX.WinForms/Dumping/SV/LocationDatabase.cs new file mode 100644 index 00000000..5f98324c --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/LocationDatabase.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace pkNX.Structures.FlatBuffers; + +public class LocationDatabase +{ + public readonly Dictionary Locations = new(); + + public LocationStorage Get(int location, string areaName, AreaInfo areaInfo) + { + if (!Locations.TryGetValue(location, out var loc)) + Locations[location] = loc = new LocationStorage(location, areaName, areaInfo); + return loc; + } +} diff --git a/pkNX.WinForms/Dumping/SV/LocationPointDetail.cs b/pkNX.WinForms/Dumping/SV/LocationPointDetail.cs new file mode 100644 index 00000000..3b060ea2 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/LocationPointDetail.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace pkNX.Structures.FlatBuffers; + +public class LocationPointDetail +{ + public readonly PointData Point; + + public LocationPointDetail(PointData point) => Point = point; + public float X => Point.Position.X; + public float Y => Point.Position.Y; + public float Z => Point.Position.Z; + public int Location { get; init; } + + public bool IsWithinLevelRange(int areaMin, int areaMax) + { + return Point.LevelRange.X <= areaMax && areaMin <= Point.LevelRange.Y; + } + + public readonly List Slots = new(); + + public void Add(PaldeaEncounter slot) => Slots.Add(slot); + + public string GetString() + { + return $"{Location:0000} ({X:0.0000},{Y:0.0000},{Z:0.0000}) {Point.Biome} ({Point.LevelRange.X}-{Point.LevelRange.Y}) {Slots.Count:00}"; + } +} diff --git a/pkNX.WinForms/Dumping/SV/LocationStorage.cs b/pkNX.WinForms/Dumping/SV/LocationStorage.cs new file mode 100644 index 00000000..fd22bbfb --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/LocationStorage.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; + +namespace pkNX.Structures.FlatBuffers; + +public class LocationStorage +{ + public readonly int Location; + public readonly AreaInfo AreaInfo; + public readonly string AreaName; + + public readonly HashSet Added = new(); + public readonly List Slots = new(); + public readonly Dictionary SlotsCrossover = new(); + public readonly List Local = new(); + public readonly List Nearby = new(); + + public LocationStorage(int loc, string areaName, AreaInfo info) + { + Location = loc; + AreaName = areaName; + AreaInfo = info; + } + + private void Add(PaldeaEncounter slot) + { + var key = slot.GetHash(); + if (Added.Add(key)) + Slots.Add(slot); + } + + private void AddCrossover(PaldeaEncounter slot, int loc) + { + if (!SlotsCrossover.TryGetValue(loc, out var s)) + SlotsCrossover[loc] = s = new LocationStorage(loc, AreaName, AreaInfo); + s.Add(slot with { CrossFromLocation = (ushort)Location }); + } + + public void Integrate() + { + foreach (var point in Local) + { + foreach (var slot in point.Slots) + Add(slot); + } + foreach (var point in Nearby) + { + foreach (var slot in point.Slots) + AddCrossover(slot, point.Location); + } + } + + public void Consolidate() + { + ConsolidateEncounters(Slots); + foreach (var loc in SlotsCrossover.Values) + loc.Consolidate(); + } + + private static void ConsolidateEncounters(List encounters) + { + // Merge and remove future indexes if they can be combined with current + // Pre-sort so we can just iterate. + encounters.Sort(); + + for (int i = 0; i < encounters.Count;) + { + // For i to i+count, merge within + var enc = encounters[i]; + int count = GetPotentialConsolidationCount(encounters, i, enc); + + // Remove all indexes that have Absorb return true with a later element + while (TryAbsorbFromRange(encounters, i, count)) + count--; + i += count; + } + } + + private static int GetPotentialConsolidationCount(List encounters, int start, PaldeaEncounter enc) + { + int count = 1; + for (int end = start + 1; end < encounters.Count; end++) + { + if (!encounters[end].IsSameSpecFormGender(enc)) + break; + count++; + } + return count; + } + + private static bool TryAbsorbFromRange(List encounters, int start, int count) + { + // Need to compare all elements with each-other; return true if any absorb. + var end = start + count - 1; + for (int i = start; i < end; i++) + { + for (int j = i + 1; j <= end; j++) + { + if (!encounters[i].Absorb(encounters[j])) + continue; + encounters.RemoveAt(j); + return true; + } + } + return false; + } + + public void LoadPoints(IEnumerable points, IContainsV3f collider, int areaMin, int areaMax) + { + foreach (var w in points) + { + if (!w.IsWithinLevelRange(areaMin, areaMax)) + continue; + if (collider.ContainsPoint(w.X, w.Y, w.Z)) + { + var composite = GetCompositePoint(w.Point, areaMin, areaMax, Location); + Local.Add(composite); // native + } + // Don't check here for cross-area, as our areaMin/areaMax is not valid for that crossover case. + } + } + + private static LocationPointDetail GetCompositePoint(PointData ep, int areaMin, int areaMax, int location) + { + var newX = Math.Max(ep.LevelRange.X, areaMin); + var newY = Math.Min(ep.LevelRange.Y, areaMax); + var newPoint = new PointData + { + Position = ep.Position, + LevelRange = new PackedVec2f { X = newX, Y = newY }, + Biome = ep.Biome, + Substance = ep.Substance, + AreaNo = ep.AreaNo, + }; + return new LocationPointDetail(newPoint) { Location = location }; + } + + public void GetEncounters(EncountPokeDataArray pokeData, PaldeaSceneModel scene) + { + foreach (var spawner in Local) + { + foreach (var pd in pokeData.Table) + { + if (!IsAbleToSpawnAt(pd, spawner, AreaName, scene)) + continue; + + // Add encount + spawner.Add(PaldeaEncounter.GetNew(pd, spawner.Point)); + if (pd.BandPoke != 0) // Add band encount + spawner.Add(PaldeaEncounter.GetBand(pd, spawner.Point)); + } + } + } + + private static bool IsAbleToSpawnAt(EncountPokeData pd, LocationPointDetail ep, string areaName, PaldeaSceneModel scene) + { + // Check area + if (!string.IsNullOrEmpty(pd.Area) && !IsInArea(pd.Area, ep.Point.AreaNo)) + return false; + + // Check loc + if (!string.IsNullOrEmpty(pd.LocationName) && !IsInArea(pd.LocationName, areaName, scene, ep.Point)) + return false; + + // Check biome + if (!HasBiome(pd, (Biome)(int)ep.Point.Biome)) + return false; + + // check level range overlap + // check area level range overlap -- already done via point + if (!LevelWithinRange(pd, ep.Point)) + return false; + + // Assume flag, enable table, timetable are fine + // Assume version is fine -- union wireless sessions can share encounters. + + return true; + } + + private static bool LevelWithinRange(EncountPokeData pd, PointData clamp) + { + return clamp.LevelRange.X <= pd.MaxLevel && pd.MinLevel <= clamp.LevelRange.Y; + } + + private static bool HasBiome(EncountPokeData pd, Biome biome) + { + if (biome == pd.Biome1) + return true; + if (biome == pd.Biome2) + return true; + if (biome == pd.Biome3) + return true; + if (biome == pd.Biome4) + return true; + return false; + } + + private static bool IsInArea(ReadOnlySpan areaName, int areaNo) + { + int start = 0; + for (int i = 0; i < areaName.Length; i++) + { + if (areaName[i] != ',') + continue; + var name = areaName[start..i]; + if (int.TryParse(name, out var tmp) && areaNo == tmp) + return true; + start = i + 1; + } + return int.TryParse(areaName[start..], out var x) && areaNo == x; + } + + private static bool IsInArea(string locName, string areaName, PaldeaSceneModel scene, PointData ep) + { + var split = locName.Split(","); + foreach (string a in split) + { + if (a == areaName) + return true; + + if (!scene.IsPointContained(a, ep.Position.X, ep.Position.Y, ep.Position.Z)) + continue; + + return true; + } + return false; + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs b/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs new file mode 100644 index 00000000..2d551d88 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers; + +namespace pkNX.WinForms; + +public class PaldeaCoinSymbolModel +{ + public readonly List Points; + + public PaldeaCoinSymbolModel(IFileInternal ROM) + { + Points = new List(); + + var cData = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_coin_placement_symbol_/world_coin_placement_symbol_0.trscn"); + // NOTE: Fine to only use Scarlet, Violet data is identical. + + var c = FlatBufferConverter.DeserializeFrom(cData); + + Points.AddRange(GetObjectTemplateSymbolPoints(c)); + } + + private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template) + { + foreach (var obj in template.Objects) + { + switch (obj.Type) + { + case "trinity_ScenePoint": + { + var scenePoint = FlatBufferConverter.DeserializeFrom(obj.Data); + + if (obj.SubObjects.Length != 1) + throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject Count {obj.SubObjects.Length}"); + + if (obj.SubObjects[0].Type != "trinity_PropertySheet") + throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject ({obj.SubObjects[0].Type})"); + + var propSheet = FlatBufferConverter.DeserializeFrom(obj.SubObjects[0].Data); + + yield return ParseCoinSymbolPoint(scenePoint, propSheet); + break; + } + default: + throw new ArgumentException($"Unsupported CoinPlacement Object Type {obj.Type}"); + } + } + } + + private PaldeaCoinSymbolPoint ParseCoinSymbolPoint(TrinityScenePointSV sp, TrinityPropertySheetSV ps) + { + switch (ps.Name) + { + case "coin_walk_symbol_point": + return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), string.Empty, sp.Position); + case "coin_box_symbol_point": + return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), GetBoxLabel(ps), sp.Position); + default: + throw new ArgumentException($"Unknown CoinSymbol PropertySheet {ps.Name}"); + } + } + + private ulong GetFirstNum(TrinityPropertySheetSV ps) + { + if (ps.Properties[0].Fields[0].Name != "firstNum") + throw new ArgumentException("Invalid PropertySheet field layout"); + + if (!ps.Properties[0].Fields[0].Data.TryGet(out TrinityPropertySheetField1SV? sv)) + throw new ArgumentException("Could not get PropertySheet Table Key"); + + return sv.Value; + } + + private string GetBoxLabel(TrinityPropertySheetSV ps) + { + if (ps.Properties[0].Fields[1].Name != "label") + throw new ArgumentException("Invalid PropertySheet field layout"); + + if (!ps.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv)) + throw new ArgumentException("Could not get PropertySheet Table Key"); + + return sv.Value; + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs b/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs new file mode 100644 index 00000000..578837e0 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs @@ -0,0 +1,26 @@ +using pkNX.Structures.FlatBuffers; + +namespace pkNX.WinForms; + +public class PaldeaCoinSymbolPoint +{ + public string Name; + public ulong FirstNum; + public string BoxLabel; + public PackedVec3f Position; + + public PaldeaCoinSymbolPoint(string name, ulong num, string boxLabel, PackedVec3f pos) + { + Name = name; + FirstNum = num; + BoxLabel = boxLabel; + Position = new PackedVec3f + { + X = pos.X, + Y = pos.Y, + Z = pos.Z, + }; + } + + public bool IsBox => !string.IsNullOrEmpty(BoxLabel); +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs b/pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs new file mode 100644 index 00000000..319ad2f1 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; + +namespace pkNX.Structures.FlatBuffers; + +public record PaldeaEncounter(ushort Species, byte Form, byte Sex, byte MinLevel, byte MaxLevel, byte Time, ushort CrossFromLocation = 0) : IComparable +{ + public byte MinLevel { get; private set; } = MinLevel; + public byte MaxLevel { get; private set; } = MaxLevel; + + public int Gender => Sex switch + { + 1 => 0, + 2 => 1, + _ => -1, + }; + + // Match the Mark time of day order: Noon, Night, Evening, Morning + private static byte GetTimeBits(TimeTable t) => (byte)((t.Noon ? 0 : 1) | (t.Night ? 0 : 2) | (t.Evening ? 0 : 4) | (t.Morning ? 0 : 8)); + + public static PaldeaEncounter GetNew(EncountPokeData pd, PointData ep) + { + // Combine the 4 bools into a single byte + var time = GetTimeBits(pd.TimeTable); + var min = (byte)Math.Max(ep.LevelRange.X, pd.MinLevel); + var max = (byte)Math.Min(ep.LevelRange.Y, pd.MaxLevel); + return new((ushort)pd.DevId, (byte)pd.Form, (byte)pd.Sex, min, max, time); + } + + public static PaldeaEncounter GetBand(EncountPokeData pd, PointData ep) + { + // Combine the 4 bools into a single byte + var time = GetTimeBits(pd.TimeTable); + var min = (byte)Math.Max(ep.LevelRange.X, pd.MinLevel); + var max = (byte)Math.Min(ep.LevelRange.Y, pd.MaxLevel); + return new((ushort)pd.BandPoke, (byte)pd.BandForm, (byte)pd.BandSex, min, max, time); + } + + public string GetEncountString(IReadOnlyList specNames) + { + var species = specNames[Species]; + return GetString(species); + } + + public override string ToString() + { + return GetString(((PKHeX.Core.Species)Species).ToString()); + } + + private string GetString(string species) + { + var form = Form == 0 ? "" : $"-{Form}"; + var sex = Sex == 0 ? "" : $" (sex={Sex})"; + return $"{species}{form}{sex} Lv. {MinLevel}-{MaxLevel}"; + } + + public bool Absorb(PaldeaEncounter other) + { + if (Time != other.Time) + return false; + if (CrossFromLocation != other.CrossFromLocation) + return false; + if (other.MinLevel == MinLevel && other.MaxLevel == MaxLevel) + return true; + + if (!IsLevelRangeOverlap(other) && !other.IsLevelRangeOverlap(this)) + return false; + + MinLevel = Math.Min(MinLevel, other.MinLevel); + MaxLevel = Math.Max(MaxLevel, other.MaxLevel); + return true; + } + + public bool IsSameSpecFormGender(PaldeaEncounter other) + { + if (Species != other.Species || Form != other.Form) + return false; + if (Sex != other.Sex) + return false; + return true; + } + + private bool IsLevelRangeOverlap(PaldeaEncounter other) + { + // If our level range overlaps with the other (with +/- 1 tolerance), return true. + return MaxLevel + 1 >= other.MinLevel && MinLevel + 1 <= other.MaxLevel; + } + + public int CompareTo(PaldeaEncounter? other) + { + if (ReferenceEquals(this, other)) return 0; + if (other is null) return 1; + int speciesComparison = Species.CompareTo(other.Species); + if (speciesComparison != 0) return speciesComparison; + int formComparison = Form.CompareTo(other.Form); + if (formComparison != 0) return formComparison; + int sexComparison = Sex.CompareTo(other.Sex); + if (sexComparison != 0) return sexComparison; + int minLevelComparison = MinLevel.CompareTo(other.MinLevel); + if (minLevelComparison != 0) return minLevelComparison; + int maxLevelComparison = MaxLevel.CompareTo(other.MaxLevel); + if (maxLevelComparison != 0) return maxLevelComparison; + int timeComparison = Time.CompareTo(other.Time); + if (timeComparison != 0) return timeComparison; + return CrossFromLocation.CompareTo(other.CrossFromLocation); + } + + public ulong GetHash() + { + ulong result = Species; + result = (result << 16) | CrossFromLocation; + + result = (result << 8) | Form; + result = (result << 8) | (byte)((byte)(Time << 4) | Sex); + result = (result << 8) | MinLevel; + result = (result << 8) | MaxLevel; + return result; + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaFieldModel.cs b/pkNX.WinForms/Dumping/SV/PaldeaFieldModel.cs new file mode 100644 index 00000000..fad4236b --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaFieldModel.cs @@ -0,0 +1,52 @@ +using System; +using pkNX.Containers; + +namespace pkNX.Structures.FlatBuffers; + +public class PaldeaFieldModel +{ + private readonly FieldMainArea[] mainAreas; + private readonly FieldSubArea[] subAreas; + private readonly FieldInsideArea[] insideAreas; + private readonly FieldDungeonArea[] dungeonAreas; + private readonly FieldLocation[] fieldLocations; + + public PaldeaFieldModel(IFileInternal ROM) + { + mainAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_main_area/field_main_area_array.bin")).Table; + subAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_sub_area/field_sub_area_array.bin")).Table; + insideAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_inside_area/field_inside_area_array.bin")).Table; + dungeonAreas = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_dungeon_area/field_dungeon_area_array.bin")).Table; + fieldLocations = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/field/area/field_location/field_location_array.bin")).Table; + } + + public AreaInfo FindAreaInfo(string name) + { + foreach (var area in mainAreas) + { + if (area.Name == name) + return area.AreaInfo; + } + foreach (var area in subAreas) + { + if (area.Name == name) + return area.AreaInfo; + } + foreach (var area in insideAreas) + { + if (area.Name == name) + return area.AreaInfo; + } + foreach (var area in dungeonAreas) + { + if (area.Name == name) + return area.AreaInfo; + } + foreach (var area in fieldLocations) + { + if (area.Name == name) + return area.AreaInfo; + } + throw new ArgumentException($"Unknown area {name}"); + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs b/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs new file mode 100644 index 00000000..8cdb19b0 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers; + +namespace pkNX.WinForms; + +public class PaldeaFixedSymbolModel +{ + public readonly List scarletPoints; + public readonly List violetPoints; + + public PaldeaFixedSymbolModel(IFileInternal ROM) + { + scarletPoints = new List(); + violetPoints = new List(); + + var p0Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_fixed_placement_symbol_/world_fixed_placement_symbol_0.trscn"); + var p1Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_fixed_placement_symbol_/world_fixed_placement_symbol_1.trscn"); + + var p0 = FlatBufferConverter.DeserializeFrom(p0Data); + var p1 = FlatBufferConverter.DeserializeFrom(p1Data); + + scarletPoints.AddRange(GetObjectTemplateSymbolPoints(p0)); + violetPoints.AddRange(GetObjectTemplateSymbolPoints(p1)); + } + + private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template) + { + foreach (var obj in template.Objects) + { + switch (obj.Type) + { + case "trinity_ObjectTemplate": + { + var sObj = FlatBufferConverter.DeserializeFrom(obj.Data); + if (sObj.Type != "trinity_ScenePoint") + continue; + var scenePoint = FlatBufferConverter.DeserializeFrom(sObj.Data); + + foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) + yield return f; + break; + } + case "trinity_ScenePoint": + { + var scenePoint = FlatBufferConverter.DeserializeFrom(obj.Data); + + foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) + yield return f; + break; + } + } + } + } + + private IEnumerable GetScenePointSymbolPoints(TrinityScenePointSV scenePoint, TrinitySceneObjectTemplateEntrySV[] subObjects) + { + // Handle SubObjects + for (var i = 0; i < subObjects.Length; i++) + { + var sobj = subObjects[i]; + switch (sobj.Type) + { + case "trinity_PropertySheet": + { + var propSheet = FlatBufferConverter.DeserializeFrom(sobj.Data); + if (propSheet.Name == "fixed_symbol_point") + { + var tableKey = GetTableKey(propSheet); + if (!string.IsNullOrEmpty(tableKey)) + { + yield return new PaldeaFixedSymbolPoint(tableKey, scenePoint.Position); + } + } + break; + } + case "trinity_ScenePoint": + { + var subScenePoint = FlatBufferConverter.DeserializeFrom(sobj.Data); + foreach (var f in GetScenePointSymbolPoints(subScenePoint, sobj.SubObjects)) + yield return f; + break; + } + default: + throw new ArgumentException($"Unknown SubObject {sobj.Type}"); + } + } + } + + public static string GetTableKey(TrinityPropertySheetSV propSheet) + { + if (propSheet.Name != "fixed_symbol_point") + throw new ArgumentException($"Invalid PropertySheet {propSheet.Name}"); + + if (propSheet.Properties[0].Fields[1].Name != "tableKey") + throw new ArgumentException("Invalid PropertySheet field layout"); + + if (!propSheet.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv)) + throw new ArgumentException("Could not get PropertySheet Table Key"); + + return sv.Value; + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolPoint.cs b/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolPoint.cs new file mode 100644 index 00000000..6f4501f5 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolPoint.cs @@ -0,0 +1,20 @@ +using pkNX.Structures.FlatBuffers; + +namespace pkNX.WinForms; + +public class PaldeaFixedSymbolPoint +{ + public string TableKey; + public PackedVec3f Position; + + public PaldeaFixedSymbolPoint(string key, PackedVec3f pos) + { + TableKey = key; + Position = new PackedVec3f + { + X = pos.X, + Y = pos.Y, + Z = pos.Z, + }; + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaSceneModel.cs b/pkNX.WinForms/Dumping/SV/PaldeaSceneModel.cs new file mode 100644 index 00000000..263dd49e --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/PaldeaSceneModel.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using pkNX.Containers; + +namespace pkNX.Structures.FlatBuffers; + +public class PaldeaSceneModel +{ + public readonly List areaNames; + public readonly Dictionary AreaInfos; + public readonly Dictionary areaColTrees; + public readonly Dictionary areaColBoxes; + public readonly Dictionary isAtlantis; + + public PaldeaSceneModel(IFileInternal ROM, PaldeaFieldModel field) + { + // NOTE: Safe to only use _0 because _1 is identical. + var area_management = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/scene/parts/field/field_system/area_management_/area_management_0.trscn")); + Debug.Assert(area_management.ObjectTemplateName == "area_management"); + + // NOTE: Safe to only use _0 because _1 is identical. + var a_w23_field_area_col = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/scene/parts/field/room/a_w23_field/a_w23_field_area_col_/a_w23_field_area_col_0.trscn")); + Debug.Assert(a_w23_field_area_col.ObjectTemplateName == "a_w23_field_area_col"); + + areaNames = new List(); + AreaInfos = new Dictionary(); + areaColTrees = new Dictionary(); + areaColBoxes = new Dictionary(); + isAtlantis = new Dictionary(); + + void AddSceneObject(string name, TrinitySceneObjectSV sceneObject, TrinityCollisionComponent1SV collisionComponent) + { + areaNames.Add(name); + AreaInfos[name] = field.FindAreaInfo(name); + + Debug.Assert(collisionComponent.CollisionShape.Discriminator is 2 or 4); + + if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeBoxSV? box)) + { + // Box collision, obj.ObjectPosition.Field_02 is pos, box.Field_01 is size of box + areaColBoxes[name] = new BoxCollision9 + { + Position = sceneObject.ObjectPosition.Field_02, + Size = box.Field_01, + }; + } + else if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeHavokSV? havok)) + { + var havokData = ROM.GetPackedFile(havok.TrcolFilePath); + areaColTrees[name] = HavokCollision.ParseAABBTree(havokData); + } + } + + foreach (var obj in area_management.Objects.Concat(a_w23_field_area_col.Objects)) + { + var isAtlantisObj = a_w23_field_area_col.Objects.Contains(obj); + if (!(obj.SubObjects.Length > 0 && obj.SubObjects[0].Type == "trinity_CollisionComponent")) + continue; + var collisionComponent = FlatBufferConverter.DeserializeFrom(obj.SubObjects[0].Data).Component.Item1; + + switch (obj.Type) + { + case "trinity_ObjectTemplate": + { + var sObj = FlatBufferConverter.DeserializeFrom(obj.Data); + if (sObj.Type != "trinity_SceneObject") + continue; + var sceneObject = FlatBufferConverter.DeserializeFrom(sObj.Data); + Debug.Assert(sceneObject.ObjectName == sObj.ObjectTemplateExtra); + + AddSceneObject(sObj.ObjectTemplateName, sceneObject, collisionComponent); + isAtlantis[sObj.ObjectTemplateName] = isAtlantisObj; + break; + } + case "trinity_SceneObject": + { + var sceneObject = FlatBufferConverter.DeserializeFrom(obj.Data); + AddSceneObject(sceneObject.ObjectName, sceneObject, collisionComponent); + isAtlantis[sceneObject.ObjectName] = isAtlantisObj; + break; + } + } + } + } + + public bool IsPointContained(string areaName, float x, float y, float z) + { + if (areaColTrees.TryGetValue(areaName, out var tree)) + return tree.ContainsPoint(x, y, z); + if (areaColBoxes.TryGetValue(areaName, out var box)) + return box.ContainsPoint(x, y, z); + return false; + } + + public bool TryGetContainsCheck(string areaName, [NotNullWhen(true)] out IContainsV3f? result) + { + if (areaColTrees.TryGetValue(areaName, out var tree)) + { + result = tree; + return true; + } + + if (areaColBoxes.TryGetValue(areaName, out var box)) + { + result = box; + return true; + } + + result = null; + return false; + } +} diff --git a/pkNX.WinForms/Subforms/MapViewer9.Designer.cs b/pkNX.WinForms/Subforms/MapViewer9.Designer.cs index 1789cc9c..005c4878 100644 --- a/pkNX.WinForms/Subforms/MapViewer9.Designer.cs +++ b/pkNX.WinForms/Subforms/MapViewer9.Designer.cs @@ -28,7 +28,6 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MapViewer9)); this.pictureBox1 = new System.Windows.Forms.PictureBox(); this.CB_Map = new System.Windows.Forms.ComboBox(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); @@ -62,7 +61,6 @@ private void InitializeComponent() this.ClientSize = new System.Drawing.Size(827, 614); this.Controls.Add(this.CB_Map); this.Controls.Add(this.pictureBox1); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.Name = "MapViewer9"; this.Text = "MapViewer9";