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
This commit is contained in:
Kurt 2025-06-23 00:26:06 -05:00
parent 31b45ee883
commit a3f7daa178
21 changed files with 1123 additions and 1045 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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<string, (string Name, int Index)> 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];
}

View File

@ -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>(T collider, LocationPointDetail point) where T : IContainsV3f
=> collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ);
public static bool IsContainedBy<T>(T collider, PackedVec3f point) where T : IContainsV3f
=> collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ);
public static ReadOnlySpan<PaldeaFieldIndex> 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<string, (string Name, int Index)> placeNameMap,
IReadOnlyDictionary<string, AreaInfo> 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<string, (string Name, int Index)> placeNameMap,
IReadOnlyDictionary<string, AreaInfo> 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<string, (string Name, int Index)> GetInternalStringLookup(ReadOnlySpan<string> text, AHTB ahtb)
{
var result = new Dictionary<string, (string Name, int Index)>();
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;
}
}

View File

@ -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,
};
}

View File

@ -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<string, (string Name, int Index)> 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);
}
}
}
}
/// <summary>
/// Allow encounters into and out of this location
/// </summary>
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<PaldeaEncounter> 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<PaldeaEncounter> 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<string> specNamesInternal, string heading, IEnumerable<PaldeaEncounter> 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<LocationPointDetail> 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<LocationPointDetail> points)
{
tw.WriteLine("===");
tw.WriteLine(heading);
tw.WriteLine("===");
foreach (var e in points)
tw.WriteLine($" - {e.GetString()}");
tw.WriteLine();
}
#endregion
}

View File

@ -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<byte[]> 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<FixedSymbolTableArray>(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<AppearTuple>();
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<AppearTuple>(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<AppearTuple> { 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<AppearTuple> 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<AppearTuple> 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<byte[]> exist, FixedSymbolTable entry, IReadOnlyList<int> 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<byte> 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);
}
}

View File

@ -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<EventBattlePokemonArray>(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<string>();
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})");
}
}
}
}

View File

@ -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<string> specNamesInternal)
public string GetEncountString(ReadOnlySpan<string> specNamesInternal)
{
var species = specNamesInternal[SpeciesConverterSV.GetInternal9(Species)];
return GetString(species);

View File

@ -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<PaldeaFieldIndex> AllMaps =>
[
PaldeaFieldIndex.Paldea,
PaldeaFieldIndex.Kitakami,
PaldeaFieldIndex.Terarium,
];
public void DumpTo(string path, IReadOnlyList<string> specNamesInternal, IReadOnlyList<string> moveNames,
Dictionary<string, (string Name, int Index)> 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<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_100000.bin"));
var alEncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_atlantis.bin"));
var su1EncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su1.bin"));
var su2EncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su2.bin"));
//var lcEncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_lc.bin"));
var pokeDataMain = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin"));
var pokeDataSu1 = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su1/pokedata_su1_array.bin"));
var pokeDataSu2 = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su2/pokedata_su2_array.bin"));
//var pokeDataLc = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(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<byte[]> serialized = [];
var fsymData = FlatBufferConverter.DeserializeFrom<FixedSymbolTableArray>(rom.GetPackedFile("world/data/field/fixed_symbol/fixed_symbol_table/fixed_symbol_table_array.bin"));
var eventBattle = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(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<AppearTuple>();
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<AppearTuple>(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<AppearTuple> { 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<string>();
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<string> specNamesInternal, IReadOnlyList<string> moveNames, Dictionary<string, (string Name, int Index)> placeNameMap,
StreamWriter gw, string tableKey, int i, FixedSymbolTable entry, List<AppearTuple> 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<AppearTuple> 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<string, (string Name, int Index)> placeNameMap,
IReadOnlyDictionary<string, AreaInfo> 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<string, (string Name, int Index)> placeNameMap,
IReadOnlyDictionary<string, AreaInfo> 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);
}
/// <summary>
/// Allow encounters into and out of this location
/// </summary>
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<byte[]> exist, FixedSymbolTable entry, IReadOnlyList<int> 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<byte> 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<PaldeaEncounter> 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<string> specNamesInternal,
IReadOnlyDictionary<string, (string Name, int Index)> placeNameMap,
string areaName, AreaInfo areaInfo, List<PaldeaEncounter> 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<string> specNamesInternal,
IReadOnlyDictionary<string, (string Name, int Index)> 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<string, (string Name, int Index)> 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<LocationPointDetail> 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<LocationPointDetail> 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<string> specNamesInternal, string heading, IEnumerable<PaldeaEncounter> encounts)
{
tw.WriteLine("===");
tw.WriteLine(heading);
tw.WriteLine("===");
foreach (var e in encounts)
tw.WriteLine($" - {e.GetEncountString(specNamesInternal)}");
tw.WriteLine();
}
public static Dictionary<string, (string Name, int Index)> GetPlaceNameMap(string[] text, AHTB ahtb)
{
var result = new Dictionary<string, (string Name, int Index)>();
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,
};
}

View File

@ -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
{

View File

@ -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)
{

View File

@ -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<PaldeaFixedSymbolPoint>[] PointsScarlet;
public readonly List<PaldeaFixedSymbolPoint>[] PointsViolet;
public readonly Dictionary<string, string[]> MultiSpawner;
private static T Get<T>(Memory<byte> data) where T : class, IFlatBufferSerializable<T>
=> FlatBufferConverter.DeserializeFrom<T>(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<GemSymbolLotteryTableArray>(raw).Table;
MultiSpawner = gemLottery.ToDictionary(z => z.LotteryKey, GetTableGetKeys);
PointsScarlet = GetSymbols(rom, 0);
PointsViolet = GetSymbols(rom, 1);
}
private List<PaldeaFixedSymbolPoint>[] GetSymbols(IFileInternal rom, int game) =>
[
[..GetSymbols(rom, game, "world")],
[..GetSymbols(rom, game, "su1_world")],
[..GetSymbols(rom, game, "su2_world")],
];
private IEnumerable<PaldeaFixedSymbolPoint> 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<TrinitySceneObjectTemplate>(data);
return GetObjectTemplateSymbolPoints(template);
}
private static string[] GetTableGetKeys(GemSymbolLotteryTable entry)
{
var list = new List<string>();
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<PaldeaFixedSymbolPoint> GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplate template)
{
foreach (var obj in template.Objects)
{
switch (obj.Type)
{
case "trinity_ObjectTemplate":
{
var sObj = Get<TrinitySceneObjectTemplateData>(obj.Data);
if (sObj.Type != "trinity_ScenePoint")
break;
var scenePoint = Get<TrinityScenePoint>(sObj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
case "trinity_ScenePoint":
{
var scenePoint = Get<TrinityScenePoint>(obj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
}
}
}
private IEnumerable<PaldeaFixedSymbolPoint> GetScenePointSymbolPoints(TrinityScenePoint scenePoint, IList<TrinitySceneObjectTemplateEntry> subObjects)
{
// Handle SubObjects
foreach (var obj in subObjects)
{
switch (obj.Type)
{
case "trinity_PropertySheet":
{
var propSheet = Get<TrinityPropertySheet>(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<TrinityScenePoint>(obj.Data);
foreach (var f in GetScenePointSymbolPoints(subScenePoint, obj.SubObjects))
yield return f;
break;
}
case "trinity_ObjectTemplate":
{
var ssObj = Get<TrinitySceneObjectTemplateData>(obj.Data);
if (ssObj.Type != "trinity_ScenePoint")
break;
var subScenePoint = Get<TrinityScenePoint>(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;
}
}

View File

@ -1,6 +1,4 @@
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
namespace pkNX.Structures.FlatBuffers;
public class PaldeaFixedSymbolPoint(string key, PackedVec3f pos)
{

View File

@ -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<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_100000.bin"));
var alEncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_atlantis.bin"));
var su1EncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su1.bin"));
var su2EncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(rom.GetPackedFile("world/data/encount/point_data/point_data/encount_data_su2.bin"));
//var lcEncPoints = FlatBufferConverter.DeserializeFrom<PointDataArray>(ROM.GetPackedFile("world/data/encount/point_data/point_data/encount_data_lc.bin"));
var pokeDataMain = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin"));
var pokeDataSu1 = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su1/pokedata_su1_array.bin"));
var pokeDataSu2 = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(rom.GetPackedFile("world/data/encount/pokedata/pokedata_su2/pokedata_su2_array.bin"));
//var pokeDataLc = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(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;
}
}
}

View File

@ -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<PaldeaFixedSymbolPoint>[] scarletPoints = [[], [], []];
public readonly List<PaldeaFixedSymbolPoint>[] violetPoints = [[], [], []];
public readonly Dictionary<string, string[]> MultiSpawner;
public PaldeaFixedSymbolModel(IFileInternal ROM)
{
var gemLottery = FlatBufferConverter.DeserializeFrom<GemSymbolLotteryTableArray>(
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<TrinitySceneObjectTemplate>(p0Data);
var p1 = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(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<TrinitySceneObjectTemplate>(k0Data);
var k1 = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(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<TrinitySceneObjectTemplate>(b0Data);
var b1 = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(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<TrinitySceneObjectTemplate>(u0Data);
//var u1 = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(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<string>();
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<PaldeaFixedSymbolPoint> GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplate template)
{
foreach (var obj in template.Objects)
{
switch (obj.Type)
{
case "trinity_ObjectTemplate":
{
var sObj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateData>(obj.Data);
if (sObj.Type != "trinity_ScenePoint")
continue;
var scenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePoint>(sObj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
case "trinity_ScenePoint":
{
var scenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePoint>(obj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
}
}
}
private IEnumerable<PaldeaFixedSymbolPoint> GetScenePointSymbolPoints(TrinityScenePoint scenePoint, IList<TrinitySceneObjectTemplateEntry> 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<TrinityPropertySheet>(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<TrinityScenePoint>(sobj.Data);
foreach (var f in GetScenePointSymbolPoints(subScenePoint, sobj.SubObjects))
yield return f;
break;
}
case "trinity_ObjectTemplate":
{
var ssObj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateData>(sobj.Data);
if (ssObj.Type != "trinity_ScenePoint")
continue;
var subScenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePoint>(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;
}
}