Add wander tolerance to encounter slot pkl creation (#301)

* Rough draft: 50f

* Don't skip spawn-less locations, cross pollinate

* Dump points and biomes

* Lower wander tolerance to 30f, reorder time flags

* Reorder crabrawler evo order
This commit is contained in:
Kurt
2022-12-10 19:25:19 -08:00
committed by GitHub
parent bc431a3d80
commit ff271ca171
17 changed files with 1371 additions and 1027 deletions

View File

@@ -35,4 +35,7 @@ public class AreaInfo
[FlatBufferItem(22)] public EncBiome BaseBiome { get; set; }
[FlatBufferItem(23)] public AreaTag Tag { get; set; }
[FlatBufferItem(24)] public OverrideBiome OverrideBiome { get; set; }
public int ActualMinLevel => MinEncLv != 0 ? MinEncLv : 1;
public int ActualMaxLevel => MaxEncLv != 0 ? MaxEncLv : 100;
}

View File

@@ -425,7 +425,7 @@ private static (int, uint) ReadVar32(ReadOnlySpan<byte> buffer)
};
}
public class AABBTree
public class AABBTree : IContainsV3f
{
private readonly List<hkcdSimdTreeNode> _nodes;
private int _numLeafRects;
@@ -471,8 +471,10 @@ public AABBTree(List<hkcdSimdTreeNode> nodes)
// Official logic for area-containment checks for y intersection between y+1 and y-10000.0
public bool ContainsPoint(float x, float y, float z) => ContainsPointInNode(1, x, y - 10000, y + 1, z);
public bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ)
=> ContainsPointInNode(1, x, y - 10000, y + 1, z, toleranceX, toleranceY, toleranceZ);
private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, float z)
private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, float z, float tx = 0f, float ty = 0f, float tz = 0f)
{
if (nodeIndex == 0)
return false;
@@ -480,16 +482,17 @@ private bool ContainsPointInNode(int nodeIndex, float x, float ly, float hy, flo
var node = _nodes[nodeIndex];
for (var i = 0; i < 4; i++)
{
if (node.lx[i] > node.hx[i] || !(node.lx[i] <= x && x <= node.hx[i]))
if (node.lx[i] > node.hx[i] || !(node.lx[i] - tx <= x) || !(node.hx[i] + tx >= x))
continue;
if (node.lz[i] > node.hz[i] || !(node.lz[i] <= z && z <= node.hz[i]))
if (node.lz[i] > node.hz[i] || !(node.lz[i] - tz <= z) || !(node.hz[i] + tz >= z))
continue;
if (node.ly[i] > node.hy[i] || !(node.ly[i] <= hy && ly <= node.hy[i]))
if (node.ly[i] > node.hy[i] || !(node.ly[i] - ty <= hy) || !(node.hy[i] + ty >= ly))
continue;
if (node.IsLeaf || ContainsPointInNode((int)node.data[i], x, ly, hy, z))
if (node.IsLeaf)
return true;
if (ContainsPointInNode((int)node.data[i], x, ly, hy, z, tx, ty, tz))
return true;
}
return false;
}
}
@@ -585,3 +588,9 @@ private struct HavokItem
public uint Count;
}
}
public interface IContainsV3f
{
bool ContainsPoint(float x, float y, float z);
bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ);
}

File diff suppressed because it is too large Load Diff

View File

@@ -353,14 +353,19 @@ static byte[] GetPickle(PersonalInfo9SV e)
{
if (!e.IsPresentInGame)
return Array.Empty<byte>();
return Write(e.FB.Evolutions);
return Write(e.FB.Info.SpeciesCopy, e.FB.Evolutions);
}
static byte[] Write(PersonalInfo9SVEvolutions[] evos)
static byte[] Write(int species, PersonalInfo9SVEvolutions[] evos)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
foreach (var m in evos) // just in case
var list = evos.ToArray();
if (species == (int)Species.Crabrawler)
Array.Reverse(list); // put the levelup evo last.
foreach (var m in list) // just in case
{
var method = ((EvolutionType)m.Method);
if (method == EvolutionType.Hisui)
@@ -533,6 +538,7 @@ private void DumpAjito()
private void DumpEncount()
{
Dump<TreeShakePokemonArray, TreeShakePokemon>("world/data/event/treeshake/treeshake_pokemon/treeshake_pokemon_array.bfbs");
Dump<PointDataArray, PointData>("world/data/encount/point_data/point_data/encount_data_atlantis.bfbs");
Dump<PointDataArray, PointData>("world/data/encount/point_data/point_data/encount_data_100000.bfbs");
Dump<EncountPokeDataArray, EncountPokeData>("world/data/encount/pokedata/pokedata/pokedata_array.bfbs");
@@ -907,6 +913,7 @@ public void DumpSpecific()
"world/data/encount/setting/setting/data.bfbs",
"world/data/encount/setting/raid_difficulty_lottery/raid_difficulty_lottery_array.bfbs",
"world/data/encount/setting/raid_gem_setting/raid_gem_setting.bfbs",
"world/data/event/treeshake/treeshake_pokemon/treeshake_pokemon_array.bfbs",
// Field
"world/data/field/area/field_dungeon_area/field_dungeon_area_array.bfbs",
"world/data/field/area/field_inside_area/field_inside_area_array.bfbs",

View File

@@ -0,0 +1,31 @@
namespace pkNX.Structures.FlatBuffers;
public class BoxCollision9 : IContainsV3f
{
public PackedVec3f Position { get; init; }
public PackedVec3f Size { get; init; }
public bool ContainsPoint(float x, float y, float z) => ContainsPoint(x, y, z, 0, 0, 0);
public bool ContainsPoint(float x, float y, float z, float toleranceX, float toleranceY, float toleranceZ)
{
var box_lx = Position.X - (Size.X / 2.0f) - toleranceX;
var box_hx = Position.X + (Size.X / 2.0f) + toleranceX;
if (box_lx > box_hx || !(box_lx <= x && x <= box_hx))
return false;
var box_lz = Position.Z - (Size.Z / 2.0f) - toleranceZ;
var box_hz = Position.Z + (Size.Z / 2.0f) + toleranceZ;
if (box_lz > box_hz || !(box_lz <= z && z <= box_hz))
return false;
var ly = y - 10000;
var hy = y + 1;
var box_ly = Position.Y - (Size.Y / 2.0f) - toleranceY;
var box_hy = Position.Y + (Size.Y / 2.0f) + toleranceY;
if (box_ly > box_hy || !(box_ly <= hy && ly <= box_hy))
return false;
return true;
}
}

View File

@@ -0,0 +1,520 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using pkNX.Containers;
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
public class EncounterDumperSV
{
private readonly IFileInternal ROM;
private const float tolX = 30f;
private const float tolY = 30f;
private const float tolZ = 30f;
public EncounterDumperSV(IFileInternal rom) => ROM = rom;
public void DumpTo(string path, IReadOnlyList<string> specNames, 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 pokeData = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(ROM.GetPackedFile("world/data/encount/pokedata/pokedata/pokedata_array.bin"));
var db = new LocationDatabase();
// Overall Logic Flow:
// 1 - At every point, spawn everything that can exist at that point.
// 2 - At each area, absorb native points into local points.
// 3 - At each area, absorb crossover points into crossover points.
// 4 - Consolidate the encounters from both point lists.
// Just compute everything (big memory!) then crunch it all down.
// Points can be used by multiple areas as crossover sources. Need to be able to "belong" to multiple areas, and indicate their parent area.
var pointMain = ReformatPoints(mlEncPoints);
var pointAtlantis = ReformatPoints(alEncPoints);
// Fill the point lists for each area, then spawn everything into those points.
foreach (var areaName in scene.areaNames)
{
var areaInfo = scene.AreaInfos[areaName];
// Determine potential spawners
if (!scene.TryGetContainsCheck(areaName, out var collider))
{
Console.WriteLine($"No collider for {areaName}");
continue;
}
// Locations that do not spawn encounters can still have crossovers bleed into them.
// We'll have empty local encounter lists for them.
var name = areaInfo.LocationNameMain;
if (string.IsNullOrEmpty(name))
continue;
var storage = db.Get(placeNameMap[name].Index, areaName, areaInfo);
if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All)
continue;
var points = scene.isAtlantis[areaName] ? pointAtlantis : pointMain;
storage.LoadPoints(points, collider, areaInfo.ActualMinLevel, areaInfo.ActualMaxLevel);
storage.GetEncounters(pokeData, scene);
}
// For each area, we need to peek at the other areas to see if they have any crossover points.
// For each of those crossover points, we need to see if they are in the current area's collider.
// If they are, we need to add them to the current area's list of crossover points.
foreach (var areaName in scene.areaNames)
{
// Same sanity checking as above iteration.
var areaInfo = scene.AreaInfos[areaName];
// Determine potential spawners
if (!scene.TryGetContainsCheck(areaName, out var collider))
{
Console.WriteLine($"No collider for {areaName}");
continue;
}
var name = areaInfo.LocationNameMain;
if (string.IsNullOrEmpty(name))
continue;
//if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All)
// continue;
var storage = db.Get(placeNameMap[name].Index, areaName, areaInfo);
// Here's where the fun begins. Iterate over areas inside this loop so we can look for all possible adjacent areas.
foreach (var otherName in scene.areaNames)
{
// Skip self
if (otherName == areaName)
continue;
// Skip areas that don't have a location name
var otherAreaInfo = scene.AreaInfos[otherName];
var otherNameMain = otherAreaInfo.LocationNameMain;
if (string.IsNullOrEmpty(otherNameMain))
continue;
// Skip areas that don't have a collider
if (!scene.TryGetContainsCheck(otherName, out _))
continue;
// Iterate over all crossover points in the other area.
var cross = db.Get(placeNameMap[otherNameMain].Index, otherName, otherAreaInfo);
foreach (var point in cross.Local)
{
// If the crossover point is close enough to the current area's collider, add it to the current area's list of crossover points.
if (collider.ContainsPoint(point.X, point.Y, point.Z, tolX, tolY, tolZ))
storage.Nearby.Add(point);
}
}
}
// Each area and their local / crossover points have been aggregated.
// Integrate the points' slots into a single list, and consolidate entries with same level ranges to as few objects as possible.
foreach (var storage in db.Locations.Values)
{
// Consolidate encounters
storage.Integrate();
storage.Consolidate();
}
// Output to stream if available.
if (writeText)
{
using var sw = File.CreateText(Path.Combine(path, "titan_enc.txt"));
foreach (var s in db.Locations.Values)
WriteLocation(sw, specNames, placeNameMap, s.AreaName, s.AreaInfo, s.Slots);
using var tw = File.CreateText(Path.Combine(path, "titan_loc_enc.txt"));
WriteLocEncList(tw, specNames, placeNameMap, db);
using var pw = File.CreateText(Path.Combine(path, "titan_loc_point.txt"));
WriteLocPointList(pw, placeNameMap, db);
}
if (writePickle)
{
string binPath = Path.Combine(path, "encounter_wild_paldea.pkl");
SerializeEncounters(binPath, db);
}
// Fixed symbols
List<byte[]> serialized = new();
int[] bannedIndexes =
{
31, // Lighthouse Wingull
};
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"));
for (var i = 0; i < fsymData.Table.Length; i++)
{
FixedSymbolTable? entry = fsymData.Table[i];
var tableKey = entry.TableKey;
var points = gamePoints.Where(p => p.TableKey == tableKey).ToList();
if (points.Count == 0)
continue;
var areas = new List<string>();
foreach (var areaName in scene.areaNames)
{
if (scene.isAtlantis[areaName])
continue;
var areaInfo = scene.AreaInfos[areaName];
var name = areaInfo.LocationNameMain;
if (string.IsNullOrEmpty(name))
continue;
if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All)
continue;
if (points.Any(p => scene.IsPointContained(areaName, p.Position.X, p.Position.Y, p.Position.Z)))
areas.Add(areaName);
}
var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList();
gw.WriteLine("===");
gw.WriteLine(entry.TableKey);
gw.WriteLine("===");
gw.WriteLine(" PokeData:");
var pd = entry.PokeDataSymbol;
gw.WriteLine($" Species: {specNames[(int)pd.DevId]}");
gw.WriteLine($" Form: {pd.FormId}");
gw.WriteLine($" Level: {pd.Level}");
gw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}");
gw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}");
var talentStr = pd.TalentType switch
{
TalentType.RANDOM => "Random",
TalentType.V_NUM => $"{pd.TalentVNum} Perfect",
TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}",
_ => "Invalid",
};
gw.WriteLine($" IVs: {talentStr}");
gw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.TokuseiIndex]}");
switch (pd.WazaType)
{
case WazaType.DEFAULT:
gw.WriteLine(" Moves: Random");
break;
case WazaType.MANUAL:
gw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}");
break;
}
gw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}");
gw.WriteLine($" GemType: {(int)pd.GemType}");
gw.WriteLine(" Points:");
foreach (var point in points)
{
gw.WriteLine($" - ({point.Position.X}, {point.Position.Y}, {point.Position.Z})");
}
gw.WriteLine(" Areas:");
foreach (var areaName in areas)
{
var areaInfo = scene.AreaInfos[areaName];
var loc = areaInfo.LocationNameMain;
(string name, int index) = placeNameMap[loc];
gw.WriteLine($" - {areaName} - {loc} - {name} ({index})");
}
// Serialize
if (locs.Count == 0)
continue;
if (bannedIndexes.Contains(i))
continue;
areas.Clear();
foreach (var areaName in scene.areaNames)
{
if (scene.isAtlantis[areaName])
continue;
var areaInfo = scene.AreaInfos[areaName];
var name = areaInfo.LocationNameMain;
if (string.IsNullOrEmpty(name))
continue;
if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All)
continue;
if (!scene.TryGetContainsCheck(areaName, out var collider))
continue;
if (points.Any(p => collider.ContainsPoint(p.Position.X, p.Position.Y, p.Position.Z, tolX, tolY, tolZ)))
areas.Add(areaName);
}
locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList();
WriteFixedSymbol(serialized, entry, locs);
}
}
using var cw = File.CreateText(Path.Combine(path, "titan_coin_symbol.txt"));
foreach (var entry in csym.Points)
{
var areas = new List<string>();
foreach (var areaName in scene.areaNames)
{
if (scene.isAtlantis[areaName])
continue;
var areaInfo = scene.AreaInfos[areaName];
var name = areaInfo.LocationNameMain;
if (string.IsNullOrEmpty(name))
continue;
if (areaInfo.Tag is AreaTag.NG_Encount or AreaTag.NG_All)
continue;
if (scene.IsPointContained(areaName, entry.Position.X, entry.Position.Y, entry.Position.Z))
areas.Add(areaName);
}
var locs = areas.Select(a => placeNameMap[scene.AreaInfos[a].LocationNameMain].Index).Distinct().ToList();
cw.WriteLine("===");
cw.WriteLine(entry.Name);
cw.WriteLine("===");
cw.WriteLine($" First Num: {entry.FirstNum}");
cw.WriteLine($" Coordinates: ({entry.Position.X}, {entry.Position.Y}, {entry.Position.Z})");
if (entry.IsBox)
{
cw.WriteLine($" Box Label: {entry.BoxLabel}");
cw.WriteLine(" PokeData:");
var pd = Array.Find(eventBattle.Table, e => e.Label == entry.BoxLabel)!.PokeData;
cw.WriteLine($" Species: {specNames[(int)pd.DevId]}");
cw.WriteLine($" Form: {pd.FormId}");
cw.WriteLine($" Level: {pd.Level}");
cw.WriteLine($" Sex: {new[] { "Random", "Male", "Female" }[(int)pd.Sex]}");
cw.WriteLine($" Shiny: {new[] { "Random", "Never", "Always" }[(int)pd.RareType]}");
var talentStr = pd.TalentType switch
{
TalentType.RANDOM => "Random",
TalentType.V_NUM => $"{pd.TalentVnum} Perfect",
TalentType.VALUE => $"{pd.TalentValue.HP}/{pd.TalentValue.ATK}/{pd.TalentValue.DEF}/{pd.TalentValue.SPA}/{pd.TalentValue.SPD}/{pd.TalentValue.SPE}",
_ => "Invalid",
};
cw.WriteLine($" IVs: {talentStr}");
cw.WriteLine($" Ability: {new[] { "1/2", "1/2/3", "1", "2", "3" }[(int)pd.Tokusei]}");
switch (pd.WazaType)
{
case WazaType.DEFAULT:
cw.WriteLine($" Moves: Random");
break;
case WazaType.MANUAL:
cw.WriteLine($" Moves: {moveNames[(int)pd.Waza1.WazaId]}/{moveNames[(int)pd.Waza2.WazaId]}/{moveNames[(int)pd.Waza3.WazaId]}/{moveNames[(int)pd.Waza4.WazaId]}");
break;
}
cw.WriteLine($" Scale: {new[] { "Random", "XS", "S", "M", "L", "XL", $"{pd.ScaleValue}" }[(int)pd.ScaleType]}");
cw.WriteLine($" GemType: {(int)pd.GemType}");
}
cw.WriteLine(" Areas:");
foreach (var areaName in areas)
{
var areaInfo = scene.AreaInfos[areaName];
var loc = areaInfo.LocationNameMain;
(string name, int index) = placeNameMap[loc];
cw.WriteLine($" - {areaName} - {loc} - {name} ({index})");
}
}
var pathPickle = Path.Combine(path, "encounter_fixed_paldea.pkl");
var ordered = serialized
.OrderBy(z => BinaryPrimitives.ReadUInt16LittleEndian(z)) // Species
.ThenBy(z => z[2]) // Form
.ThenBy(z => z[3]) // Level
;
File.WriteAllBytes(pathPickle, ordered.SelectMany(z => z).ToArray());
}
private static LocationPointDetail[] ReformatPoints(PointDataArray all)
{
var arr = all.Table;
var result = new LocationPointDetail[arr.Length];
for (int i = 0; i < arr.Length; i++)
result[i] = new LocationPointDetail(arr[i]);
return result;
}
private static void WriteFixedSymbol(ICollection<byte[]> exist, FixedSymbolTable entry, IReadOnlyList<int> locs)
{
var enc = entry.PokeDataSymbol;
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
bw.Write((ushort)enc.DevId);
bw.Write((byte)enc.FormId);
bw.Write((byte)enc.Level);
bw.Write((byte)enc.TalentVNum);
bw.Write((byte)enc.GemType);
bw.Write((byte)(enc.Sex - 1));
bw.Write((byte)0); // reserved
bw.Write((ushort)enc.Waza1.WazaId);
bw.Write((ushort)enc.Waza2.WazaId);
bw.Write((ushort)enc.Waza3.WazaId);
bw.Write((ushort)enc.Waza4.WazaId);
// At most 3 locations, but just use 4 bytes.
Span<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)
{
bw.Write((ushort)slot.Species);
bw.Write((byte)slot.Form);
bw.Write((byte)slot.Gender);
bw.Write((byte)slot.MinLevel);
bw.Write((byte)slot.MaxLevel);
bw.Write((byte)slot.Time);
bw.Write((byte)0);
}
return ms.ToArray();
}
private static PokeDataSymbol GetPokeDataSymbol(FixedSymbolTableArray fsymTable, string tableKey)
{
foreach (var entry in fsymTable.Table)
{
if (entry.TableKey == tableKey)
return entry.PokeDataSymbol;
}
throw new ArgumentException($"TableKey not found ({tableKey})");
}
private static void WriteLocation(TextWriter tw, IReadOnlyList<string> specNames,
IReadOnlyDictionary<string, (string Name, int Index)> placeNameMap,
string areaName, AreaInfo areaInfo, List<PaldeaEncounter> encounts)
{
var loc = areaInfo.LocationNameMain;
(string name, int index) = placeNameMap[loc];
var heading = $"{areaName} - {loc} - {name} ({index})";
WriteEncounts(tw, specNames, heading, encounts);
}
private static void WriteLocEncList(TextWriter tw, IReadOnlyList<string> specNames,
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, specNames, 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()).OrderBy(z => z);
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> specNames, string heading, IEnumerable<PaldeaEncounter> encounts)
{
tw.WriteLine("===");
tw.WriteLine(heading);
tw.WriteLine("===");
foreach (var e in encounts)
tw.WriteLine($" - {e.GetEncountString(specNames)}");
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;
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace pkNX.Structures.FlatBuffers;
public class LocationDatabase
{
public readonly Dictionary<int, LocationStorage> Locations = new();
public LocationStorage Get(int location, string areaName, AreaInfo areaInfo)
{
if (!Locations.TryGetValue(location, out var loc))
Locations[location] = loc = new LocationStorage(location, areaName, areaInfo);
return loc;
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace pkNX.Structures.FlatBuffers;
public class LocationPointDetail
{
public readonly PointData Point;
public LocationPointDetail(PointData point) => Point = point;
public float X => Point.Position.X;
public float Y => Point.Position.Y;
public float Z => Point.Position.Z;
public int Location { get; init; }
public bool IsWithinLevelRange(int areaMin, int areaMax)
{
return Point.LevelRange.X <= areaMax && areaMin <= Point.LevelRange.Y;
}
public readonly List<PaldeaEncounter> Slots = new();
public void Add(PaldeaEncounter slot) => Slots.Add(slot);
public string GetString()
{
return $"{Location:0000} ({X:0.0000},{Y:0.0000},{Z:0.0000}) {Point.Biome} ({Point.LevelRange.X}-{Point.LevelRange.Y}) {Slots.Count:00}";
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
namespace pkNX.Structures.FlatBuffers;
public class LocationStorage
{
public readonly int Location;
public readonly AreaInfo AreaInfo;
public readonly string AreaName;
public readonly HashSet<ulong> Added = new();
public readonly List<PaldeaEncounter> Slots = new();
public readonly Dictionary<int, LocationStorage> SlotsCrossover = new();
public readonly List<LocationPointDetail> Local = new();
public readonly List<LocationPointDetail> Nearby = new();
public LocationStorage(int loc, string areaName, AreaInfo info)
{
Location = loc;
AreaName = areaName;
AreaInfo = info;
}
private void Add(PaldeaEncounter slot)
{
var key = slot.GetHash();
if (Added.Add(key))
Slots.Add(slot);
}
private void AddCrossover(PaldeaEncounter slot, int loc)
{
if (!SlotsCrossover.TryGetValue(loc, out var s))
SlotsCrossover[loc] = s = new LocationStorage(loc, AreaName, AreaInfo);
s.Add(slot with { CrossFromLocation = (ushort)Location });
}
public void Integrate()
{
foreach (var point in Local)
{
foreach (var slot in point.Slots)
Add(slot);
}
foreach (var point in Nearby)
{
foreach (var slot in point.Slots)
AddCrossover(slot, point.Location);
}
}
public void Consolidate()
{
ConsolidateEncounters(Slots);
foreach (var loc in SlotsCrossover.Values)
loc.Consolidate();
}
private static void ConsolidateEncounters(List<PaldeaEncounter> encounters)
{
// Merge and remove future indexes if they can be combined with current
// Pre-sort so we can just iterate.
encounters.Sort();
for (int i = 0; i < encounters.Count;)
{
// For i to i+count, merge within
var enc = encounters[i];
int count = GetPotentialConsolidationCount(encounters, i, enc);
// Remove all indexes that have Absorb return true with a later element
while (TryAbsorbFromRange(encounters, i, count))
count--;
i += count;
}
}
private static int GetPotentialConsolidationCount(List<PaldeaEncounter> encounters, int start, PaldeaEncounter enc)
{
int count = 1;
for (int end = start + 1; end < encounters.Count; end++)
{
if (!encounters[end].IsSameSpecFormGender(enc))
break;
count++;
}
return count;
}
private static bool TryAbsorbFromRange(List<PaldeaEncounter> encounters, int start, int count)
{
// Need to compare all elements with each-other; return true if any absorb.
var end = start + count - 1;
for (int i = start; i < end; i++)
{
for (int j = i + 1; j <= end; j++)
{
if (!encounters[i].Absorb(encounters[j]))
continue;
encounters.RemoveAt(j);
return true;
}
}
return false;
}
public void LoadPoints(IEnumerable<LocationPointDetail> points, IContainsV3f collider, int areaMin, int areaMax)
{
foreach (var w in points)
{
if (!w.IsWithinLevelRange(areaMin, areaMax))
continue;
if (collider.ContainsPoint(w.X, w.Y, w.Z))
{
var composite = GetCompositePoint(w.Point, areaMin, areaMax, Location);
Local.Add(composite); // native
}
// Don't check here for cross-area, as our areaMin/areaMax is not valid for that crossover case.
}
}
private static LocationPointDetail GetCompositePoint(PointData ep, int areaMin, int areaMax, int location)
{
var newX = Math.Max(ep.LevelRange.X, areaMin);
var newY = Math.Min(ep.LevelRange.Y, areaMax);
var newPoint = new PointData
{
Position = ep.Position,
LevelRange = new PackedVec2f { X = newX, Y = newY },
Biome = ep.Biome,
Substance = ep.Substance,
AreaNo = ep.AreaNo,
};
return new LocationPointDetail(newPoint) { Location = location };
}
public void GetEncounters(EncountPokeDataArray pokeData, PaldeaSceneModel scene)
{
foreach (var spawner in Local)
{
foreach (var pd in pokeData.Table)
{
if (!IsAbleToSpawnAt(pd, spawner, AreaName, scene))
continue;
// Add encount
spawner.Add(PaldeaEncounter.GetNew(pd, spawner.Point));
if (pd.BandPoke != 0) // Add band encount
spawner.Add(PaldeaEncounter.GetBand(pd, spawner.Point));
}
}
}
private static bool IsAbleToSpawnAt(EncountPokeData pd, LocationPointDetail ep, string areaName, PaldeaSceneModel scene)
{
// Check area
if (!string.IsNullOrEmpty(pd.Area) && !IsInArea(pd.Area, ep.Point.AreaNo))
return false;
// Check loc
if (!string.IsNullOrEmpty(pd.LocationName) && !IsInArea(pd.LocationName, areaName, scene, ep.Point))
return false;
// Check biome
if (!HasBiome(pd, (Biome)(int)ep.Point.Biome))
return false;
// check level range overlap
// check area level range overlap -- already done via point
if (!LevelWithinRange(pd, ep.Point))
return false;
// Assume flag, enable table, timetable are fine
// Assume version is fine -- union wireless sessions can share encounters.
return true;
}
private static bool LevelWithinRange(EncountPokeData pd, PointData clamp)
{
return clamp.LevelRange.X <= pd.MaxLevel && pd.MinLevel <= clamp.LevelRange.Y;
}
private static bool HasBiome(EncountPokeData pd, Biome biome)
{
if (biome == pd.Biome1)
return true;
if (biome == pd.Biome2)
return true;
if (biome == pd.Biome3)
return true;
if (biome == pd.Biome4)
return true;
return false;
}
private static bool IsInArea(ReadOnlySpan<char> areaName, int areaNo)
{
int start = 0;
for (int i = 0; i < areaName.Length; i++)
{
if (areaName[i] != ',')
continue;
var name = areaName[start..i];
if (int.TryParse(name, out var tmp) && areaNo == tmp)
return true;
start = i + 1;
}
return int.TryParse(areaName[start..], out var x) && areaNo == x;
}
private static bool IsInArea(string locName, string areaName, PaldeaSceneModel scene, PointData ep)
{
var split = locName.Split(",");
foreach (string a in split)
{
if (a == areaName)
return true;
if (!scene.IsPointContained(a, ep.Position.X, ep.Position.Y, ep.Position.Z))
continue;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using pkNX.Containers;
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
public class PaldeaCoinSymbolModel
{
public readonly List<PaldeaCoinSymbolPoint> Points;
public PaldeaCoinSymbolModel(IFileInternal ROM)
{
Points = new List<PaldeaCoinSymbolPoint>();
var cData = ROM.GetPackedFile("world/scene/parts/field/streaming_event/world_coin_placement_symbol_/world_coin_placement_symbol_0.trscn");
// NOTE: Fine to only use Scarlet, Violet data is identical.
var c = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateSV>(cData);
Points.AddRange(GetObjectTemplateSymbolPoints(c));
}
private IEnumerable<PaldeaCoinSymbolPoint> GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template)
{
foreach (var obj in template.Objects)
{
switch (obj.Type)
{
case "trinity_ScenePoint":
{
var scenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePointSV>(obj.Data);
if (obj.SubObjects.Length != 1)
throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject Count {obj.SubObjects.Length}");
if (obj.SubObjects[0].Type != "trinity_PropertySheet")
throw new ArgumentException($"Unexpected CoinSymbolPoint SubObject ({obj.SubObjects[0].Type})");
var propSheet = FlatBufferConverter.DeserializeFrom<TrinityPropertySheetSV>(obj.SubObjects[0].Data);
yield return ParseCoinSymbolPoint(scenePoint, propSheet);
break;
}
default:
throw new ArgumentException($"Unsupported CoinPlacement Object Type {obj.Type}");
}
}
}
private PaldeaCoinSymbolPoint ParseCoinSymbolPoint(TrinityScenePointSV sp, TrinityPropertySheetSV ps)
{
switch (ps.Name)
{
case "coin_walk_symbol_point":
return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), string.Empty, sp.Position);
case "coin_box_symbol_point":
return new PaldeaCoinSymbolPoint(sp.Name, GetFirstNum(ps), GetBoxLabel(ps), sp.Position);
default:
throw new ArgumentException($"Unknown CoinSymbol PropertySheet {ps.Name}");
}
}
private ulong GetFirstNum(TrinityPropertySheetSV ps)
{
if (ps.Properties[0].Fields[0].Name != "firstNum")
throw new ArgumentException("Invalid PropertySheet field layout");
if (!ps.Properties[0].Fields[0].Data.TryGet(out TrinityPropertySheetField1SV? sv))
throw new ArgumentException("Could not get PropertySheet Table Key");
return sv.Value;
}
private string GetBoxLabel(TrinityPropertySheetSV ps)
{
if (ps.Properties[0].Fields[1].Name != "label")
throw new ArgumentException("Invalid PropertySheet field layout");
if (!ps.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv))
throw new ArgumentException("Could not get PropertySheet Table Key");
return sv.Value;
}
}

View File

@@ -0,0 +1,26 @@
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
public class PaldeaCoinSymbolPoint
{
public string Name;
public ulong FirstNum;
public string BoxLabel;
public PackedVec3f Position;
public PaldeaCoinSymbolPoint(string name, ulong num, string boxLabel, PackedVec3f pos)
{
Name = name;
FirstNum = num;
BoxLabel = boxLabel;
Position = new PackedVec3f
{
X = pos.X,
Y = pos.Y,
Z = pos.Z,
};
}
public bool IsBox => !string.IsNullOrEmpty(BoxLabel);
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
namespace pkNX.Structures.FlatBuffers;
public record PaldeaEncounter(ushort Species, byte Form, byte Sex, byte MinLevel, byte MaxLevel, byte Time, ushort CrossFromLocation = 0) : IComparable<PaldeaEncounter>
{
public byte MinLevel { get; private set; } = MinLevel;
public byte MaxLevel { get; private set; } = MaxLevel;
public int Gender => Sex switch
{
1 => 0,
2 => 1,
_ => -1,
};
// Match the Mark time of day order: Noon, Night, Evening, Morning
private static byte GetTimeBits(TimeTable t) => (byte)((t.Noon ? 0 : 1) | (t.Night ? 0 : 2) | (t.Evening ? 0 : 4) | (t.Morning ? 0 : 8));
public static PaldeaEncounter GetNew(EncountPokeData pd, PointData ep)
{
// Combine the 4 bools into a single byte
var time = GetTimeBits(pd.TimeTable);
var min = (byte)Math.Max(ep.LevelRange.X, pd.MinLevel);
var max = (byte)Math.Min(ep.LevelRange.Y, pd.MaxLevel);
return new((ushort)pd.DevId, (byte)pd.Form, (byte)pd.Sex, min, max, time);
}
public static PaldeaEncounter GetBand(EncountPokeData pd, PointData ep)
{
// Combine the 4 bools into a single byte
var time = GetTimeBits(pd.TimeTable);
var min = (byte)Math.Max(ep.LevelRange.X, pd.MinLevel);
var max = (byte)Math.Min(ep.LevelRange.Y, pd.MaxLevel);
return new((ushort)pd.BandPoke, (byte)pd.BandForm, (byte)pd.BandSex, min, max, time);
}
public string GetEncountString(IReadOnlyList<string> specNames)
{
var species = specNames[Species];
return GetString(species);
}
public override string ToString()
{
return GetString(((PKHeX.Core.Species)Species).ToString());
}
private string GetString(string species)
{
var form = Form == 0 ? "" : $"-{Form}";
var sex = Sex == 0 ? "" : $" (sex={Sex})";
return $"{species}{form}{sex} Lv. {MinLevel}-{MaxLevel}";
}
public bool Absorb(PaldeaEncounter other)
{
if (Time != other.Time)
return false;
if (CrossFromLocation != other.CrossFromLocation)
return false;
if (other.MinLevel == MinLevel && other.MaxLevel == MaxLevel)
return true;
if (!IsLevelRangeOverlap(other) && !other.IsLevelRangeOverlap(this))
return false;
MinLevel = Math.Min(MinLevel, other.MinLevel);
MaxLevel = Math.Max(MaxLevel, other.MaxLevel);
return true;
}
public bool IsSameSpecFormGender(PaldeaEncounter other)
{
if (Species != other.Species || Form != other.Form)
return false;
if (Sex != other.Sex)
return false;
return true;
}
private bool IsLevelRangeOverlap(PaldeaEncounter other)
{
// If our level range overlaps with the other (with +/- 1 tolerance), return true.
return MaxLevel + 1 >= other.MinLevel && MinLevel + 1 <= other.MaxLevel;
}
public int CompareTo(PaldeaEncounter? other)
{
if (ReferenceEquals(this, other)) return 0;
if (other is null) return 1;
int speciesComparison = Species.CompareTo(other.Species);
if (speciesComparison != 0) return speciesComparison;
int formComparison = Form.CompareTo(other.Form);
if (formComparison != 0) return formComparison;
int sexComparison = Sex.CompareTo(other.Sex);
if (sexComparison != 0) return sexComparison;
int minLevelComparison = MinLevel.CompareTo(other.MinLevel);
if (minLevelComparison != 0) return minLevelComparison;
int maxLevelComparison = MaxLevel.CompareTo(other.MaxLevel);
if (maxLevelComparison != 0) return maxLevelComparison;
int timeComparison = Time.CompareTo(other.Time);
if (timeComparison != 0) return timeComparison;
return CrossFromLocation.CompareTo(other.CrossFromLocation);
}
public ulong GetHash()
{
ulong result = Species;
result = (result << 16) | CrossFromLocation;
result = (result << 8) | Form;
result = (result << 8) | (byte)((byte)(Time << 4) | Sex);
result = (result << 8) | MinLevel;
result = (result << 8) | MaxLevel;
return result;
}
}

View File

@@ -0,0 +1,52 @@
using System;
using pkNX.Containers;
namespace pkNX.Structures.FlatBuffers;
public class PaldeaFieldModel
{
private readonly FieldMainArea[] mainAreas;
private readonly FieldSubArea[] subAreas;
private readonly FieldInsideArea[] insideAreas;
private readonly FieldDungeonArea[] dungeonAreas;
private readonly FieldLocation[] fieldLocations;
public PaldeaFieldModel(IFileInternal ROM)
{
mainAreas = FlatBufferConverter.DeserializeFrom<FieldMainAreaArray>(ROM.GetPackedFile("world/data/field/area/field_main_area/field_main_area_array.bin")).Table;
subAreas = FlatBufferConverter.DeserializeFrom<FieldSubAreaArray>(ROM.GetPackedFile("world/data/field/area/field_sub_area/field_sub_area_array.bin")).Table;
insideAreas = FlatBufferConverter.DeserializeFrom<FieldInsideAreaArray>(ROM.GetPackedFile("world/data/field/area/field_inside_area/field_inside_area_array.bin")).Table;
dungeonAreas = FlatBufferConverter.DeserializeFrom<FieldDungeonAreaArray>(ROM.GetPackedFile("world/data/field/area/field_dungeon_area/field_dungeon_area_array.bin")).Table;
fieldLocations = FlatBufferConverter.DeserializeFrom<FieldLocationArray>(ROM.GetPackedFile("world/data/field/area/field_location/field_location_array.bin")).Table;
}
public AreaInfo FindAreaInfo(string name)
{
foreach (var area in mainAreas)
{
if (area.Name == name)
return area.AreaInfo;
}
foreach (var area in subAreas)
{
if (area.Name == name)
return area.AreaInfo;
}
foreach (var area in insideAreas)
{
if (area.Name == name)
return area.AreaInfo;
}
foreach (var area in dungeonAreas)
{
if (area.Name == name)
return area.AreaInfo;
}
foreach (var area in fieldLocations)
{
if (area.Name == name)
return area.AreaInfo;
}
throw new ArgumentException($"Unknown area {name}");
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using pkNX.Containers;
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
public class PaldeaFixedSymbolModel
{
public readonly List<PaldeaFixedSymbolPoint> scarletPoints;
public readonly List<PaldeaFixedSymbolPoint> violetPoints;
public PaldeaFixedSymbolModel(IFileInternal ROM)
{
scarletPoints = new List<PaldeaFixedSymbolPoint>();
violetPoints = new List<PaldeaFixedSymbolPoint>();
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<TrinitySceneObjectTemplateSV>(p0Data);
var p1 = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateSV>(p1Data);
scarletPoints.AddRange(GetObjectTemplateSymbolPoints(p0));
violetPoints.AddRange(GetObjectTemplateSymbolPoints(p1));
}
private IEnumerable<PaldeaFixedSymbolPoint> GetObjectTemplateSymbolPoints(TrinitySceneObjectTemplateSV template)
{
foreach (var obj in template.Objects)
{
switch (obj.Type)
{
case "trinity_ObjectTemplate":
{
var sObj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateDataSV>(obj.Data);
if (sObj.Type != "trinity_ScenePoint")
continue;
var scenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePointSV>(sObj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
case "trinity_ScenePoint":
{
var scenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePointSV>(obj.Data);
foreach (var f in GetScenePointSymbolPoints(scenePoint, obj.SubObjects))
yield return f;
break;
}
}
}
}
private IEnumerable<PaldeaFixedSymbolPoint> GetScenePointSymbolPoints(TrinityScenePointSV scenePoint, TrinitySceneObjectTemplateEntrySV[] subObjects)
{
// Handle SubObjects
for (var i = 0; i < subObjects.Length; i++)
{
var sobj = subObjects[i];
switch (sobj.Type)
{
case "trinity_PropertySheet":
{
var propSheet = FlatBufferConverter.DeserializeFrom<TrinityPropertySheetSV>(sobj.Data);
if (propSheet.Name == "fixed_symbol_point")
{
var tableKey = GetTableKey(propSheet);
if (!string.IsNullOrEmpty(tableKey))
{
yield return new PaldeaFixedSymbolPoint(tableKey, scenePoint.Position);
}
}
break;
}
case "trinity_ScenePoint":
{
var subScenePoint = FlatBufferConverter.DeserializeFrom<TrinityScenePointSV>(sobj.Data);
foreach (var f in GetScenePointSymbolPoints(subScenePoint, sobj.SubObjects))
yield return f;
break;
}
default:
throw new ArgumentException($"Unknown SubObject {sobj.Type}");
}
}
}
public static string GetTableKey(TrinityPropertySheetSV propSheet)
{
if (propSheet.Name != "fixed_symbol_point")
throw new ArgumentException($"Invalid PropertySheet {propSheet.Name}");
if (propSheet.Properties[0].Fields[1].Name != "tableKey")
throw new ArgumentException("Invalid PropertySheet field layout");
if (!propSheet.Properties[0].Fields[1].Data.TryGet(out TrinityPropertySheetFieldStringValueSV? sv))
throw new ArgumentException("Could not get PropertySheet Table Key");
return sv.Value;
}
}

View File

@@ -0,0 +1,20 @@
using pkNX.Structures.FlatBuffers;
namespace pkNX.WinForms;
public class PaldeaFixedSymbolPoint
{
public string TableKey;
public PackedVec3f Position;
public PaldeaFixedSymbolPoint(string key, PackedVec3f pos)
{
TableKey = key;
Position = new PackedVec3f
{
X = pos.X,
Y = pos.Y,
Z = pos.Z,
};
}
}

View File

@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using pkNX.Containers;
namespace pkNX.Structures.FlatBuffers;
public class PaldeaSceneModel
{
public readonly List<string> areaNames;
public readonly Dictionary<string, AreaInfo> AreaInfos;
public readonly Dictionary<string, HavokCollision.AABBTree> areaColTrees;
public readonly Dictionary<string, BoxCollision9> areaColBoxes;
public readonly Dictionary<string, bool> isAtlantis;
public PaldeaSceneModel(IFileInternal ROM, PaldeaFieldModel field)
{
// NOTE: Safe to only use _0 because _1 is identical.
var area_management = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateSV>(ROM.GetPackedFile("world/scene/parts/field/field_system/area_management_/area_management_0.trscn"));
Debug.Assert(area_management.ObjectTemplateName == "area_management");
// NOTE: Safe to only use _0 because _1 is identical.
var a_w23_field_area_col = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateSV>(ROM.GetPackedFile("world/scene/parts/field/room/a_w23_field/a_w23_field_area_col_/a_w23_field_area_col_0.trscn"));
Debug.Assert(a_w23_field_area_col.ObjectTemplateName == "a_w23_field_area_col");
areaNames = new List<string>();
AreaInfos = new Dictionary<string, AreaInfo>();
areaColTrees = new Dictionary<string, HavokCollision.AABBTree>();
areaColBoxes = new Dictionary<string, BoxCollision9>();
isAtlantis = new Dictionary<string, bool>();
void AddSceneObject(string name, TrinitySceneObjectSV sceneObject, TrinityCollisionComponent1SV collisionComponent)
{
areaNames.Add(name);
AreaInfos[name] = field.FindAreaInfo(name);
Debug.Assert(collisionComponent.CollisionShape.Discriminator is 2 or 4);
if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeBoxSV? box))
{
// Box collision, obj.ObjectPosition.Field_02 is pos, box.Field_01 is size of box
areaColBoxes[name] = new BoxCollision9
{
Position = sceneObject.ObjectPosition.Field_02,
Size = box.Field_01,
};
}
else if (collisionComponent.CollisionShape.TryGet(out TrinityCollisionShapeHavokSV? havok))
{
var havokData = ROM.GetPackedFile(havok.TrcolFilePath);
areaColTrees[name] = HavokCollision.ParseAABBTree(havokData);
}
}
foreach (var obj in area_management.Objects.Concat(a_w23_field_area_col.Objects))
{
var isAtlantisObj = a_w23_field_area_col.Objects.Contains(obj);
if (!(obj.SubObjects.Length > 0 && obj.SubObjects[0].Type == "trinity_CollisionComponent"))
continue;
var collisionComponent = FlatBufferConverter.DeserializeFrom<TrinityCollisionComponentSV>(obj.SubObjects[0].Data).Component.Item1;
switch (obj.Type)
{
case "trinity_ObjectTemplate":
{
var sObj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateDataSV>(obj.Data);
if (sObj.Type != "trinity_SceneObject")
continue;
var sceneObject = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectSV>(sObj.Data);
Debug.Assert(sceneObject.ObjectName == sObj.ObjectTemplateExtra);
AddSceneObject(sObj.ObjectTemplateName, sceneObject, collisionComponent);
isAtlantis[sObj.ObjectTemplateName] = isAtlantisObj;
break;
}
case "trinity_SceneObject":
{
var sceneObject = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectSV>(obj.Data);
AddSceneObject(sceneObject.ObjectName, sceneObject, collisionComponent);
isAtlantis[sceneObject.ObjectName] = isAtlantisObj;
break;
}
}
}
}
public bool IsPointContained(string areaName, float x, float y, float z)
{
if (areaColTrees.TryGetValue(areaName, out var tree))
return tree.ContainsPoint(x, y, z);
if (areaColBoxes.TryGetValue(areaName, out var box))
return box.ContainsPoint(x, y, z);
return false;
}
public bool TryGetContainsCheck(string areaName, [NotNullWhen(true)] out IContainsV3f? result)
{
if (areaColTrees.TryGetValue(areaName, out var tree))
{
result = tree;
return true;
}
if (areaColBoxes.TryGetValue(areaName, out var box))
{
result = box;
return true;
}
result = null;
return false;
}
}

View File

@@ -28,7 +28,6 @@ protected override void Dispose(bool disposing)
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MapViewer9));
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.CB_Map = new System.Windows.Forms.ComboBox();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
@@ -62,7 +61,6 @@ private void InitializeComponent()
this.ClientSize = new System.Drawing.Size(827, 614);
this.Controls.Add(this.CB_Map);
this.Controls.Add(this.pictureBox1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "MapViewer9";
this.Text = "MapViewer9";