From a3f7daa178bb7dfb891b4593dfdba3a0310cb220 Mon Sep 17 00:00:00 2001 From: Kurt Date: Mon, 23 Jun 2025 00:26:06 -0500 Subject: [PATCH] Pre-work: clean up some encounter dump logic Extract individual dumpers to separate classes & files. Too much was happening in that one file. Move to separate folders --- pkNX.WinForms/Dumping/GameDumperSV.cs | 22 +- pkNX.WinForms/Dumping/MassOutbreakRipper.cs | 2 +- .../SV/Encounter/EncounterDumpConfigSV.cs | 18 + .../Dumping/SV/Encounter/EncounterDumperSV.cs | 142 +++ .../SV/Encounter/EncounterExtensions9.cs | 65 ++ .../SV/Encounter/EncounterSlotDumper9.cs | 277 ++++++ .../SV/Encounter/FixedSymbolDumper9.cs | 286 ++++++ .../Dumping/SV/Encounter/GimmighoulDumper.cs | 93 ++ .../SV/{ => Encounter}/PaldeaEncounter.cs | 3 +- pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs | 850 ------------------ .../SV/{ => Location}/LocationDatabase.cs | 0 .../SV/{ => Location}/LocationPointDetail.cs | 0 .../SV/{ => Location}/LocationStorage.cs | 0 .../SV/{ => Models}/PaldeaCoinSymbolModel.cs | 3 +- .../SV/{ => Models}/PaldeaCoinSymbolPoint.cs | 4 +- .../SV/{ => Models}/PaldeaFieldModel.cs | 0 .../SV/Models/PaldeaFixedSymbolModel.cs | 157 ++++ .../SV/{ => Models}/PaldeaFixedSymbolPoint.cs | 4 +- .../SV/{ => Models}/PaldeaSceneModel.cs | 0 .../Dumping/SV/Models/PaldeaSpawnModel.cs | 66 ++ .../Dumping/SV/PaldeaFixedSymbolModel.cs | 176 ---- 21 files changed, 1123 insertions(+), 1045 deletions(-) create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/EncounterDumpConfigSV.cs create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/EncounterDumperSV.cs create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/EncounterExtensions9.cs create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/EncounterSlotDumper9.cs create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/FixedSymbolDumper9.cs create mode 100644 pkNX.WinForms/Dumping/SV/Encounter/GimmighoulDumper.cs rename pkNX.WinForms/Dumping/SV/{ => Encounter}/PaldeaEncounter.cs (97%) delete mode 100644 pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs rename pkNX.WinForms/Dumping/SV/{ => Location}/LocationDatabase.cs (100%) rename pkNX.WinForms/Dumping/SV/{ => Location}/LocationPointDetail.cs (100%) rename pkNX.WinForms/Dumping/SV/{ => Location}/LocationStorage.cs (100%) rename pkNX.WinForms/Dumping/SV/{ => Models}/PaldeaCoinSymbolModel.cs (98%) rename pkNX.WinForms/Dumping/SV/{ => Models}/PaldeaCoinSymbolPoint.cs (85%) rename pkNX.WinForms/Dumping/SV/{ => Models}/PaldeaFieldModel.cs (100%) create mode 100644 pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolModel.cs rename pkNX.WinForms/Dumping/SV/{ => Models}/PaldeaFixedSymbolPoint.cs (77%) rename pkNX.WinForms/Dumping/SV/{ => Models}/PaldeaSceneModel.cs (100%) create mode 100644 pkNX.WinForms/Dumping/SV/Models/PaldeaSpawnModel.cs delete mode 100644 pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs diff --git a/pkNX.WinForms/Dumping/GameDumperSV.cs b/pkNX.WinForms/Dumping/GameDumperSV.cs index f488d2c9..e5c8262e 100644 --- a/pkNX.WinForms/Dumping/GameDumperSV.cs +++ b/pkNX.WinForms/Dumping/GameDumperSV.cs @@ -642,15 +642,21 @@ public void DumpBalloon() public void DumpEncounters() { - var dumper = new EncounterDumperSV(rom); - var path = GetPath("encounters"); + const string language = "English"; + var cfg = new TextConfig(GameVersion.SV); - var specNamesInternal = GetCommonText("monsname", "English", cfg); - var moveNames = GetCommonText("wazaname", "English", cfg); - var place_names = GetCommonText("place_name", "English", cfg); - var ahtb = GetCommonAHTB("place_name", "English"); - var nameDict = EncounterDumperSV.GetPlaceNameMap(place_names, ahtb); - dumper.DumpTo(path, specNamesInternal, moveNames, nameDict); + var ahtb = GetCommonAHTB("place_name", language); + var place_names = GetCommonText("place_name", language, cfg); + var nameDict = EncounterDumperSV.GetInternalStringLookup(place_names, ahtb); + + var config = new EncounterDumpConfigSV + { + PlaceNameMap = nameDict, + SpecNamesInternal = GetCommonText("monsname", language, cfg), + MoveNames = GetCommonText("wazaname", language, cfg), + Path = GetPath("encounters"), + }; + EncounterDumperSV.Dump(rom, config); } public void DumpRaid() diff --git a/pkNX.WinForms/Dumping/MassOutbreakRipper.cs b/pkNX.WinForms/Dumping/MassOutbreakRipper.cs index d648019a..a37ac5d7 100644 --- a/pkNX.WinForms/Dumping/MassOutbreakRipper.cs +++ b/pkNX.WinForms/Dumping/MassOutbreakRipper.cs @@ -26,7 +26,7 @@ public static void DumpDeliveryOutbreaks(IFileInternal ROM, string path, string var place_names = GetCommonText(ROM, "place_name", "English", cfg); var data = ROM.GetPackedFile("message/dat/English/common/place_name.tbl"); var ahtb = new AHTB(data); - NameDict = EncounterDumperSV.GetPlaceNameMap(place_names, ahtb); + NameDict = EncounterDumperSV.GetInternalStringLookup(place_names, ahtb); var dirs = Directory.GetDirectories(path, "*", SearchOption.AllDirectories).Order(); foreach (var dir in dirs) diff --git a/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumpConfigSV.cs b/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumpConfigSV.cs new file mode 100644 index 00000000..8b12a68d --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumpConfigSV.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public record EncounterDumpConfigSV +{ + public required string Path { get; init; } + public required string[] SpecNamesInternal { get; init; } + public required string[] MoveNames { get; init; } + public required Dictionary PlaceNameMap { get; init; } + + public bool WriteText { get; init; } = true; + public bool WritePickle { get; init; } = true; + + public string this[WazaSet move] => MoveNames[(int)move.WazaId]; + public string this[DevID species] => SpecNamesInternal[(int)species]; +} diff --git a/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumperSV.cs b/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumperSV.cs new file mode 100644 index 00000000..59bfc787 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/EncounterDumperSV.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public static class EncounterDumperSV +{ + private const float tolX = 30f; + private const float tolY = 30f; + private const float tolZ = 30f; + + public static bool IsContainedBy(T collider, LocationPointDetail point) where T : IContainsV3f + => collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ); + + public static bool IsContainedBy(T collider, PackedVec3f point) where T : IContainsV3f + => collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ); + + public static ReadOnlySpan AllMaps => + [ + PaldeaFieldIndex.Paldea, + PaldeaFieldIndex.Kitakami, + PaldeaFieldIndex.Terarium, + ]; + + public static void Dump(IFileInternal rom, EncounterDumpConfigSV config) + { + if (!Directory.Exists(config.Path)) + Directory.CreateDirectory(config.Path); + + var field = new PaldeaFieldModel(rom); + var scene = new PaldeaSceneModel(rom, field); + EncounterSlotDumper9.DumpSlots(rom, config, scene); + FixedSymbolDumper9.DumpFixedSymbols(rom, config, scene); + GimmighoulDump.DumpGimmighoul(rom, config, scene); + + // Raw dumps for inspection + DumpScene(scene, config.Path); + DumpField(field, config.Path); + } + + public static bool TryGetPlaceName(ref string areaName, AreaInfo areaInfo, + IContainsV3f collider, Dictionary placeNameMap, + IReadOnlyDictionary areas, + PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out string placeName) + { + placeName = areaInfo.LocationNameMain; + if (!string.IsNullOrEmpty(placeName)) + return true; + + // Maybe this is a sub-area? Try to get the parent area name. + bool IsValidParentAreaName(string aName) + { + if (!areas.TryGetValue(aName, out var info)) + return false; + var n = info.LocationNameMain; + if (string.IsNullOrEmpty(n)) + return false; + return placeNameMap.ContainsKey(n); + } + + if (!scene.TryGetParentAreaName(fieldIndex, areaName, collider, IsValidParentAreaName, out var parentAreaName)) + { + Console.WriteLine($"No parent area for {areaName}"); + return false; + } + + areaName = parentAreaName; + placeName = areas[parentAreaName].LocationNameMain; + return true; + } + + public static bool TryGetPlaceName(ref string areaName, AreaInfo areaInfo, + PackedVec3f point, Dictionary placeNameMap, + IReadOnlyDictionary areas, + PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out string placeName) + { + placeName = areaInfo.LocationNameMain; + if (!string.IsNullOrEmpty(placeName)) + return true; + + // Maybe this is a sub-area? Try to get the parent area name. + bool IsValidParentAreaName(string aName) + { + if (!areas.TryGetValue(aName, out var info)) + return false; + var n = info.LocationNameMain; + if (string.IsNullOrEmpty(n)) + return false; + return placeNameMap.ContainsKey(n); + } + + if (!scene.TryGetParentAreaName(fieldIndex, areaName, point, IsValidParentAreaName, out var parentAreaName)) + { + Console.WriteLine($"No parent area for {areaName}"); + return false; + } + + areaName = parentAreaName; + placeName = areas[parentAreaName].LocationNameMain; + return true; + } + + private static void DumpScene(PaldeaSceneModel scene, string path) + { + // Dump each property to json. + var dest = Path.Combine(path, "paldea_scene.json"); + var json = JsonSerializer.Serialize(scene, new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); + File.WriteAllText(dest, json); + } + + private static void DumpField(PaldeaFieldModel field, string path) + { + // Dump each property to json. + var dest = Path.Combine(path, "paldea_field.json"); + var json = JsonSerializer.Serialize(field, new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); + File.WriteAllText(dest, json); + } + + public static bool IsCrossoverAllowed(int MetLocation) => MetLocation switch + { + 8 => false, // Mesagoza + 124 => false, // Area Zero + _ => true, + }; + + public static Dictionary GetInternalStringLookup(ReadOnlySpan text, AHTB ahtb) + { + var result = new Dictionary(); + for (var i = 0; i < text.Length; i++) + { + var entry = ahtb.Entries[i]; + var name = entry.Name; + var value = (text[i], i); + result.Add(name, value); + } + return result; + } +} diff --git a/pkNX.WinForms/Dumping/SV/Encounter/EncounterExtensions9.cs b/pkNX.WinForms/Dumping/SV/Encounter/EncounterExtensions9.cs new file mode 100644 index 00000000..813a46fa --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/EncounterExtensions9.cs @@ -0,0 +1,65 @@ +using System; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +internal static class EncounterExtensions9 +{ + public static string Humanize(this SizeType type, short value) => type switch + { + SizeType.RANDOM => "Random", + SizeType.XS => "XS", + SizeType.S => "S", + SizeType.M => "M", + SizeType.L => "L", + SizeType.XL => "XL", + SizeType.VALUE => value.ToString(), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + + public static string Humanize(this RareType type) => type switch + { + RareType.DEFAULT => "Random", + RareType.NO_RARE => "Never", + RareType.RARE => "Always", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + + public static string Humanize(this SexType type) => type switch + { + SexType.DEFAULT => "Random", + SexType.MALE => "Male", + SexType.FEMALE => "Female", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + + public static string Humanize(this TokuseiType type) => type switch + { + TokuseiType.RANDOM_12 => "1/2", + TokuseiType.RANDOM_123 => "1/2/H", + TokuseiType.SET_1 => "1", + TokuseiType.SET_2 => "2", + TokuseiType.SET_3 => "H", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Invalid ability index"), + }; + + public static bool IsAvoidantAction(this PokemonActionID action) => action switch + { + PokemonActionID.FS_POP_PATH_RUN_SPEED_UP_1_NOT_COL_DELAY => true, // lighthouse Wingull (South) and Wattrel (East, West) + PokemonActionID.FS_POP_PATH_RUN_SPEED_UP_1_NOT_COL_DESTROY => true, // fence Wingull (Cabo Poco) + PokemonActionID.FS_POP_COMMON_FLIGHT_NOT_COL_DESTROY => true, // starting Fletchling (level 2) + _ => false, + }; + + public static bool IsStationary(this PokemonActionID action) => action switch + { + PokemonActionID.FS_POP_ALWAYS_GAZE_BIRD_TARGET_PLAYER => true, + PokemonActionID.FS_POP_ALWAYS_GAZE_TARGET_PLAYER => true, + PokemonActionID.ALWAYS_GAZE_BIRD_TARGET_PLAYER_LOOP => true, + PokemonActionID.FS_POP_LAND_SLEEPING_CURRENT_LOCATION => true, + PokemonActionID.FS_POP_LEVITATION_SLEEPING_TREE_BRANCH => true, + PokemonActionID.FS_POP_AREA22_DRAGONITE => true, // Handle separately. + _ => false, + }; + +} diff --git a/pkNX.WinForms/Dumping/SV/Encounter/EncounterSlotDumper9.cs b/pkNX.WinForms/Dumping/SV/Encounter/EncounterSlotDumper9.cs new file mode 100644 index 00000000..b8a33b7a --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/EncounterSlotDumper9.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public static class EncounterSlotDumper9 +{ + public static void DumpSlots(IFileInternal rom, EncounterDumpConfigSV config, PaldeaSceneModel scene) + { + var spawns = new PaldeaSpawnModel(rom); + var db = new LocationDatabase(); + + // Process all field indices + foreach (var index in EncounterDumperSV.AllMaps) ProcessAreas(index, scene, db, spawns, config.PlaceNameMap); + + // 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(); + if (storage.AreaName == "a_su0104") + storage.Consolidate(); + } + + // Output to stream if available. + if (config.WriteText) + { + using var sw = File.CreateText(Path.Combine(config.Path, "titan_enc.txt")); + foreach (var s in db.Locations.Values) + WriteLocation(sw, config, s.AreaName, s.AreaInfo, s.Slots); + using var tw = File.CreateText(Path.Combine(config.Path, "titan_loc_enc.txt")); + WriteLocEncList(tw, config, db); + + using var pw = File.CreateText(Path.Combine(config.Path, "titan_loc_point.txt")); + WriteLocPointList(pw, config, db); + } + if (config.WritePickle) + { + string binPath = Path.Combine(config.Path, "encounter_wild_paldea.pkl"); + SerializeEncounters(binPath, db); + } + } + + private static void ProcessAreas(PaldeaFieldIndex fieldIndex, PaldeaSceneModel scene, LocationDatabase db, PaldeaSpawnModel map, Dictionary placeNameMap) + { + // 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. + + // Fill the point lists for each area, then spawn everything into those points. + var areaNames = scene.AreaNames[(int)fieldIndex]; + var areas = scene.AreaInfos[(int)fieldIndex]; + var types = scene.PaldeaType[(int)fieldIndex]; + for (var i = areaNames.Count - 1; i >= 0; i--) + { + var areaName = areaNames[i]; + + // Determine potential spawners + if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) + { + Console.WriteLine($"No collider for {areaName}"); + continue; + } + + var areaInfo = areas[areaName]; + // Areas without a location name can't be reversed into location ID. + if (!EncounterDumperSV.TryGetPlaceName(ref areaName, areaInfo, collider, placeNameMap, areas, scene, fieldIndex, out var placeName)) + continue; + + // Locations that do not spawn encounters can still have crossovers bleed into them. + // We'll have empty local encounter lists for them. + var storage = db.Get(placeNameMap[placeName].Index, fieldIndex, areaName, areas[areaName]); + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + var type = types[areaName]; + var points = map.GetPoints(fieldIndex, type); + storage.LoadPoints(points, collider, areaInfo.ActualMinLevel, areaInfo.ActualMaxLevel, areaInfo.AdjustEncLv); + storage.GetEncounters(map.GetPokeData(fieldIndex), 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 areaNames) + { + // Same sanity checking as above iteration. + var areaInfo = areas[areaName]; + + // Determine potential spawners + if (!scene.TryGetContainsCheck(fieldIndex, 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, fieldIndex, areaName, areaInfo); + if (!IsCrossoverAllowed(storage)) + continue; + + // 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 areaNames) + { + // Skip self + if (otherName == areaName) + continue; + // Skip areas that don't have a location name -- subzones were the initial spawn spot if so. + var otherAreaInfo = areas[otherName]; + var otherNameMain = otherAreaInfo.LocationNameMain; + if (string.IsNullOrEmpty(otherNameMain)) + continue; + + // Skip areas that don't have a collider + if (!scene.TryGetContainsCheck(fieldIndex, otherName, out _)) + continue; + + // Iterate over all crossover points in the other area. + var cross = db.Get(placeNameMap[otherNameMain].Index, fieldIndex, otherName, otherAreaInfo); + if (!IsCrossoverAllowed(cross)) + continue; + 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 (EncounterDumperSV.IsContainedBy(collider, point)) + storage.Nearby.Add(point); + } + } + } + } + + /// + /// Allow encounters into and out of this location + /// + public static bool IsCrossoverAllowed(LocationStorage storage) + { + var loc = storage.Location; + return EncounterDumperSV.IsCrossoverAllowed(loc); + } + + 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) + { + ushort species = slot.Species; + byte form = slot.Species switch + { + (ushort)Species.Vivillon or (ushort)Species.Spewpa or (ushort)Species.Scatterbug => 30, + (ushort)Species.Minior => 31, + _ => slot.Form, + }; + + // ReSharper disable RedundantCast + bw.Write(species); + bw.Write(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); + // ReSharper restore RedundantCast + } + return ms.ToArray(); + } + + #region Writing + + private static void WriteLocation(TextWriter tw, EncounterDumpConfigSV config, string areaName, AreaInfo areaInfo, List encounts) + { + var loc = areaInfo.LocationNameMain; + if (!config.PlaceNameMap.TryGetValue(loc, out var value)) + return; + var (name, index) = value; + var heading = $"{areaName} - {loc} - {name} ({index})"; + WriteEncounts(tw, config.SpecNamesInternal, heading, encounts); + } + + private static void WriteLocEncList(TextWriter tw, EncounterDumpConfigSV config, LocationDatabase db) + { + var names = config.PlaceNameMap; + foreach (var place in names.Keys.OrderBy(p => names[p].Index)) + { + var (name, index) = names[place]; + if (!db.Locations.TryGetValue(index, out var encounts)) + continue; + + var heading = $"{name} ({index})"; + WriteEncounts(tw, config.SpecNamesInternal, heading, encounts.Slots); + } + } + + private static void WriteEncounts(TextWriter tw, ReadOnlySpan specNamesInternal, string heading, IEnumerable encounts) + { + tw.WriteLine("==="); + tw.WriteLine(heading); + tw.WriteLine("==="); + foreach (var e in encounts) + tw.WriteLine($" - {e.GetEncountString(specNamesInternal)}"); + tw.WriteLine(); + } + + private static void WriteLocPointList(TextWriter tw, EncounterDumpConfigSV config, LocationDatabase db) + { + var names = config.PlaceNameMap; + foreach (var place in names.Keys.OrderBy(p => names[p].Index)) + { + var (name, index) = names[place]; + if (!db.Locations.TryGetValue(index, out var loc)) + continue; + + var heading = $"{name} ({index})"; + WriteBiomes(tw, heading, loc.Local); + } + + foreach (var place in names.Keys.OrderBy(p => names[p].Index)) + { + var (name, index) = names[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()).Order(); + 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(); + } + #endregion +} diff --git a/pkNX.WinForms/Dumping/SV/Encounter/FixedSymbolDumper9.cs b/pkNX.WinForms/Dumping/SV/Encounter/FixedSymbolDumper9.cs new file mode 100644 index 00000000..399016f6 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/FixedSymbolDumper9.cs @@ -0,0 +1,286 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public static class FixedSymbolDumper9 +{ + private record struct AppearTuple(string PlaceName, string AreaName, int Adjust, PackedVec3f Point); + + public static void Dump(IFileInternal rom, EncounterDumpConfigSV config, PaldeaSceneModel scene) + { + List serialized = []; + var fsym = new PaldeaFixedSymbolModel(rom); + var array = rom.GetPackedFile("world/data/field/fixed_symbol/fixed_symbol_table/fixed_symbol_table_array.bin"); + var fsymData = FlatBufferConverter.DeserializeFrom(array); + foreach (var (game, gamePoints) in new[] { ("sl", points: fsym.PointsScarlet), ("vl", points: fsym.PointsViolet) }) + { + using var gw = File.CreateText(Path.Combine(config.Path, $"titan_fixed_{game}.txt")); + foreach (var fieldIndex in EncounterDumperSV.AllMaps) + { + var areaNames = scene.AreaNames[(int)fieldIndex]; + var areas = scene.AreaInfos[(int)fieldIndex]; + var atlantis = scene.PaldeaType[(int)fieldIndex]; + var allPoints = gamePoints[(int)fieldIndex]; + + if (fieldIndex is PaldeaFieldIndex.Kitakami) + { + areas = areas.Where(z => z.Value.AdjustEncLv != 0) + .ToDictionary(z => z.Key, z => z.Value); + areaNames = [.. areas.Keys]; + } + + for (var i = 0; i < fsymData.Table.Count; i++) + { + var entry = fsymData.Table[i]; + var tableKey = entry.TableKey; + if (tableKey.StartsWith("su2_w23d10_")) + continue; // handle later, manually + var points = allPoints.FindAll(p => p.TableKey == tableKey); + if (points.Count == 0) + continue; + + var appearAreas = new List(); + + if (!FindArea(AreaType.Cave)) + FindArea(AreaType.Default); + + bool FindArea(AreaType type) + { + for (var x = areaNames.Count - 1; x >= 0; x--) + { + var areaName = areaNames[x]; + if (atlantis[areaName] != PaldeaPointPivot.Overworld) + continue; + + var areaInfo = areas[areaName]; + if (type != AreaType.Default && areaInfo.Type != type) + continue; + if (areaInfo.Tag is AreaTag.NG_Encount) + continue; + if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) + continue; + for (int p = 0; p < points.Count; p++) + { + var point = points[p]; + var pt = point.Position; + if (!collider.ContainsPoint(pt.X, pt.Y, pt.Z)) + continue; + var tmp = areaName; + if (!EncounterDumperSV.TryGetPlaceName(ref tmp, areaInfo, pt, config.PlaceNameMap, areas, scene, fieldIndex, out var placeName)) + continue; + if (!appearAreas.Exists(z => z.Point == pt && z.AreaName == tmp)) + appearAreas.Add(new(placeName, tmp, areaInfo.AdjustEncLv, pt)); + points.RemoveAt(p); + p--; + } + + if (points.Count == 0) + return true; + } + return false; + } + + WriteFixedSpawn(config, gw, tableKey, i, entry, appearAreas); + + // Serialize + if (entry.PokeAI.ActionId.IsAvoidantAction()) + continue; // not actually encounter-able, they're just for spectacle + // var locs = appearAreas.Select(a => placeNameMap[a.PlaceName].Index); + if (appearAreas.Count == 0) + continue; + + // If not stationary, allow some tolerance. + var wanderAreas = new List(0); + if (!entry.PokeAI.ActionId.IsStationary()) + { + var allInfos = scene.AreaInfos[(int)fieldIndex]; + var allNames = scene.AreaNames[(int)fieldIndex]; + for (var a = 0; a < allNames.Count; a++) + { + var areaName = allNames[a]; + if (atlantis[areaName] != PaldeaPointPivot.Overworld) + continue; + + var areaInfo = allInfos[areaName]; + if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) + continue; + if (areaInfo.Tag is AreaTag.NG_Encount) + continue; + if (!TryGetBleedArea(collider, appearAreas, scene, fieldIndex, out var bledFrom)) + continue; // can't bleed into this area + + // Bleeding from zones should start from the sub-zone, so we don't need to check other sub-zones. + if (!EncounterDumperSV.TryGetPlaceName(ref areaName, areaInfo, collider, config.PlaceNameMap, allInfos, scene, fieldIndex, out var placeName)) + continue; + wanderAreas.Add(bledFrom with { PlaceName = placeName, AreaName = allNames[a] }); + } + } + + foreach (var area in appearAreas.GroupBy(z => z.Adjust)) + { + var adjust = area.Key; + var wander = wanderAreas.Where(z => z.Adjust == adjust); + var allLocations = area.Concat(wander); + var locationIDs = allLocations.Select(z => config.PlaceNameMap[z.PlaceName].Index).Distinct().ToList(); + if (fieldIndex == PaldeaFieldIndex.Paldea && entry.PokeAI.ActionId == PokemonActionID.FS_POP_AREA22_DRAGONITE) // Flies around not using tolerance. + locationIDs.Add(46); // North Province (Area One) + locationIDs.Sort(); + + WriteFixedSymbol(serialized, entry, locationIDs); + if (adjust != 0) + WriteFixedSymbol(serialized, entry, locationIDs, adjust); + } + } + } + + var underDepths = Array.FindIndex(fsymData.Table.ToArray(), x => x.TableKey == "su2_w23d10_01"); + for (int i = underDepths; i < underDepths + 23; i++) + { + var entry = fsymData.Table[i]; + var tableKey = entry.TableKey; + var appearAreas = new List { new("PLACENAME_a_w23_d10_01", "a_w23_d10_subarea", 0, new()) }; + WriteFixedSpawn(config, gw, tableKey, i, entry, appearAreas); + WriteFixedSymbol(serialized, entry, [196]); + } + } + + var pathPickle = Path.Combine(config.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 WriteFixedSpawn(EncounterDumpConfigSV config, StreamWriter gw, string tableKey, int i, FixedSymbolTable entry, List appearAreas) + { + gw.WriteLine("==="); + gw.WriteLine($"{tableKey} - {i}"); + gw.WriteLine("==="); + gw.WriteLine(" PokeData:"); + var pd = entry.Symbol; + gw.WriteLine($" Species: {config.SpecNamesInternal[(int)pd.DevId]}"); + gw.WriteLine($" Form: {pd.FormId}"); + gw.Write($" Level: {pd.Level}"); + foreach (var adj in appearAreas.Select(a => a.Adjust).Where(lv => lv != 0).Distinct().Order()) + { + gw.Write($", {pd.Level + adj}"); + } + gw.WriteLine(); + gw.WriteLine($" Sex: {pd.Sex.Humanize()}"); + gw.WriteLine($" Shiny: {pd.RareType.Humanize()}"); + + 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: {pd.TokuseiIndex.Humanize()}"); + switch (pd.WazaType) + { + case WazaType.DEFAULT: + gw.WriteLine(" Moves: Random"); + break; + case WazaType.MANUAL: + gw.WriteLine($" Moves: {config[pd.Waza1]}/{config[pd.Waza2]}/{config[pd.Waza3]}/{config[pd.Waza4]}"); + break; + } + + gw.WriteLine($" Scale: {pd.ScaleType.Humanize(pd.ScaleValue)}"); + gw.WriteLine($" GemType: {(int)pd.GemType}"); + + gw.WriteLine($" Probability: {entry.PokeGeneration.RepopProbability}"); + gw.WriteLine($" Pattern: {entry.PokeGeneration.GenerationPattern}"); + gw.WriteLine($" Scenario: {entry.PokeGeneration.RequireScenarioId}"); + gw.WriteLine($" Action: {entry.PokeAI.ActionId}"); + gw.WriteLine($" Trigger: {entry.PokeAI.TriggerActionId}"); + + gw.WriteLine(" Areas:"); + foreach (var area in appearAreas) + { + var loc = area.PlaceName; + var (name, index) = config.PlaceNameMap[loc]; + gw.WriteLine($" - {area.PlaceName} - {loc} - {name} ({index})"); + var point = area.Point; + gw.WriteLine($" @ ({point.X}, {point.Y}, {point.Z})"); + } + } + + private static bool TryGetBleedArea(IContainsV3f collider, List appearAreas, PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out AppearTuple bledFrom) + { + foreach (var a in appearAreas) + { + var p = a.Point; + if (!EncounterDumperSV.IsContainedBy(collider, p)) + continue; + // Get the original area it bled from. + bledFrom = appearAreas.First(z => + scene.TryGetContainsCheck(fieldIndex, z.AreaName, out var c) && + c.ContainsPoint(p.X, p.Y, p.Z)); + return true; + } + bledFrom = default; + return false; + } + + private static void WriteFixedSymbol(ICollection exist, FixedSymbolTable entry, IReadOnlyList locs, int adjustLevel = 0) + { + var enc = entry.Symbol; + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + + ushort species = SpeciesConverterSV.GetNational9((ushort)enc.DevId); + byte form = (byte)enc.FormId; + if (species == (int)Species.Minior) + { + // Not form random -- keep original form ID. + // Encounter Slots themselves are form random, but the fixed symbols aren't. + form += 7; // Out of battle form ID (without the rocky shields up) + } + + bw.Write(species); + bw.Write(form); + bw.Write((byte)(enc.Level + adjustLevel)); + + bw.Write(enc.TalentType == TalentType.RANDOM ? (byte)0 : (byte)enc.TalentVNum); + bw.Write((byte)enc.GemType); + bw.Write((byte)(enc.Sex - 1)); + bw.Write((byte)(enc.TokuseiIndex switch + { + TokuseiType.RANDOM_12 => PKHeX.Core.AbilityPermission.Any12, + TokuseiType.RANDOM_123 => PKHeX.Core.AbilityPermission.Any12H, + TokuseiType.SET_1 => PKHeX.Core.AbilityPermission.OnlyFirst, + TokuseiType.SET_2 => PKHeX.Core.AbilityPermission.OnlySecond, + TokuseiType.SET_3 => PKHeX.Core.AbilityPermission.OnlyHidden, + _ => throw new ArgumentOutOfRangeException(nameof(TokuseiType), enc.TokuseiIndex, null), + })); + + 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); + } +} diff --git a/pkNX.WinForms/Dumping/SV/Encounter/GimmighoulDumper.cs b/pkNX.WinForms/Dumping/SV/Encounter/GimmighoulDumper.cs new file mode 100644 index 00000000..bb406fe3 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Encounter/GimmighoulDumper.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public static class GimmighoulDumper +{ + public static void Dump(IFileInternal rom, EncounterDumpConfigSV config, PaldeaSceneModel scene) + { + var csym = new PaldeaCoinSymbolModel(rom); + var eventBattle = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin")); + Dump(config, csym, scene, eventBattle); + } + + private static void Dump(EncounterDumpConfigSV config, PaldeaCoinSymbolModel csym, PaldeaSceneModel scene, EventBattlePokemonArray eventBattle) + { + using var cw = File.CreateText(Path.Combine(config.Path, "titan_coin_symbol.txt")); + foreach (var entry in csym.Points[(int)PaldeaFieldIndex.Paldea]) + { + var areas = new List(); + foreach (var areaName in scene.AreaNames[(int)PaldeaFieldIndex.Paldea]) + { + if (scene.PaldeaType[(int)PaldeaFieldIndex.Paldea][areaName] != PaldeaPointPivot.Overworld) + continue; + + var areaInfo = scene.AreaInfos[(int)PaldeaFieldIndex.Paldea][areaName]; + var name = areaInfo.LocationNameMain; + if (string.IsNullOrEmpty(name)) // Don't worry about subzones + continue; + if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) + continue; + + if (scene.IsPointContained(PaldeaFieldIndex.Paldea, 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 = eventBattle.Table.First(e => e.Label == entry.BoxLabel).PokeData; + + cw.WriteLine($" Species: {config[pd.DevId]}"); + cw.WriteLine($" Form: {pd.FormId}"); + cw.WriteLine($" Level: {pd.Level}"); + cw.WriteLine($" Sex: {pd.Sex.Humanize()}"); + cw.WriteLine($" Shiny: {pd.RareType.Humanize()}"); + + 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: {pd.Tokusei.Humanize()}"); + switch (pd.WazaType) + { + case WazaType.DEFAULT: + cw.WriteLine(" Moves: Random"); + break; + case WazaType.MANUAL: + cw.WriteLine($" Moves: {config[pd.Waza1]}/{config[pd.Waza2]}/{config[pd.Waza3]}/{config[pd.Waza4]}"); + break; + } + + cw.WriteLine($" Scale: {pd.ScaleType.Humanize(pd.ScaleValue)}"); + cw.WriteLine($" GemType: {(int)pd.GemType}"); + } + + cw.WriteLine(" Areas:"); + foreach (var areaName in areas) + { + var areaInfo = scene.AreaInfos[(int)PaldeaFieldIndex.Paldea][areaName]; + var loc = areaInfo.LocationNameMain; + var (name, index) = config.PlaceNameMap[loc]; + cw.WriteLine($" - {areaName} - {loc} - {name} ({index})"); + } + } + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs b/pkNX.WinForms/Dumping/SV/Encounter/PaldeaEncounter.cs similarity index 97% rename from pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs rename to pkNX.WinForms/Dumping/SV/Encounter/PaldeaEncounter.cs index 8b051f39..7a6f5ccf 100644 --- a/pkNX.WinForms/Dumping/SV/PaldeaEncounter.cs +++ b/pkNX.WinForms/Dumping/SV/Encounter/PaldeaEncounter.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using pkNX.Structures.FlatBuffers.SV; namespace pkNX.Structures.FlatBuffers; @@ -39,7 +38,7 @@ public static PaldeaEncounter GetBand(EncountPokeData pd, PointData ep, int adju return new(SpeciesConverterSV.GetNational9((ushort)pd.BandPoke), (byte)pd.BandForm, (byte)pd.BandSex, min, max, time); } - public string GetEncountString(IReadOnlyList specNamesInternal) + public string GetEncountString(ReadOnlySpan specNamesInternal) { var species = specNamesInternal[SpeciesConverterSV.GetInternal9(Species)]; return GetString(species); diff --git a/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs b/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs deleted file mode 100644 index 93211c4c..00000000 --- a/pkNX.WinForms/Dumping/SV/EncounterDumperSV.cs +++ /dev/null @@ -1,850 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using pkNX.Containers; -using pkNX.Structures; -using pkNX.Structures.FlatBuffers; -using pkNX.Structures.FlatBuffers.SV; - -namespace pkNX.WinForms; - -public class EncounterDumperSV(IFileInternal rom) -{ - private const float tolX = 30f; - private const float tolY = 30f; - private const float tolZ = 30f; - - private static ReadOnlySpan AllMaps => - [ - PaldeaFieldIndex.Paldea, - PaldeaFieldIndex.Kitakami, - PaldeaFieldIndex.Terarium, - ]; - - public void DumpTo(string path, IReadOnlyList specNamesInternal, 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 su1EncPoints = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su1.bin")); - var su2EncPoints = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su2.bin")); - //var lcEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_lc.bin")); - var pokeDataMain = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin")); - var pokeDataSu1 = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su1/pokedata_su1_array.bin")); - var pokeDataSu2 = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su2/pokedata_su2_array.bin")); - //var pokeDataLc = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/pokedata/pokedata_lc/pokedata_lc_array.bfbs")); - - var db = new LocationDatabase(); - - // 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); - var pointSu1 = ReformatPoints(su1EncPoints); - var pointSu2 = ReformatPoints(su2EncPoints); - //var lc = ReformatPoints(lcEncPoints); - - // Process all field indices - ProcessAreas(PaldeaFieldIndex.Paldea); - ProcessAreas(PaldeaFieldIndex.Kitakami); - ProcessAreas(PaldeaFieldIndex.Terarium); - - // 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(); - if (storage.AreaName == "a_su0104") - 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, specNamesInternal, placeNameMap, s.AreaName, s.AreaInfo, s.Slots); - using var tw = File.CreateText(Path.Combine(path, "titan_loc_enc.txt")); - WriteLocEncList(tw, specNamesInternal, 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 = []; - 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")); - foreach (var fieldIndex in AllMaps) - { - var areaNames = scene.AreaNames[(int)fieldIndex]; - var areas = scene.AreaInfos[(int)fieldIndex]; - var atlantis = scene.PaldeaType[(int)fieldIndex]; - var allPoints = gamePoints[(int)fieldIndex]; - - if (fieldIndex is PaldeaFieldIndex.Kitakami) - { - areas = areas.Where(z => z.Value.AdjustEncLv != 0) - .ToDictionary(z => z.Key, z => z.Value); - areaNames = [.. areas.Keys]; - } - - for (var i = 0; i < fsymData.Table.Count; i++) - { - var entry = fsymData.Table[i]; - var tableKey = entry.TableKey; - if (tableKey.StartsWith("su2_w23d10_")) - continue; // handle later, manually - var points = allPoints.FindAll(p => p.TableKey == tableKey); - if (points.Count == 0) - continue; - - var appearAreas = new List(); - - if (!FindArea(AreaType.Cave)) - FindArea(AreaType.Default); - - bool FindArea(AreaType type) - { - for (var x = areaNames.Count - 1; x >= 0; x--) - { - var areaName = areaNames[x]; - if (atlantis[areaName] != PaldeaPointPivot.Overworld) - continue; - - var areaInfo = areas[areaName]; - if (type != AreaType.Default && areaInfo.Type != type) - continue; - if (areaInfo.Tag is AreaTag.NG_Encount) - continue; - if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) - continue; - for (int p = 0; p < points.Count; p++) - { - var point = points[p]; - var pt = point.Position; - if (!collider.ContainsPoint(pt.X, pt.Y, pt.Z)) - continue; - var tmp = areaName; - if (!TryGetPlaceName(ref tmp, areaInfo, pt, placeNameMap, areas, scene, fieldIndex, out var placeName)) - continue; - if (!appearAreas.Exists(z => z.Point == pt && z.AreaName == tmp)) - appearAreas.Add(new(placeName, tmp, areaInfo.AdjustEncLv, pt)); - points.RemoveAt(p); - p--; - } - - if (points.Count == 0) - return true; - } - return false; - } - - WriteFixedSpawn(specNamesInternal, moveNames, placeNameMap, gw, tableKey, i, entry, appearAreas); - - // Serialize - if (IsAvoidantAction(entry.PokeAI.ActionId)) - continue; // not actually encounter-able, they're just for spectacle - // var locs = appearAreas.Select(a => placeNameMap[a.PlaceName].Index); - if (appearAreas.Count == 0) - continue; - - // If not stationary, allow some tolerance. - var aiStationary = GetIsStationary(entry.PokeAI.ActionId); - var wanderAreas = new List(0); - if (!aiStationary) - { - var allInfos = scene.AreaInfos[(int)fieldIndex]; - var allNames = scene.AreaNames[(int)fieldIndex]; - for (var a = 0; a < allNames.Count; a++) - { - var areaName = allNames[a]; - if (atlantis[areaName] != PaldeaPointPivot.Overworld) - continue; - - var areaInfo = allInfos[areaName]; - if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) - continue; - if (areaInfo.Tag is AreaTag.NG_Encount) - continue; - if (!TryGetBleedArea(collider, appearAreas, scene, fieldIndex, out var bledFrom)) - continue; // can't bleed into this area - - // Bleeding from zones should start from the sub-zone, so we don't need to check other sub-zones. - if (!TryGetPlaceName(ref areaName, areaInfo, collider, placeNameMap, allInfos, scene, fieldIndex, out var placeName)) - continue; - wanderAreas.Add(bledFrom with { PlaceName = placeName, AreaName = allNames[a] }); - } - } - - foreach (var area in appearAreas.GroupBy(z => z.Adjust)) - { - var adjust = area.Key; - var wander = wanderAreas.Where(z => z.Adjust == adjust); - var allLocations = area.Concat(wander); - var locationIDs = allLocations.Select(z => placeNameMap[z.PlaceName].Index).Distinct().ToList(); - if (fieldIndex == PaldeaFieldIndex.Paldea && entry.PokeAI.ActionId == PokemonActionID.FS_POP_AREA22_DRAGONITE) // Flies around not using tolerance. - locationIDs.Add(46); // North Province (Area One) - locationIDs.Sort(); - - WriteFixedSymbol(serialized, entry, locationIDs); - if (adjust != 0) - WriteFixedSymbol(serialized, entry, locationIDs, adjust); - } - } - } - - var underDepths = Array.FindIndex(fsymData.Table.ToArray(), x => x.TableKey == "su2_w23d10_01"); - for (int i = underDepths; i < underDepths + 23; i++) - { - var entry = fsymData.Table[i]; - var tableKey = entry.TableKey; - var appearAreas = new List { new("PLACENAME_a_w23_d10_01", "a_w23_d10_subarea", 0, new()) }; - WriteFixedSpawn(specNamesInternal, moveNames, placeNameMap, gw, tableKey, i, entry, appearAreas); - WriteFixedSymbol(serialized, entry, [196]); - } - } - - // Gimmighoul only in Paldea - using var cw = File.CreateText(Path.Combine(path, "titan_coin_symbol.txt")); - foreach (var entry in csym.Points[(int)PaldeaFieldIndex.Paldea]) - { - var areas = new List(); - foreach (var areaName in scene.AreaNames[(int)PaldeaFieldIndex.Paldea]) - { - if (scene.PaldeaType[(int)PaldeaFieldIndex.Paldea][areaName] != PaldeaPointPivot.Overworld) - continue; - - var areaInfo = scene.AreaInfos[(int)PaldeaFieldIndex.Paldea][areaName]; - var name = areaInfo.LocationNameMain; - if (string.IsNullOrEmpty(name)) // Don't worry about subzones - continue; - if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) - continue; - - if (scene.IsPointContained(PaldeaFieldIndex.Paldea, 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 = eventBattle.Table.First(e => e.Label == entry.BoxLabel).PokeData; - - cw.WriteLine($" Species: {specNamesInternal[(int)pd.DevId]}"); - cw.WriteLine($" Form: {pd.FormId}"); - cw.WriteLine($" Level: {pd.Level}"); - cw.WriteLine($" Sex: {Humanize(pd.Sex)}"); - cw.WriteLine($" Shiny: {Humanize(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: {Humanize(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: {Humanize(pd.ScaleType, pd.ScaleValue)}"); - cw.WriteLine($" GemType: {(int)pd.GemType}"); - } - - cw.WriteLine(" Areas:"); - foreach (var areaName in areas) - { - var areaInfo = scene.AreaInfos[(int)PaldeaFieldIndex.Paldea][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()); - DumpScene(scene, path); - DumpField(field, path); - return; - - // HELPERS - LocationPointDetail[] GetPoints(PaldeaFieldIndex fieldIndex, PaldeaPointPivot type) => fieldIndex switch - { - PaldeaFieldIndex.Paldea => type switch - { - PaldeaPointPivot.Overworld => pointMain, - PaldeaPointPivot.AreaZero => pointAtlantis, - _ => throw new ArgumentException($"Could not handle {type}"), - }, - PaldeaFieldIndex.Kitakami => pointSu1, - PaldeaFieldIndex.Terarium => pointSu2, - _ => throw new ArgumentException($"Could not handle {fieldIndex}"), - }; - - EncountPokeDataArray GetPokeData(PaldeaFieldIndex fieldIndex) => fieldIndex switch - { - PaldeaFieldIndex.Paldea => pokeDataMain, - PaldeaFieldIndex.Kitakami => pokeDataSu1, - PaldeaFieldIndex.Terarium => pokeDataSu2, - _ => throw new ArgumentException($"Could not handle {fieldIndex}"), - }; - - void ProcessAreas(PaldeaFieldIndex fieldIndex) - { - // 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. - - // Fill the point lists for each area, then spawn everything into those points. - var areaNames = scene.AreaNames[(int)fieldIndex]; - var areas = scene.AreaInfos[(int)fieldIndex]; - var types = scene.PaldeaType[(int)fieldIndex]; - for (var i = areaNames.Count - 1; i >= 0; i--) - { - var areaName = areaNames[i]; - - // Determine potential spawners - if (!scene.TryGetContainsCheck(fieldIndex, areaName, out var collider)) - { - Console.WriteLine($"No collider for {areaName}"); - continue; - } - - var areaInfo = areas[areaName]; - // Areas without a location name can't be reversed into location ID. - if (!TryGetPlaceName(ref areaName, areaInfo, collider, placeNameMap, areas, scene, fieldIndex, out var placeName)) - continue; - - // Locations that do not spawn encounters can still have crossovers bleed into them. - // We'll have empty local encounter lists for them. - var storage = db.Get(placeNameMap[placeName].Index, fieldIndex, areaName, areas[areaName]); - if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All) - continue; - - var type = types[areaName]; - var points = GetPoints(fieldIndex, type); - storage.LoadPoints(points, collider, areaInfo.ActualMinLevel, areaInfo.ActualMaxLevel, areaInfo.AdjustEncLv); - storage.GetEncounters(GetPokeData(fieldIndex), 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 areaNames) - { - // Same sanity checking as above iteration. - var areaInfo = areas[areaName]; - - // Determine potential spawners - if (!scene.TryGetContainsCheck(fieldIndex, 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, fieldIndex, areaName, areaInfo); - if (!IsCrossoverAllowed(storage)) - continue; - - // 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 areaNames) - { - // Skip self - if (otherName == areaName) - continue; - // Skip areas that don't have a location name -- subzones were the initial spawn spot if so. - var otherAreaInfo = areas[otherName]; - var otherNameMain = otherAreaInfo.LocationNameMain; - if (string.IsNullOrEmpty(otherNameMain)) - continue; - - // Skip areas that don't have a collider - if (!scene.TryGetContainsCheck(fieldIndex, otherName, out _)) - continue; - - // Iterate over all crossover points in the other area. - var cross = db.Get(placeNameMap[otherNameMain].Index, fieldIndex, otherName, otherAreaInfo); - if (!IsCrossoverAllowed(cross)) - continue; - 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); - } - } - } - } - } - - private static void WriteFixedSpawn(IReadOnlyList specNamesInternal, IReadOnlyList moveNames, Dictionary placeNameMap, - StreamWriter gw, string tableKey, int i, FixedSymbolTable entry, List appearAreas) - { - gw.WriteLine("==="); - gw.WriteLine($"{tableKey} - {i}"); - gw.WriteLine("==="); - gw.WriteLine(" PokeData:"); - var pd = entry.Symbol; - gw.WriteLine($" Species: {specNamesInternal[(int)pd.DevId]}"); - gw.WriteLine($" Form: {pd.FormId}"); - gw.Write($" Level: {pd.Level}"); - foreach (var adj in appearAreas.Select(a => a.Adjust).Where(lv => lv != 0).Distinct().Order()) - { - gw.Write($", {pd.Level + adj}"); - } - gw.WriteLine(); - gw.WriteLine($" Sex: {Humanize(pd.Sex)}"); - gw.WriteLine($" Shiny: {Humanize(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: {Humanize(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: {Humanize(pd.ScaleType, pd.ScaleValue)}"); - gw.WriteLine($" GemType: {(int)pd.GemType}"); - - gw.WriteLine($" Probability: {entry.PokeGeneration.RepopProbability}"); - gw.WriteLine($" Pattern: {entry.PokeGeneration.GenerationPattern}"); - gw.WriteLine($" Scenario: {entry.PokeGeneration.RequireScenarioId}"); - gw.WriteLine($" Action: {entry.PokeAI.ActionId}"); - gw.WriteLine($" Trigger: {entry.PokeAI.TriggerActionId}"); - - gw.WriteLine(" Areas:"); - foreach (var area in appearAreas) - { - var loc = area.PlaceName; - (string name, int index) = placeNameMap[loc]; - gw.WriteLine($" - {area.PlaceName} - {loc} - {name} ({index})"); - var point = area.Point; - gw.WriteLine($" @ ({point.X}, {point.Y}, {point.Z})"); - } - } - - private static bool TryGetBleedArea(IContainsV3f collider, List appearAreas, PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out AppearTuple bledFrom) - { - foreach (var a in appearAreas) - { - var p = a.Point; - if (!collider.ContainsPoint(p.X, p.Y, p.Z, tolX, tolY, tolZ)) - continue; - // Get the original area it bled from. - bledFrom = appearAreas.First(z => - scene.TryGetContainsCheck(fieldIndex, z.AreaName, out var c) && - c.ContainsPoint(p.X, p.Y, p.Z)); - return true; - } - bledFrom = default; - return false; - } - - private static bool TryGetPlaceName(ref string areaName, AreaInfo areaInfo, - IContainsV3f collider, Dictionary placeNameMap, - IReadOnlyDictionary areas, - PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out string placeName) - { - placeName = areaInfo.LocationNameMain; - if (!string.IsNullOrEmpty(placeName)) - return true; - - // Maybe this is a sub-area? Try to get the parent area name. - bool IsValidParentAreaName(string aName) - { - if (!areas.TryGetValue(aName, out var info)) - return false; - var n = info.LocationNameMain; - if (string.IsNullOrEmpty(n)) - return false; - return placeNameMap.ContainsKey(n); - } - - if (!scene.TryGetParentAreaName(fieldIndex, areaName, collider, IsValidParentAreaName, out var parentAreaName)) - { - Console.WriteLine($"No parent area for {areaName}"); - return false; - } - - areaName = parentAreaName; - placeName = areas[parentAreaName].LocationNameMain; - return true; - } - - public static bool TryGetPlaceName(ref string areaName, AreaInfo areaInfo, - PackedVec3f point, Dictionary placeNameMap, - IReadOnlyDictionary areas, - PaldeaSceneModel scene, PaldeaFieldIndex fieldIndex, out string placeName) - { - placeName = areaInfo.LocationNameMain; - if (!string.IsNullOrEmpty(placeName)) - return true; - - // Maybe this is a sub-area? Try to get the parent area name. - bool IsValidParentAreaName(string aName) - { - if (!areas.TryGetValue(aName, out var info)) - return false; - var n = info.LocationNameMain; - if (string.IsNullOrEmpty(n)) - return false; - return placeNameMap.ContainsKey(n); - } - - if (!scene.TryGetParentAreaName(fieldIndex, areaName, point, IsValidParentAreaName, out var parentAreaName)) - { - Console.WriteLine($"No parent area for {areaName}"); - return false; - } - - areaName = parentAreaName; - placeName = areas[parentAreaName].LocationNameMain; - return true; - } - - private record struct AppearTuple(string PlaceName, string AreaName, int Adjust, PackedVec3f Point); - - private static string Humanize(SizeType type, short value) => type switch - { - SizeType.RANDOM => "Random", - SizeType.XS => "XS", - SizeType.S => "S", - SizeType.M => "M", - SizeType.L => "L", - SizeType.XL => "XL", - SizeType.VALUE => value.ToString(), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - - private static string Humanize(RareType type) => type switch - { - RareType.DEFAULT => "Random", - RareType.NO_RARE => "Never", - RareType.RARE => "Always", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - - private static string Humanize(SexType type) => type switch - { - SexType.DEFAULT => "Random", - SexType.MALE => "Male", - SexType.FEMALE => "Female", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - - private static string Humanize(TokuseiType type) => type switch - { - TokuseiType.RANDOM_12 => "1/2", - TokuseiType.RANDOM_123 => "1/2/H", - TokuseiType.SET_1 => "1", - TokuseiType.SET_2 => "2", - TokuseiType.SET_3 => "H", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Invalid ability index"), - }; - - private static void DumpScene(PaldeaSceneModel scene, string path) - { - // Dump each property to json. - var dest = Path.Combine(path, "paldea_scene.json"); - var json = JsonSerializer.Serialize(scene, new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); - File.WriteAllText(dest, json); - } - - private static void DumpField(PaldeaFieldModel field, string path) - { - // Dump each property to json. - var dest = Path.Combine(path, "paldea_field.json"); - var json = JsonSerializer.Serialize(field, new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); - File.WriteAllText(dest, json); - } - - /// - /// Allow encounters into and out of this location - /// - private static bool IsCrossoverAllowed(LocationStorage storage) - { - var loc = storage.Location; - return IsCrossoverAllowed(loc); - } - - public static bool IsCrossoverAllowed(int MetLocation) => MetLocation switch - { - 8 => false, // Mesagoza - 124 => false, // Area Zero - _ => true, - }; - - private static bool GetIsStationary(PokemonActionID action) => action switch - { - PokemonActionID.FS_POP_ALWAYS_GAZE_BIRD_TARGET_PLAYER => true, - PokemonActionID.FS_POP_ALWAYS_GAZE_TARGET_PLAYER => true, - PokemonActionID.ALWAYS_GAZE_BIRD_TARGET_PLAYER_LOOP => true, - PokemonActionID.FS_POP_LAND_SLEEPING_CURRENT_LOCATION => true, - PokemonActionID.FS_POP_LEVITATION_SLEEPING_TREE_BRANCH => true, - PokemonActionID.FS_POP_AREA22_DRAGONITE => true, // Handle separately. - _ => false, - }; - - private static LocationPointDetail[] ReformatPoints(PointDataArray all) - { - var arr = all.Table; - var result = new LocationPointDetail[arr.Count]; - for (int i = 0; i < arr.Count; i++) - result[i] = new LocationPointDetail(arr[i]); - return result; - } - - private static void WriteFixedSymbol(ICollection exist, FixedSymbolTable entry, IReadOnlyList locs, int adjustLevel = 0) - { - var enc = entry.Symbol; - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); - - ushort species = SpeciesConverterSV.GetNational9((ushort)enc.DevId); - byte form = (byte)enc.FormId; - if (species == (int)Species.Minior) - { - // Not form random -- keep original form ID. - // Encounter Slots themselves are form random, but the fixed symbols aren't. - form += 7; // Out of battle form ID (without the rocky shields up) - } - - bw.Write(species); - bw.Write(form); - bw.Write((byte)(enc.Level + adjustLevel)); - - bw.Write(enc.TalentType == TalentType.RANDOM ? (byte)0 : (byte)enc.TalentVNum); - bw.Write((byte)enc.GemType); - bw.Write((byte)(enc.Sex - 1)); - bw.Write((byte)(enc.TokuseiIndex switch - { - TokuseiType.RANDOM_12 => PKHeX.Core.AbilityPermission.Any12, - TokuseiType.RANDOM_123 => PKHeX.Core.AbilityPermission.Any12H, - TokuseiType.SET_1 => PKHeX.Core.AbilityPermission.OnlyFirst, - TokuseiType.SET_2 => PKHeX.Core.AbilityPermission.OnlySecond, - TokuseiType.SET_3 => PKHeX.Core.AbilityPermission.OnlyHidden, - _ => throw new ArgumentOutOfRangeException(nameof(TokuseiType), enc.TokuseiIndex, null), - })); - - 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) - { - ushort species = slot.Species; - byte form = slot.Species switch - { - (ushort)Species.Vivillon or (ushort)Species.Spewpa or (ushort)Species.Scatterbug => 30, - (ushort)Species.Minior => 31, - _ => slot.Form, - }; - - // ReSharper disable RedundantCast - bw.Write(species); - bw.Write(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); - // ReSharper restore RedundantCast - } - return ms.ToArray(); - } - - private static void WriteLocation(TextWriter tw, IReadOnlyList specNamesInternal, - IReadOnlyDictionary placeNameMap, - string areaName, AreaInfo areaInfo, List encounts) - { - var loc = areaInfo.LocationNameMain; - if (!placeNameMap.ContainsKey(loc)) - return; - (string name, int index) = placeNameMap[loc]; - var heading = $"{areaName} - {loc} - {name} ({index})"; - WriteEncounts(tw, specNamesInternal, heading, encounts); - } - - private static void WriteLocEncList(TextWriter tw, IReadOnlyList specNamesInternal, - 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, specNamesInternal, 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()).Order(); - 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 specNamesInternal, string heading, IEnumerable encounts) - { - tw.WriteLine("==="); - tw.WriteLine(heading); - tw.WriteLine("==="); - foreach (var e in encounts) - tw.WriteLine($" - {e.GetEncountString(specNamesInternal)}"); - 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; - } - - static bool IsAvoidantAction(PokemonActionID action) => action switch - { - PokemonActionID.FS_POP_PATH_RUN_SPEED_UP_1_NOT_COL_DELAY => true, // lighthouse Wingull (South) and Wattrel (East, West) - PokemonActionID.FS_POP_PATH_RUN_SPEED_UP_1_NOT_COL_DESTROY => true, // fence Wingull (Cabo Poco) - PokemonActionID.FS_POP_COMMON_FLIGHT_NOT_COL_DESTROY => true, // starting Fletchling (level 2) - _ => false, - }; -} diff --git a/pkNX.WinForms/Dumping/SV/LocationDatabase.cs b/pkNX.WinForms/Dumping/SV/Location/LocationDatabase.cs similarity index 100% rename from pkNX.WinForms/Dumping/SV/LocationDatabase.cs rename to pkNX.WinForms/Dumping/SV/Location/LocationDatabase.cs diff --git a/pkNX.WinForms/Dumping/SV/LocationPointDetail.cs b/pkNX.WinForms/Dumping/SV/Location/LocationPointDetail.cs similarity index 100% rename from pkNX.WinForms/Dumping/SV/LocationPointDetail.cs rename to pkNX.WinForms/Dumping/SV/Location/LocationPointDetail.cs diff --git a/pkNX.WinForms/Dumping/SV/LocationStorage.cs b/pkNX.WinForms/Dumping/SV/Location/LocationStorage.cs similarity index 100% rename from pkNX.WinForms/Dumping/SV/LocationStorage.cs rename to pkNX.WinForms/Dumping/SV/Location/LocationStorage.cs diff --git a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolModel.cs similarity index 98% rename from pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs rename to pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolModel.cs index 82ad5bc2..328aa420 100644 --- a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolModel.cs +++ b/pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolModel.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using pkNX.Containers; -using pkNX.Structures.FlatBuffers; using pkNX.Structures.FlatBuffers.SV.Trinity; -namespace pkNX.WinForms; +namespace pkNX.Structures.FlatBuffers; public class PaldeaCoinSymbolModel { diff --git a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolPoint.cs similarity index 85% rename from pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs rename to pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolPoint.cs index 722d8750..f88ab956 100644 --- a/pkNX.WinForms/Dumping/SV/PaldeaCoinSymbolPoint.cs +++ b/pkNX.WinForms/Dumping/SV/Models/PaldeaCoinSymbolPoint.cs @@ -1,6 +1,4 @@ -using pkNX.Structures.FlatBuffers; - -namespace pkNX.WinForms; +namespace pkNX.Structures.FlatBuffers; public class PaldeaCoinSymbolPoint(string name, ulong num, string boxLabel, PackedVec3f pos) { diff --git a/pkNX.WinForms/Dumping/SV/PaldeaFieldModel.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaFieldModel.cs similarity index 100% rename from pkNX.WinForms/Dumping/SV/PaldeaFieldModel.cs rename to pkNX.WinForms/Dumping/SV/Models/PaldeaFieldModel.cs diff --git a/pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolModel.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolModel.cs new file mode 100644 index 00000000..cd46506c --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolModel.cs @@ -0,0 +1,157 @@ +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; +using pkNX.Structures.FlatBuffers.SV.Trinity; +using System; +using System.Collections.Generic; +using System.Linq; +using FlatSharp; + +namespace pkNX.Structures.FlatBuffers; + +public class PaldeaFixedSymbolModel +{ + public readonly List[] PointsScarlet; + public readonly List[] PointsViolet; + + public readonly Dictionary MultiSpawner; + + private static T Get(Memory data) where T : class, IFlatBufferSerializable + => FlatBufferConverter.DeserializeFrom(data); + + public PaldeaFixedSymbolModel(IFileInternal rom) + { + var raw = rom.GetPackedFile("world/data/field/fixed_symbol/gem_symbol_lottery_table/gem_symbol_lottery_table_array.bin"); + var gemLottery = Get(raw).Table; + MultiSpawner = gemLottery.ToDictionary(z => z.LotteryKey, GetTableGetKeys); + + PointsScarlet = GetSymbols(rom, 0); + PointsViolet = GetSymbols(rom, 1); + } + + private List[] GetSymbols(IFileInternal rom, int game) => + [ + [..GetSymbols(rom, game, "world")], + [..GetSymbols(rom, game, "su1_world")], + [..GetSymbols(rom, game, "su2_world")], + ]; + + private IEnumerable GetSymbols(IFileInternal rom, int game, string mapName) + { + var data = rom.GetPackedFile($"world/scene/parts/field/streaming_event/{mapName}_fixed_placement_symbol_/{mapName}_fixed_placement_symbol_{game}.trscn"); + var template = Get(data); + return GetObjectTemplateSymbolPoints(template); + } + + private static string[] GetTableGetKeys(GemSymbolLotteryTable entry) + { + var list = new List(); + if (!string.IsNullOrWhiteSpace(entry.TableKey0)) list.Add(entry.TableKey0); + if (!string.IsNullOrWhiteSpace(entry.TableKey1)) list.Add(entry.TableKey1); + if (!string.IsNullOrWhiteSpace(entry.TableKey2)) list.Add(entry.TableKey2); + if (!string.IsNullOrWhiteSpace(entry.TableKey3)) list.Add(entry.TableKey3); + if (!string.IsNullOrWhiteSpace(entry.TableKey4)) list.Add(entry.TableKey4); + if (!string.IsNullOrWhiteSpace(entry.TableKey5)) list.Add(entry.TableKey5); + if (!string.IsNullOrWhiteSpace(entry.TableKey6)) list.Add(entry.TableKey6); + if (!string.IsNullOrWhiteSpace(entry.TableKey7)) list.Add(entry.TableKey7); + if (!string.IsNullOrWhiteSpace(entry.TableKey8)) list.Add(entry.TableKey8); + if (!string.IsNullOrWhiteSpace(entry.TableKey9)) list.Add(entry.TableKey9); + return [.. list]; + } + + private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplate template) + { + foreach (var obj in template.Objects) + { + switch (obj.Type) + { + case "trinity_ObjectTemplate": + { + var sObj = Get(obj.Data); + if (sObj.Type != "trinity_ScenePoint") + break; + var scenePoint = Get(sObj.Data); + + foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) + yield return f; + break; + } + case "trinity_ScenePoint": + { + var scenePoint = Get(obj.Data); + + foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects)) + yield return f; + break; + } + } + } + } + + private IEnumerable GetScenePointSymbolPoints(TrinityScenePoint scenePoint, IList subObjects) + { + // Handle SubObjects + foreach (var obj in subObjects) + { + switch (obj.Type) + { + case "trinity_PropertySheet": + { + var propSheet = Get(obj.Data); + if (propSheet.Name != "fixed_symbol_point") + break; + + var tableKey = GetTableKey(propSheet); + if (string.IsNullOrEmpty(tableKey)) + break; + + var position = scenePoint.Position; + if (MultiSpawner.TryGetValue(tableKey, out var others)) + { + // Can spawn multiple fixed encounters. + foreach (var other in others) + yield return new PaldeaFixedSymbolPoint(other, position); + } + else + { + yield return new PaldeaFixedSymbolPoint(tableKey, position); + } + break; + } + case "trinity_ScenePoint": + { + var subScenePoint = Get(obj.Data); + foreach (var f in GetScenePointSymbolPoints(subScenePoint, obj.SubObjects)) + yield return f; + break; + } + case "trinity_ObjectTemplate": + { + var ssObj = Get(obj.Data); + if (ssObj.Type != "trinity_ScenePoint") + break; + var subScenePoint = Get(ssObj.Data); + + foreach (var f in GetScenePointSymbolPoints(subScenePoint, obj.SubObjects)) + yield return f; + break; + } + default: + throw new ArgumentException($"Unknown SubObject {obj.Type}"); + } + } + } + + public static string GetTableKey(TrinityPropertySheet 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.Item3 is not { } 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/Models/PaldeaFixedSymbolPoint.cs similarity index 77% rename from pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolPoint.cs rename to pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolPoint.cs index d1ff7456..47648836 100644 --- a/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolPoint.cs +++ b/pkNX.WinForms/Dumping/SV/Models/PaldeaFixedSymbolPoint.cs @@ -1,6 +1,4 @@ -using pkNX.Structures.FlatBuffers; - -namespace pkNX.WinForms; +namespace pkNX.Structures.FlatBuffers; public class PaldeaFixedSymbolPoint(string key, PackedVec3f pos) { diff --git a/pkNX.WinForms/Dumping/SV/PaldeaSceneModel.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaSceneModel.cs similarity index 100% rename from pkNX.WinForms/Dumping/SV/PaldeaSceneModel.cs rename to pkNX.WinForms/Dumping/SV/Models/PaldeaSceneModel.cs diff --git a/pkNX.WinForms/Dumping/SV/Models/PaldeaSpawnModel.cs b/pkNX.WinForms/Dumping/SV/Models/PaldeaSpawnModel.cs new file mode 100644 index 00000000..73245922 --- /dev/null +++ b/pkNX.WinForms/Dumping/SV/Models/PaldeaSpawnModel.cs @@ -0,0 +1,66 @@ +using System; +using pkNX.Containers; +using pkNX.Structures.FlatBuffers.SV; + +namespace pkNX.Structures.FlatBuffers; + +public record PaldeaSpawnModel +{ + public PaldeaSpawnSet Main { get; init; } + public PaldeaSpawnSet Atlantis { get; init; } + public PaldeaSpawnSet Kitakami { get; init; } + public PaldeaSpawnSet Terarium { get; init; } + + public PaldeaSpawnModel(IFileInternal 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 su1EncPoints = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su1.bin")); + var su2EncPoints = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su2.bin")); + //var lcEncPoints = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_lc.bin")); + var pokeDataMain = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin")); + var pokeDataSu1 = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su1/pokedata_su1_array.bin")); + var pokeDataSu2 = FlatBufferConverter.DeserializeFrom(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su2/pokedata_su2_array.bin")); + //var pokeDataLc = FlatBufferConverter.DeserializeFrom(ROM.GetPackedFile("world/data/encount/pokedata/pokedata_lc/pokedata_lc_array.bfbs")); + + Main = new PaldeaSpawnSet(pokeDataMain, mlEncPoints); + Atlantis = new PaldeaSpawnSet(pokeDataMain, alEncPoints); + Kitakami = new PaldeaSpawnSet(pokeDataSu1, su1EncPoints); + Terarium = new PaldeaSpawnSet(pokeDataSu2, su2EncPoints); + //var lc = ReformatPoints(lcEncPoints); + } + + public PaldeaSpawnSet GetSet(PaldeaFieldIndex fieldIndex, PaldeaPointPivot type) => fieldIndex switch + { + PaldeaFieldIndex.Paldea => type switch + { + PaldeaPointPivot.Overworld => Main, + PaldeaPointPivot.AreaZero => Atlantis, + _ => throw new ArgumentException($"Could not handle {type}"), + }, + PaldeaFieldIndex.Kitakami => Kitakami, + PaldeaFieldIndex.Terarium => Terarium, + _ => throw new ArgumentException($"Could not handle {fieldIndex}"), + }; + + public LocationPointDetail[] GetPoints(PaldeaFieldIndex fieldIndex, PaldeaPointPivot type) => GetSet(fieldIndex, type).Points; + public EncountPokeDataArray GetPokeData(PaldeaFieldIndex fieldIndex) => GetSet(fieldIndex, PaldeaPointPivot.Overworld).Criteria; + + public record PaldeaSpawnSet(EncountPokeDataArray Criteria, LocationPointDetail[] Points) + { + public PaldeaSpawnSet(EncountPokeDataArray criteria, PointDataArray points) : this(criteria, ReformatPoints(points)) + { + } + + // Points can be used by multiple areas as crossover sources. Need to be able to "belong" to multiple areas, and indicate their parent area. + private static LocationPointDetail[] ReformatPoints(PointDataArray all) + { + var arr = all.Table; + var result = new LocationPointDetail[arr.Count]; + for (int i = 0; i < arr.Count; i++) + result[i] = new LocationPointDetail(arr[i]); + return result; + } + } +} diff --git a/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs b/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs deleted file mode 100644 index 80a92d70..00000000 --- a/pkNX.WinForms/Dumping/SV/PaldeaFixedSymbolModel.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using pkNX.Containers; -using pkNX.Structures.FlatBuffers; -using pkNX.Structures.FlatBuffers.SV; -using pkNX.Structures.FlatBuffers.SV.Trinity; - -namespace pkNX.WinForms; - -public class PaldeaFixedSymbolModel -{ - public readonly List[] scarletPoints = [[], [], []]; - public readonly List[] violetPoints = [[], [], []]; - - public readonly Dictionary MultiSpawner; - - public PaldeaFixedSymbolModel(IFileInternal ROM) - { - var gemLottery = FlatBufferConverter.DeserializeFrom( - ROM.GetPackedFile("world/data/field/fixed_symbol/gem_symbol_lottery_table/gem_symbol_lottery_table_array.bin")).Table; - MultiSpawner = gemLottery.ToDictionary(z => z.LotteryKey, GetTableGetKeys); - - // Paldea - 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[(int)PaldeaFieldIndex.Paldea].AddRange(GetObjectTemplateSymbolPoints(p0)); - violetPoints[(int)PaldeaFieldIndex.Paldea].AddRange(GetObjectTemplateSymbolPoints(p1)); - - // Kitakami - var k0Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/su1_world_fixed_placement_symbol_/su1_world_fixed_placement_symbol_0.trscn"); - var k1Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/su1_world_fixed_placement_symbol_/su1_world_fixed_placement_symbol_1.trscn"); - - var k0 = FlatBufferConverter.DeserializeFrom(k0Data); - var k1 = FlatBufferConverter.DeserializeFrom(k1Data); - - scarletPoints[(int)PaldeaFieldIndex.Kitakami].AddRange(GetObjectTemplateSymbolPoints(k0)); - violetPoints[(int)PaldeaFieldIndex.Kitakami].AddRange(GetObjectTemplateSymbolPoints(k1)); - - // Terarium - var b0Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/su2_world_fixed_placement_symbol_/su2_world_fixed_placement_symbol_0.trscn"); - var b1Data = ROM.GetPackedFile("world/scene/parts/field/streaming_event/su2_world_fixed_placement_symbol_/su2_world_fixed_placement_symbol_1.trscn"); - - var b0 = FlatBufferConverter.DeserializeFrom(b0Data); - var b1 = FlatBufferConverter.DeserializeFrom(b1Data); - - scarletPoints[(int)PaldeaFieldIndex.Terarium].AddRange(GetObjectTemplateSymbolPoints(b0)); - violetPoints[(int)PaldeaFieldIndex.Terarium].AddRange(GetObjectTemplateSymbolPoints(b1)); - - // Underdepths - //var u0Data = ROM.GetPackedFile("world/scene/parts/field/room/a_w23_d10/a_w23_d10_event/a_w23_d10_fixed_placement_symbol_/a_w23_d10_fixed_placement_symbol_0.trscn"); - //var u1Data = ROM.GetPackedFile("world/scene/parts/field/room/a_w23_d10/a_w23_d10_event/a_w23_d10_fixed_placement_symbol_/a_w23_d10_fixed_placement_symbol_1.trscn"); - //var u0 = FlatBufferConverter.DeserializeFrom(u0Data); - //var u1 = FlatBufferConverter.DeserializeFrom(u1Data); - // - //scarletPoints[(int)PaldeaFieldIndex.Paldea].AddRange(GetObjectTemplateSymbolPoints(u0)); - //violetPoints[(int)PaldeaFieldIndex.Paldea].AddRange(GetObjectTemplateSymbolPoints(u1)); - } - - private static string[] GetTableGetKeys(GemSymbolLotteryTable entry) - { - var list = new List(); - if (!string.IsNullOrWhiteSpace(entry.TableKey0)) list.Add(entry.TableKey0); - if (!string.IsNullOrWhiteSpace(entry.TableKey1)) list.Add(entry.TableKey1); - if (!string.IsNullOrWhiteSpace(entry.TableKey2)) list.Add(entry.TableKey2); - if (!string.IsNullOrWhiteSpace(entry.TableKey3)) list.Add(entry.TableKey3); - if (!string.IsNullOrWhiteSpace(entry.TableKey4)) list.Add(entry.TableKey4); - if (!string.IsNullOrWhiteSpace(entry.TableKey5)) list.Add(entry.TableKey5); - if (!string.IsNullOrWhiteSpace(entry.TableKey6)) list.Add(entry.TableKey6); - if (!string.IsNullOrWhiteSpace(entry.TableKey7)) list.Add(entry.TableKey7); - if (!string.IsNullOrWhiteSpace(entry.TableKey8)) list.Add(entry.TableKey8); - if (!string.IsNullOrWhiteSpace(entry.TableKey9)) list.Add(entry.TableKey9); - return [.. list]; - } - - private IEnumerable GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplate 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(TrinityScenePoint scenePoint, IList subObjects) - { - // Handle SubObjects - for (var i = 0; i < subObjects.Count; 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)) - { - if (MultiSpawner.TryGetValue(tableKey, out var others)) - { - // Can spawn multiple fixed encounters. - foreach (var other in others) - yield return new PaldeaFixedSymbolPoint(other, scenePoint.Position); - } - else - { - 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; - } - case "trinity_ObjectTemplate": - { - var ssObj = FlatBufferConverter.DeserializeFrom(sobj.Data); - if (ssObj.Type != "trinity_ScenePoint") - continue; - var subScenePoint = FlatBufferConverter.DeserializeFrom(ssObj.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(TrinityPropertySheet 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.Item3 is not { } sv) - throw new ArgumentException("Could not get PropertySheet Table Key"); - - return sv.Value; - } -}