PKHeX/PKHeX.Core/Legality/Structures/LegalInfo.cs

96 lines
4.2 KiB
C#
Raw Normal View History

using System.Collections.Generic;
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
namespace PKHeX.Core
{
/// <summary>
/// Calculated Information storage with properties useful for parsing the legality of the input <see cref="PKM"/>.
/// </summary>
2020-12-24 23:23:38 +00:00
public sealed class LegalInfo : IGeneration
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
{
/// <summary>The <see cref="PKM"/> object used for comparisons.</summary>
private readonly PKM pkm;
/// <summary>The generation of games the <see cref="PKM"/> originated from.</summary>
public int Generation { get; internal set; }
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
/// <summary>The Game the <see cref="PKM"/> originated from.</summary>
public GameVersion Game { get; internal set; }
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
/// <summary>The matched Encounter details for the <see cref="PKM"/>. </summary>
public IEncounterable EncounterMatch
{
get => _match;
set
{
if (!ReferenceEquals(_match, EncounterInvalid.Default) && (value.LevelMin != _match.LevelMin || value.Species != _match.Species))
_evochains = null; // clear if evo chain has the potential to be different
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
_match = value;
Parse.Clear();
}
}
/// <summary>
/// Original encounter data for the <see cref="pkm"/>.
/// </summary>
/// <remarks>
/// Generation 1/2 <see cref="pkm"/> that are transferred forward to Generation 7 are restricted to new encounter details.
/// By retaining their original match, more information can be provided by the parse.
/// </remarks>
public IEncounterable EncounterOriginal => EncounterOriginalGB ?? EncounterMatch;
internal IEncounterable? EncounterOriginalGB;
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
private IEncounterable _match = EncounterInvalid.Default;
/// <summary>Top level Legality Check result list for the <see cref="EncounterMatch"/>.</summary>
internal readonly List<CheckResult> Parse;
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
public readonly CheckResult[] Relearn = new CheckResult[4];
public CheckMoveResult[] Moves { get; internal set; } = new CheckMoveResult[4];
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
2020-12-22 01:17:56 +00:00
private static readonly ValidEncounterMoves NONE = new();
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
public ValidEncounterMoves EncounterMoves { get; internal set; } = NONE;
public IReadOnlyList<EvoCriteria>[] EvoChainsAllGens => _evochains ??= EvolutionChain.GetEvolutionChainsAllGens(pkm, EncounterMatch);
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
private IReadOnlyList<EvoCriteria>[]? _evochains;
/// <summary><see cref="RNG"/> related information that generated the <see cref="PKM.PID"/>/<see cref="PKM.IVs"/> value(s).</summary>
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
public PIDIV PIDIV
{
get => _pidiv;
internal set
{
_pidiv = value;
PIDParsed = true;
}
}
public bool PIDParsed { get; private set; }
private PIDIV _pidiv = PIDIV.None;
/// <summary>Indicates whether or not the <see cref="PIDIV"/> can originate from the <see cref="EncounterMatch"/>.</summary>
/// <remarks>This boolean is true until all valid <see cref="PIDIV"/> encounters are tested, after which it is false.</remarks>
public bool PIDIVMatches { get; internal set; } = true;
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
/// <summary>Indicates whether or not the <see cref="PIDIV"/> can originate from the <see cref="EncounterMatch"/> with explicit <see cref="RNG"/> <see cref="Frame"/> matching.</summary>
/// <remarks>This boolean is true until all valid <see cref="Frame"/> entries are tested for all possible <see cref="EncounterSlot"/> matches, after which it is false.</remarks>
public bool FrameMatches { get; internal set; } = true;
public LegalInfo(PKM pk, List<CheckResult> parse)
{
pkm = pk;
Parse = parse;
// Store repeatedly accessed values
Game = (GameVersion)pkm.Version;
Generation = pkm.Generation;
}
/// <summary>List of all near-matches that were rejected for a given reason.</summary>
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
public List<EncounterRejected>? InvalidMatches;
internal void Reject(CheckResult c)
{
(InvalidMatches ??= new List<EncounterRejected>()).Add(new EncounterRejected(EncounterMatch, c));
}
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
}
}