using Microsoft.Xna.Framework; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; namespace OpenVIII { public static partial class Saves { #region Enums /// /// /// Hyne has switchlocked0 and 1 [Flags] public enum Exists : byte { Unavailable = 0x0, /// /// Shows in menu. /// Available = 0x1, /// /// Party members that cannot leave or be added /// SwitchLocked0 = 0x2, /// /// Party members that cannot leave or be added /// SwitchLocked1 = 0x4, /// /// Many have this set. I donno what it does. /// Unk8 = 0x8, /// /// Many have this set. I donno what it does. /// Unk10 = 0x10, /// /// Many have this set. I donno what it does. /// Unk20 = 0x20, /// /// Many have this set. I donno what it does. /// Unk30 = 0x40, /// /// Many have this set. I donno what it does. /// Unk40 = 0x80, } #endregion Enums #region Classes /// /// Data for each Character /// /// public partial class CharacterData : Damageable, ICharacterData { #region Fields /// /// Total amount of spells the will be loaded/saved. /// private const int MagicCapacity = 32; /// /// Total Exp /// private uint experience; #endregion Fields #region Methods private void Auto(IReadOnlyList list) { RemoveMagic(); List unlockedlist = UnlockedGFAbilities; foreach (Kernel_bin.Stat stat in list) { if (Unlocked(unlockedlist, stat)) foreach (Kernel_bin.Magic_Data spell in SortedMagic(stat)) { if (!Stat_J.ContainsValue(spell.ID)) { //TODO make smarter. //example if you can get max stat with a weaker spell use that first. // if stat is max with out spell skip if (stat != Kernel_bin.Stat.HP && TotalStat(stat) == Kernel_bin.MAX_STAT_VALUE) break; // if hp is max without spell skip else if (stat == Kernel_bin.Stat.HP && TotalStat(stat) == Kernel_bin.MAX_HP_VALUE) break; // junction spell else Stat_J[stat] = spell.ID; break; } } } } public OrderedDictionary CloneMagic() => Magics.Clone(); public Dictionary CloneMagicJunction() => new Dictionary(Stat_J); #endregion Methods /// /// Raw HP buff from items. /// public ushort _HP; /// /// Junctioned Abilities /// public List Abilities; /// /// Alt Models/different costumes /// (Normal, SeeD, Soldier...) /// public byte Alternativemodel; /// /// Junctioned Commands /// public List Commands; /// /// Compatibility With GFs /// Effects Summon speed and such. /// public Dictionary CompatibilitywithGFs; /// /// Value determines if a character shows in menu and can be added to party. /// /// 15,9,7,4,1 shows on menu, 0 locked, 6 hidden // I think I wonder if this is a flags value. /// /// public Exists Exists; /// /// Juctioned GFs - raw value /// private GFflags rawJunctionedGFs; /// /// Juctioned GFs /// public IEnumerable JunctionedGFs => Enum.GetValues(rawJunctionedGFs.GetType()).Cast().Where(x => rawJunctionedGFs.HasFlag(x) && ConvertGFEnum.ContainsKey(x)).Distinct().Select(x=> ConvertGFEnum[x]); public void JunctionGF(GFs gf) => rawJunctionedGFs |= Saves.ConvertGFEnum.FirstOrDefault(x => x.Value == gf).Key; public void RemoveJunctionedGF(GFs gf) => rawJunctionedGFs ^= Saves.ConvertGFEnum.FirstOrDefault(x => x.Value == gf).Key; //public byte _STR; //0x09 //public byte _VIT; //0x0A //public byte _MAG; //0x0B //public byte _SPR; //0x0C //public byte _SPD; //0x0D //public byte _LCK; //0x0E public OrderedDictionary Magics; /// /// Character Model /// public byte ModelID; /// /// Number of Kills /// public ushort Numberofkills; /// /// Number of KOs /// public ushort NumberofKOs; public byte Paddingorunusedcommand; /// /// Stats that can be incrased via items. Except for HP because it's a ushort not a byte. /// public Dictionary RawStats; /// /// Junctioned Magic per stat. /// public Dictionary Stat_J; public byte Unknown1; public byte Unknown2; public byte Unknown3; public byte Unknown4; /// /// Weapon /// public byte WeaponID; //public CharacterData(BinaryReader br, Characters c) => Read(br, c); public CharacterData() { } /// /// 25.4% chance to cast automaticly on gameover, if used once in battle /// /// /// Memory.State.Fieldvars. has a value that tracks if PhoenixPinion is used just need to /// find it /// public bool CanPhoenixPinion => IsDead && !(IsPetrify || (Statuses1 & (Kernel_bin.Battle_Only_Statuses.Eject)) != 0) && Memory.State.Items.Where(m => m.ID == 31 && m.QTY >= 1).Count() > 0; public Module_battle_debug.CharacterInstanceInformation CII { get; private set; } /// /// Set by GenerateCrisisLevel(), -1 means no limit break. >=0 has a limit break. /// /// -1 - 4 /// https://finalfantasy.fandom.com/wiki/Crisis_Level public sbyte CurrentCrisisLevel { get; private set; } public override int EXP => checked((int)Experience); public uint Experience { get => experience; set { if (experience == 0) experience = value; else if (!IsGameOver && experience != value) experience = value; //trying to give my self a good break point } } public ushort ExperienceToNextLevel => (ushort)(Level == 100 ? 0 : MathHelper.Clamp(CharacterStats.EXP((byte)(Level + 1)) - Experience, 0, CharacterStats.EXP(2))); private Characters id; /// /// If TeamLaguna the id will change to a Luguna Party member /// public Characters ID { get { int ind = 0; if (Memory.State != null && Memory.State.TeamLaguna && (ind = Memory.State.PartyData.FindIndex(x => x.Equals(id))) >= 0 && !Memory.State.Party.Contains(id)) return Memory.State.Party[ind]; return id; } } public override bool IsCritical => CurrentHP() <= CriticalHP(); public override byte Level { get { if (CharacterStats != null) return CharacterStats.LEVEL(Experience); return 0; } } /// /// If TeamLaguna the name will change to a Luguna Party member /// public override FF8String Name { get { if (Memory.State != null && Memory.State.TeamLaguna) { return Memory.Strings.GetName(ID); } return base.Name; } set => base.Name = value; } public List UnlockedGFAbilities { get { BitArray total = new BitArray(16 * 8); List abilities = new List(); foreach (GFs gf in JunctionedGFs) { total.Or(Memory.State.GFs[gf].Complete); } for (int i = 1; i < total.Length; i++)//0 is none so skipping it. { if (total[i]) abilities.Add((Kernel_bin.Abilities)i); } return abilities; } } //0x6B (padding?) /// /// Visible /// public bool Available => (Exists & Exists.Available) != 0; /// /// Kernel Stats /// public Kernel_bin.Character_Stats CharacterStats { get { if (Kernel_bin.CharacterStats != null && Kernel_bin.CharacterStats.TryGetValue(id, out Kernel_bin.Character_Stats value)) return value; return null; } } public override byte EVA => checked((byte)TotalStat(Kernel_bin.Stat.EVA)); public override byte HIT => checked((byte)TotalStat(Kernel_bin.Stat.HIT)); public override byte LUCK => checked((byte)TotalStat(Kernel_bin.Stat.LUCK)); public override byte MAG => checked((byte)TotalStat(Kernel_bin.Stat.MAG)); public override byte SPD => checked((byte)TotalStat(Kernel_bin.Stat.SPD)); public override byte SPR => checked((byte)TotalStat(Kernel_bin.Stat.SPR)); public override byte STR => checked((byte)TotalStat(Kernel_bin.Stat.STR)); /// /// Cannot remove from party or add to party. /// public bool SwitchLocked => (Exists & (Exists.SwitchLocked0 | Exists.SwitchLocked1)) != 0; public override byte VIT => checked((byte)TotalStat(Kernel_bin.Stat.VIT)); public void AutoATK() => Auto(Kernel_bin.AutoATK); public void AutoDEF() => Auto(Kernel_bin.AutoDEF); public void AutoMAG() => Auto(Kernel_bin.AutoMAG); public void BattleStart(Module_battle_debug.CharacterInstanceInformation cii) { CII = cii; Statuses1 = Kernel_bin.Battle_Only_Statuses.None; if (Abilities.Contains(Kernel_bin.Abilities.Auto_Haste)) Statuses1 |= Kernel_bin.Battle_Only_Statuses.Haste; if (Abilities.Contains(Kernel_bin.Abilities.Auto_Protect)) Statuses1 |= Kernel_bin.Battle_Only_Statuses.Protect; if (Abilities.Contains(Kernel_bin.Abilities.Auto_Reflect)) Statuses1 |= Kernel_bin.Battle_Only_Statuses.Reflect; if (Abilities.Contains(Kernel_bin.Abilities.Auto_Shell)) Statuses1 |= Kernel_bin.Battle_Only_Statuses.Shell; //reset the ATB timer. ATBTimer.FirstTurn(); } public override Damageable Clone() { //Shadowcopy CharacterData c = (CharacterData)MemberwiseClone(); //Deepcopy c.Name = Name.Clone(); c.CompatibilitywithGFs = CompatibilitywithGFs.ToDictionary(e => e.Key, e => e.Value); c.Stat_J = Stat_J.ToDictionary(e => e.Key, e => e.Value); c.Magics = new OrderedDictionary(Magics.Count); foreach (KeyValuePair magic in Magics) c.Magics.Add(magic.Key, magic.Value); c.RawStats = RawStats.ToDictionary(e => e.Key, e => e.Value); c.Commands = Commands.ConvertAll(Item => Item); c.Abilities = Abilities.ConvertAll(Item => Item); return c; } public int CriticalHP(Characters value) => MaxHP(value) / 4 - 1; public override ushort CurrentHP() => CurrentHP(ID); public ushort CurrentHP(Characters c) { ushort max = MaxHP(c); if (max < _CurrentHP) _CurrentHP = max; return _CurrentHP; } public override short ElementalResistance(Kernel_bin.Element @in) => throw new NotImplementedException(); /// /// /// This Generates a Crisis Level. Run this each turn in battle. Though in real game it /// runs when the menu pops up. /// /// -1 means no limit break. >=0 has a limit break. /// /// -1 - 4 /// https://finalfantasy.fandom.com/wiki/Crisis_Level /// TODO: Need to confirm the formula is correct via reverse public sbyte GenerateCrisisLevel() { ushort current = CurrentHP(); ushort max = MaxHP(); //if ((id == Characters.Seifer_Almasy && CurrentHP() < (max * 84 / 100))) //{ int HPMod = CharacterStats.Crisis * 10 * current / max; int DeathBonus = Memory.State.DeadPartyMembers() * 200 + 1600; int StatusBonus = (int)(Statuses0.Count() * 10); // I think this is status of all party members int RandomMod = Memory.Random.Next(byte.MaxValue + 1) + 160; int crisislevel = (StatusBonus + DeathBonus - HPMod) / RandomMod; // better random number? if (crisislevel == 5) { CurrentCrisisLevel = 0; return CurrentCrisisLevel; } else if (crisislevel == 6) { CurrentCrisisLevel = 1; return CurrentCrisisLevel; } else if (crisislevel == 7) { CurrentCrisisLevel = 2; return CurrentCrisisLevel; } else if (crisislevel >= 8) { CurrentCrisisLevel = 3; return CurrentCrisisLevel; } //} return CurrentCrisisLevel = -1; } public void JunctionSpell(Kernel_bin.Stat stat, byte spell) { //see if magic is in use, if so remove it if (Stat_J.ContainsValue(spell)) { Kernel_bin.Stat key = Stat_J.FirstOrDefault(x => x.Value == spell).Key; Stat_J[key] = 0; } //junction magic Stat_J[stat] = spell; } /// /// Max HP /// /// Force another character's HP calculation /// public ushort MaxHP(Characters c) => TotalStat(Kernel_bin.Stat.HP, c); public override ushort MaxHP() => MaxHP(ID); public override float PercentFullHP() => PercentFullHP(ID); public float PercentFullHP(Characters c) => (float)CurrentHP(c) / MaxHP(c); public static CharacterData Load(BinaryReader br, Characters @enum, Data data) => Load(br, @enum,data); protected override void ReadData(BinaryReader br, Enum c) { if (!c.GetType().Equals(typeof(Characters))) throw new ArgumentException($"Enum {c} is not Characters"); id = (Characters)c; Name = Memory.Strings.GetName(id, Data ?? Memory.State); _CurrentHP = br.ReadUInt16();//0x00 _HP = br.ReadUInt16();//0x02 Experience = br.ReadUInt32();//0x04 ModelID = br.ReadByte();//0x08 WeaponID = br.ReadByte();//0x09 RawStats = new Dictionary(6) { [Kernel_bin.Stat.STR] = br.ReadByte(),//0x0A [Kernel_bin.Stat.VIT] = br.ReadByte(),//0x0B [Kernel_bin.Stat.MAG] = br.ReadByte(),//0x0C [Kernel_bin.Stat.SPR] = br.ReadByte(),//0x0D [Kernel_bin.Stat.SPD] = br.ReadByte(),//0x0E [Kernel_bin.Stat.LUCK] = br.ReadByte()//0x0F }; Magics = new OrderedDictionary(MagicCapacity); for (int i = 0; i < MagicCapacity; i++) { byte key = br.ReadByte(); byte val = br.ReadByte(); if (key >= 0 && !Magics.ContainsKey(key)) Magics.Add(key, val);//0x10 } Commands = Array.ConvertAll(br.ReadBytes(3), Item => (Kernel_bin.Abilities)Item).ToList();//0x50 Paddingorunusedcommand = br.ReadByte();//0x53 Abilities = Array.ConvertAll(br.ReadBytes(4), Item => (Kernel_bin.Abilities)Item).ToList();//0x54 rawJunctionedGFs = (GFflags)br.ReadUInt16();//0x58 each bit is one gf. Unknown1 = br.ReadByte();//0x5A Alternativemodel = br.ReadByte();//0x5B (Normal, SeeD, Soldier...) Stat_J = new Dictionary(9); for (int i = 0; i < 19; i++) { Kernel_bin.Stat key = (Kernel_bin.Stat)i; byte val = br.ReadByte(); if (!Stat_J.ContainsKey(key)) Stat_J.Add(key, val); } Unknown2 = br.ReadByte();//0x6F (padding?) CompatibilitywithGFs = new Dictionary(16); for (int i = 0; i < 16; i++) CompatibilitywithGFs.Add((GFs)i, br.ReadUInt16());//0x70 Numberofkills = br.ReadUInt16();//0x90 NumberofKOs = br.ReadUInt16();//0x92 Exists = (Exists)br.ReadByte();//0x94 Unknown3 = br.ReadByte();//0x95 Statuses0 = (Kernel_bin.Persistent_Statuses)br.ReadByte();//0x96 Unknown4 = br.ReadByte();//0x97 } public void RemoveAll() { Stat_J = Stat_J.ToDictionary(e => e.Key, e => (byte)0); Commands = Commands.ConvertAll(Item => Kernel_bin.Abilities.None); Abilities = Abilities.ConvertAll(Item => Kernel_bin.Abilities.None); rawJunctionedGFs = GFflags.None; } public void RemoveMagic() => Stat_J = Stat_J.ToDictionary(e => e.Key, e => (byte)0); /// /// Sorted Enumerable based on best to worst for Stat. Uses character's total magic and /// kernel bin's stat value. /// /// Stat sorting by. /// Ordered Enumberable public IOrderedEnumerable SortedMagic(Kernel_bin.Stat Stat) => Kernel_bin.MagicData.OrderBy(x => (-x.totalStatVal(Stat) * (Magics.ContainsKey(x.ID) ? Magics[x.ID] : 0)) / 100); public override sbyte StatusResistance(Kernel_bin.Battle_Only_Statuses s) => throw new NotImplementedException(); public override sbyte StatusResistance(Kernel_bin.Persistent_Statuses s) => throw new NotImplementedException(); public override string ToString() => Name.Length > 0 ? Name.ToString() : base.ToString(); public ushort TotalStat(Kernel_bin.Stat s, Characters c = Characters.Blank) { if (c == Characters.Blank) c = id; if (c != id && c < Characters.Laguna_Loire) throw new ArgumentException($"{this}::Wrong visible character value({c}). Must match ({id}) unless Laguna Kiros or Ward!"); int total = 0; if (Kernel_bin.Statpercentabilities != null) foreach (Kernel_bin.Abilities i in Abilities) { if (Kernel_bin.Statpercentabilities.TryGetValue(i, out Kernel_bin.Stat_percent_abilities ability) && ability.Stat == s) total += ability.Value; } if (CharacterStats != null) switch (s) { case Kernel_bin.Stat.HP: return CharacterStats.HP((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], _HP, total); case Kernel_bin.Stat.EVA: //TODO confirm if there is no flat stat buff for eva. If there isn't then remove from function. return CharacterStats.EVA((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], 0, TotalStat(Kernel_bin.Stat.SPD, c), total); case Kernel_bin.Stat.SPD: return CharacterStats.SPD((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total); case Kernel_bin.Stat.HIT: return CharacterStats.HIT(Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], WeaponID); case Kernel_bin.Stat.LUCK: return CharacterStats.LUCK((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total); case Kernel_bin.Stat.MAG: return CharacterStats.MAG((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total); case Kernel_bin.Stat.SPR: return CharacterStats.SPR((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total); case Kernel_bin.Stat.STR: return CharacterStats.STR((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total, WeaponID); case Kernel_bin.Stat.VIT: return CharacterStats.VIT((sbyte)Level, Stat_J[s], Stat_J[s] == 0 ? 0 : Magics[Stat_J[s]], RawStats[s], total); } return 0; } public override ushort TotalStat(Kernel_bin.Stat s) => TotalStat(s, ID); public bool Unlocked(Kernel_bin.Stat stat) => Unlocked(UnlockedGFAbilities, stat); public bool Unlocked(List unlocked, Kernel_bin.Stat stat) { switch (stat) { default: return unlocked.Contains(Kernel_bin.Stat2Ability[stat]); case Kernel_bin.Stat.EL_Atk: return unlocked.Contains(Kernel_bin.Abilities.EL_Atk_J); case Kernel_bin.Stat.EL_Def_1: return unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx1) || unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx2) || unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx4); case Kernel_bin.Stat.EL_Def_2: return unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx2) || unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx4); case Kernel_bin.Stat.EL_Def_3: case Kernel_bin.Stat.EL_Def_4: return unlocked.Contains(Kernel_bin.Abilities.EL_Def_Jx4); case Kernel_bin.Stat.ST_Atk: return unlocked.Contains(Kernel_bin.Abilities.ST_Atk_J); case Kernel_bin.Stat.ST_Def_1: return unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx1) || unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx2) || unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx4); case Kernel_bin.Stat.ST_Def_2: return unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx2) || unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx4); case Kernel_bin.Stat.ST_Def_3: case Kernel_bin.Stat.ST_Def_4: return unlocked.Contains(Kernel_bin.Abilities.ST_Def_Jx4); } } } #endregion Classes } }