mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
Feature: Localization of Battle Templates (Showdown Set) (#4482)
* Localization capability for each language, import & export * Lines with stat names (IVs/EVs) can be configured to many representations (X/X/X/X/X/X, HABCDS, etc). * Add nonstandard localizations * Add token types for Showdown's new set format * Add new program settings for hover & export styles. Allows users to select which presentation format they want for the hover previews, as well as the set export format. * Revises preview hover GUI to use new settings * Revises export events to use new settings * Moves no longer indicate end of set * Enhance robustness of stat parsing * Expand all settings in settings editor on form load * Extract clipboard -> sets operation to api for maintainability & reusability
This commit is contained in:
parent
63516fc718
commit
f730f7d19a
281
PKHeX.Core/Editing/BattleTemplate/BattleTemplateConfig.cs
Normal file
281
PKHeX.Core/Editing/BattleTemplate/BattleTemplateConfig.cs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Grammar and prefix/suffix tokens for <see cref="IBattleTemplate"/> localization.
|
||||
/// </summary>
|
||||
public sealed record BattleTemplateConfig
|
||||
{
|
||||
public sealed record BattleTemplateTuple(BattleTemplateToken Token, string Text);
|
||||
|
||||
/// <summary> Prefix tokens - e.g. Friendship: {100} </summary>
|
||||
public required BattleTemplateTuple[] Left { get; init; }
|
||||
|
||||
/// <summary> Suffix tokens - e.g. {Timid} Nature </summary>
|
||||
public required BattleTemplateTuple[] Right { get; init; }
|
||||
|
||||
/// <summary> Tokens that always display the same text, with no value - e.g. Shiny: Yes </summary>
|
||||
public required BattleTemplateTuple[] Center { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stat names, ordered with speed in the middle (not last).
|
||||
/// </summary>
|
||||
public required StatDisplayConfig StatNames { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stat names, ordered with speed in the middle (not last).
|
||||
/// </summary>
|
||||
public required StatDisplayConfig StatNamesFull { get; init; }
|
||||
|
||||
public required string Male { get; init; }
|
||||
public required string Female { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stat names in the requested format.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public StatDisplayConfig GetStatDisplay(StatDisplayStyle style = StatDisplayStyle.Abbreviated) => style switch
|
||||
{
|
||||
StatDisplayStyle.Abbreviated => StatNames,
|
||||
StatDisplayStyle.Full => StatNamesFull,
|
||||
StatDisplayStyle.HABCDS => StatDisplayConfig.HABCDS,
|
||||
StatDisplayStyle.Raw => StatDisplayConfig.Raw,
|
||||
StatDisplayStyle.Raw00 => StatDisplayConfig.Raw00,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(style), style, null),
|
||||
};
|
||||
|
||||
public static ReadOnlySpan<char> GetMoveDisplay(MoveDisplayStyle style = MoveDisplayStyle.Fill) => style switch
|
||||
{
|
||||
MoveDisplayStyle.Fill => "----",
|
||||
MoveDisplayStyle.Directional => "↑←↓→",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(style), style, null),
|
||||
};
|
||||
|
||||
public static bool IsMovePrefix(char c) => c is '-' or '–' or '↑' or '←' or '↓' or '→';
|
||||
|
||||
public static ReadOnlySpan<BattleTemplateToken> CommunityStandard =>
|
||||
[
|
||||
BattleTemplateToken.FirstLine,
|
||||
BattleTemplateToken.Ability,
|
||||
BattleTemplateToken.Level,
|
||||
BattleTemplateToken.Shiny,
|
||||
BattleTemplateToken.Friendship,
|
||||
BattleTemplateToken.DynamaxLevel,
|
||||
BattleTemplateToken.Gigantamax,
|
||||
BattleTemplateToken.TeraType,
|
||||
BattleTemplateToken.EVs,
|
||||
BattleTemplateToken.Nature,
|
||||
BattleTemplateToken.IVs,
|
||||
BattleTemplateToken.Moves,
|
||||
];
|
||||
|
||||
public static ReadOnlySpan<BattleTemplateToken> Showdown => CommunityStandard;
|
||||
|
||||
public static ReadOnlySpan<BattleTemplateToken> ShowdownNew =>
|
||||
[
|
||||
BattleTemplateToken.FirstLine,
|
||||
BattleTemplateToken.AbilityHeldItem,
|
||||
BattleTemplateToken.Moves,
|
||||
BattleTemplateToken.EVsAppendNature,
|
||||
BattleTemplateToken.IVs,
|
||||
BattleTemplateToken.Level,
|
||||
BattleTemplateToken.Shiny,
|
||||
BattleTemplateToken.Friendship,
|
||||
BattleTemplateToken.DynamaxLevel,
|
||||
BattleTemplateToken.Gigantamax,
|
||||
BattleTemplateToken.TeraType,
|
||||
];
|
||||
|
||||
public static ReadOnlySpan<BattleTemplateToken> DefaultHover =>
|
||||
[
|
||||
// First line is handled manually.
|
||||
BattleTemplateToken.HeldItem,
|
||||
BattleTemplateToken.Ability,
|
||||
BattleTemplateToken.Level,
|
||||
BattleTemplateToken.Shiny,
|
||||
BattleTemplateToken.DynamaxLevel,
|
||||
BattleTemplateToken.Gigantamax,
|
||||
BattleTemplateToken.TeraType,
|
||||
BattleTemplateToken.EVs,
|
||||
BattleTemplateToken.IVs,
|
||||
BattleTemplateToken.Nature,
|
||||
BattleTemplateToken.Moves,
|
||||
|
||||
// Other tokens are handled manually (Ganbaru, Awakening) as they are not stored by the battle template interface, only entity objects.
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the line for a token and value, if applicable.
|
||||
/// </summary>
|
||||
/// <param name="line">Line to parse</param>
|
||||
/// <param name="value">Value for the token, if applicable</param>
|
||||
/// <returns>Token type that was found</returns>
|
||||
public BattleTemplateToken TryParse(ReadOnlySpan<char> line, out ReadOnlySpan<char> value)
|
||||
{
|
||||
value = default;
|
||||
if (line.Length == 0)
|
||||
return BattleTemplateToken.None;
|
||||
foreach (var tuple in Left)
|
||||
{
|
||||
if (!line.StartsWith(tuple.Text, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
value = line[tuple.Text.Length..];
|
||||
return tuple.Token;
|
||||
}
|
||||
foreach (var tuple in Right)
|
||||
{
|
||||
if (!line.EndsWith(tuple.Text, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
value = line[..^tuple.Text.Length];
|
||||
return tuple.Token;
|
||||
}
|
||||
foreach (var tuple in Center)
|
||||
{
|
||||
if (!line.Equals(tuple.Text, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
return tuple.Token;
|
||||
}
|
||||
return BattleTemplateToken.None;
|
||||
}
|
||||
|
||||
private string GetToken(BattleTemplateToken token, out bool isLeft)
|
||||
{
|
||||
foreach (var tuple in Left)
|
||||
{
|
||||
if (tuple.Token != token)
|
||||
continue;
|
||||
isLeft = true;
|
||||
return tuple.Text;
|
||||
}
|
||||
foreach (var tuple in Right)
|
||||
{
|
||||
if (tuple.Token != token)
|
||||
continue;
|
||||
isLeft = false;
|
||||
return tuple.Text;
|
||||
}
|
||||
foreach (var tuple in Center)
|
||||
{
|
||||
if (tuple.Token != token)
|
||||
continue;
|
||||
isLeft = false;
|
||||
return tuple.Text;
|
||||
}
|
||||
throw new ArgumentException($"Token {token} not found in config");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of the token. No value is combined with it.
|
||||
/// </summary>
|
||||
public string Push(BattleTemplateToken token) => GetToken(token, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of the token, and combines the value with it.
|
||||
/// </summary>
|
||||
public string Push<T>(BattleTemplateToken token, T value)
|
||||
{
|
||||
var str = GetToken(token, out var isLeft);
|
||||
if (isLeft)
|
||||
return $"{str}{value}";
|
||||
return $"{value}{str}";
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Push{T}(BattleTemplateToken,T)"/>
|
||||
public void Push<T>(BattleTemplateToken token, T value, StringBuilder sb)
|
||||
{
|
||||
var str = GetToken(token, out var isLeft);
|
||||
if (isLeft)
|
||||
sb.Append(str).Append(value);
|
||||
else
|
||||
sb.Append(value).Append(str);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks all representations of the stat name for a match.
|
||||
/// </summary>
|
||||
/// <param name="stat">Stat name</param>
|
||||
/// <returns>-1 if not found, otherwise the index of the stat</returns>
|
||||
public int GetStatIndex(ReadOnlySpan<char> stat)
|
||||
{
|
||||
var index = StatNames.GetStatIndex(stat);
|
||||
if (index != -1)
|
||||
return index;
|
||||
index = StatNamesFull.GetStatIndex(stat);
|
||||
if (index != -1)
|
||||
return index;
|
||||
|
||||
foreach (var set in StatDisplayConfig.Custom)
|
||||
{
|
||||
index = set.GetStatIndex(stat);
|
||||
if (index != -1)
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public StatParseResult TryParseStats(ReadOnlySpan<char> message, Span<int> bestResult)
|
||||
{
|
||||
var result = ParseInternal(message, bestResult);
|
||||
ReorderSpeedNotLast(bestResult);
|
||||
result.TreatAmpsAsSpeedNotLast();
|
||||
return result;
|
||||
}
|
||||
|
||||
private StatParseResult ParseInternal(ReadOnlySpan<char> message, Span<int> bestResult)
|
||||
{
|
||||
Span<int> original = stackalloc int[bestResult.Length];
|
||||
bestResult.CopyTo(original);
|
||||
|
||||
var result = StatNames.TryParse(message, bestResult);
|
||||
if (result.IsParseClean)
|
||||
return result;
|
||||
|
||||
// Check if the others get a better result
|
||||
int bestCount = result.CountParsed;
|
||||
Span<int> tmp = stackalloc int[bestResult.Length];
|
||||
// Check Long Stat names
|
||||
{
|
||||
original.CopyTo(tmp); // restore original defaults
|
||||
var other = StatNamesFull.TryParse(message, tmp);
|
||||
if (other.IsParseClean)
|
||||
{
|
||||
tmp.CopyTo(bestResult);
|
||||
return other;
|
||||
}
|
||||
if (other.CountParsed > bestCount)
|
||||
{
|
||||
bestCount = other.CountParsed;
|
||||
tmp.CopyTo(bestResult);
|
||||
}
|
||||
}
|
||||
// Check custom parsers
|
||||
foreach (var set in StatDisplayConfig.Custom)
|
||||
{
|
||||
original.CopyTo(tmp); // restore original defaults
|
||||
var other = set.TryParse(message, tmp);
|
||||
if (other.IsParseClean)
|
||||
{
|
||||
tmp.CopyTo(bestResult);
|
||||
return other;
|
||||
}
|
||||
if (other.CountParsed > bestCount)
|
||||
{
|
||||
bestCount = other.CountParsed;
|
||||
tmp.CopyTo(bestResult);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ReorderSpeedNotLast<T>(Span<T> arr)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(arr.Length, 6);
|
||||
var speed = arr[5];
|
||||
arr[5] = arr[4];
|
||||
arr[4] = arr[3];
|
||||
arr[3] = speed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Token order for displaying the battle template.
|
||||
/// </summary>
|
||||
public enum BattleTemplateDisplayStyle : sbyte
|
||||
{
|
||||
Custom = -1,
|
||||
Showdown = 0, // default
|
||||
Legacy,
|
||||
Brief, // default preview hover style
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for exporting a battle template.
|
||||
/// </summary>
|
||||
public readonly ref struct BattleTemplateExportSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Order of the tokens in the export.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<BattleTemplateToken> Order { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Localization for the battle template.
|
||||
/// </summary>
|
||||
public BattleTemplateLocalization Localization { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Display style for the EVs.
|
||||
/// </summary>
|
||||
public StatDisplayStyle StatsEVs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Display style for the IVs.
|
||||
/// </summary>
|
||||
public StatDisplayStyle StatsIVs { get; init; }
|
||||
|
||||
public StatDisplayStyle StatsOther { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Display style for the moves.
|
||||
/// </summary>
|
||||
public MoveDisplayStyle Moves { get; init; }
|
||||
|
||||
public static BattleTemplateExportSettings Showdown => new(BattleTemplateConfig.Showdown);
|
||||
public static BattleTemplateExportSettings CommunityStandard => new(BattleTemplateConfig.CommunityStandard);
|
||||
|
||||
public BattleTemplateExportSettings(string language) : this(BattleTemplateConfig.Showdown, language) { }
|
||||
public BattleTemplateExportSettings(LanguageID language) : this(BattleTemplateConfig.Showdown, language) { }
|
||||
|
||||
public BattleTemplateExportSettings(ReadOnlySpan<BattleTemplateToken> order, string language = BattleTemplateLocalization.DefaultLanguage)
|
||||
{
|
||||
Localization = BattleTemplateLocalization.GetLocalization(language);
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public BattleTemplateExportSettings(ReadOnlySpan<BattleTemplateToken> order, LanguageID language)
|
||||
{
|
||||
Localization = BattleTemplateLocalization.GetLocalization(language);
|
||||
Order = order;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the token is in the export.
|
||||
/// </summary>
|
||||
public bool IsTokenInExport(BattleTemplateToken token)
|
||||
{
|
||||
foreach (var t in Order)
|
||||
{
|
||||
if (t == token)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the token in the export.
|
||||
/// </summary>
|
||||
public int GetTokenIndex(BattleTemplateToken token)
|
||||
{
|
||||
for (int i = 0; i < Order.Length; i++)
|
||||
{
|
||||
if (Order[i] == token)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the token is in the export.
|
||||
/// </summary>
|
||||
/// <remarks>Should be a static method, but is not because it feels better this way.</remarks>
|
||||
/// <param name="token">Token to check</param>
|
||||
/// <param name="tokens">Tokens to check against</param>
|
||||
public bool IsTokenInExport(BattleTemplateToken token, ReadOnlySpan<BattleTemplateToken> tokens)
|
||||
{
|
||||
foreach (var t in tokens)
|
||||
{
|
||||
if (t == token)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides information for localizing <see cref="IBattleTemplate"/> sets.
|
||||
/// </summary>
|
||||
/// <param name="Strings">In-game strings</param>
|
||||
/// <param name="Config">Grammar and prefix/suffix tokens</param>
|
||||
public sealed record BattleTemplateLocalization(GameStrings Strings, BattleTemplateConfig Config)
|
||||
{
|
||||
public const string DefaultLanguage = GameLanguage.DefaultLanguage; // English
|
||||
|
||||
private static readonly Dictionary<string, BattleTemplateLocalization> Cache = new();
|
||||
public static readonly BattleTemplateLocalization Default = GetLocalization(DefaultLanguage);
|
||||
|
||||
/// <param name="language"><see cref="LanguageID"/> index</param>
|
||||
/// <inheritdoc cref="GetLocalization(string)"/>
|
||||
public static BattleTemplateLocalization GetLocalization(LanguageID language) =>
|
||||
GetLocalization(language.GetLanguageCode());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localization for the requested language.
|
||||
/// </summary>
|
||||
/// <param name="language">Language code</param>
|
||||
public static BattleTemplateLocalization GetLocalization(string language)
|
||||
{
|
||||
if (Cache.TryGetValue(language, out var result))
|
||||
return result;
|
||||
|
||||
var strings = GameInfo.GetStrings(language);
|
||||
var cfg = GetConfig(language);
|
||||
result = new BattleTemplateLocalization(strings, cfg);
|
||||
Cache[language] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string GetJson(string language) => Util.GetStringResource($"battle_{language}.json");
|
||||
private static BattleTemplateConfigContext GetContext() => new();
|
||||
|
||||
private static BattleTemplateConfig GetConfig(string language)
|
||||
{
|
||||
var text = GetJson(language);
|
||||
var result = JsonSerializer.Deserialize(text, GetContext().BattleTemplateConfig)
|
||||
?? throw new JsonException($"Failed to deserialize {nameof(BattleTemplateConfig)} for {language}");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force loads all localizations.
|
||||
/// </summary>
|
||||
public static bool ForceLoadAll()
|
||||
{
|
||||
bool anyLoaded = false;
|
||||
foreach (var lang in GameLanguage.AllSupportedLanguages)
|
||||
{
|
||||
if (Cache.ContainsKey(lang))
|
||||
continue;
|
||||
_ = GetLocalization(lang);
|
||||
anyLoaded = true;
|
||||
}
|
||||
return anyLoaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all localizations.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, BattleTemplateLocalization> GetAll()
|
||||
{
|
||||
_ = ForceLoadAll();
|
||||
return Cache;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(BattleTemplateConfig))]
|
||||
public sealed partial class BattleTemplateConfigContext : JsonSerializerContext;
|
||||
49
PKHeX.Core/Editing/BattleTemplate/BattleTemplateToken.cs
Normal file
49
PKHeX.Core/Editing/BattleTemplate/BattleTemplateToken.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Enum for the different tokens used in battle templates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each token represents a specific aspect of a Pokémon's battle template.
|
||||
/// One token per line. Each token can have specific grammar rules depending on the language.
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<BattleTemplateToken>))]
|
||||
public enum BattleTemplateToken : byte
|
||||
{
|
||||
None = 0, // invalid, used as a magic value to signal that a token is not recognized
|
||||
|
||||
// Standard tokens
|
||||
Shiny,
|
||||
Ability,
|
||||
Nature,
|
||||
Friendship,
|
||||
EVs,
|
||||
IVs,
|
||||
Level,
|
||||
DynamaxLevel,
|
||||
Gigantamax,
|
||||
TeraType,
|
||||
|
||||
// Tokens that can appear multiple times
|
||||
Moves,
|
||||
|
||||
// When present, first line will not contain values for these tokens (instead outputting on separate token line)
|
||||
// Not part of the standard export format, but can be recognized/optionally used in the program
|
||||
HeldItem,
|
||||
Nickname,
|
||||
Gender,
|
||||
|
||||
// Manually appended, not stored or recognized on import
|
||||
AVs,
|
||||
GVs,
|
||||
|
||||
// Future Showdown propositions
|
||||
AbilityHeldItem, // [Ability] Item
|
||||
EVsWithNature, // +/-
|
||||
EVsAppendNature, // +/- and .. (Nature)
|
||||
|
||||
// Omitting the first line (species) shouldn't be done unless it is manually added in the presentation/export.
|
||||
FirstLine = byte.MaxValue,
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
public sealed class BattleTemplateSettings
|
||||
{
|
||||
[LocalizedDescription("Settings for showing details when hovering a slot.")]
|
||||
public BattleTemplateTypeSetting Hover { get; set; } = new(BattleTemplateDisplayStyle.Brief, LanguageID.None, MoveDisplayStyle.Directional);
|
||||
|
||||
[LocalizedDescription("Settings for showing details when exporting a slot.")]
|
||||
public BattleTemplateTypeSetting Export { get; set; } = new(BattleTemplateDisplayStyle.Showdown, LanguageID.English);
|
||||
}
|
||||
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
public sealed class BattleTemplateTypeSetting
|
||||
{
|
||||
|
||||
[LocalizedDescription("Language to use when exporting a battle template. If not specified in settings, will use current language.")]
|
||||
public LanguageID Language { get; set; }
|
||||
public StatDisplayStyle StyleStatEVs { get; set; }
|
||||
public StatDisplayStyle StyleStatIVs { get; set; }
|
||||
public StatDisplayStyle StyleStatOther { get; set; }
|
||||
public MoveDisplayStyle StyleMove { get; set; }
|
||||
|
||||
[LocalizedDescription("Custom stat labels and grammar.")]
|
||||
public StatDisplayConfig StatsCustom { get; set; } = StatDisplayConfig.HABCDS;
|
||||
|
||||
[LocalizedDescription("Display format to use when exporting a battle template from the program.")]
|
||||
public BattleTemplateDisplayStyle TokenOrder { get; set; }
|
||||
|
||||
[LocalizedDescription("Custom ordering for exporting a set, if chosen via export display style.")]
|
||||
public BattleTemplateToken[] TokenOrderCustom { get; set; } = BattleTemplateConfig.Showdown.ToArray();
|
||||
|
||||
public BattleTemplateTypeSetting() { }
|
||||
public BattleTemplateTypeSetting(BattleTemplateDisplayStyle style, LanguageID lang, MoveDisplayStyle move = MoveDisplayStyle.Fill)
|
||||
{
|
||||
TokenOrder = style;
|
||||
Language = lang;
|
||||
StyleMove = move;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{TokenOrder} {Language}";
|
||||
|
||||
private LanguageID GetLanguageExport(LanguageID program) => GetLanguage(Language, program);
|
||||
|
||||
public BattleTemplateExportSettings GetSettings(LanguageID programLanguage, EntityContext context) => new(GetOrder(TokenOrder, TokenOrderCustom), GetLanguageExport(programLanguage))
|
||||
{
|
||||
StatsEVs = StyleStatEVs,
|
||||
StatsIVs = StyleStatIVs,
|
||||
StatsOther = StyleStatOther,
|
||||
Moves = GetMoveDisplayStyle(StyleMove, context),
|
||||
};
|
||||
|
||||
private static LanguageID GetLanguage(LanguageID choice, LanguageID program)
|
||||
{
|
||||
if (choice != LanguageID.None)
|
||||
return choice;
|
||||
if (program == LanguageID.None)
|
||||
return LanguageID.English;
|
||||
return program;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<BattleTemplateToken> GetOrder(BattleTemplateDisplayStyle style, ReadOnlySpan<BattleTemplateToken> custom) => style switch
|
||||
{
|
||||
BattleTemplateDisplayStyle.Legacy => BattleTemplateConfig.CommunityStandard,
|
||||
BattleTemplateDisplayStyle.Brief => BattleTemplateConfig.DefaultHover,
|
||||
BattleTemplateDisplayStyle.Custom => custom,
|
||||
_ => BattleTemplateConfig.Showdown,
|
||||
};
|
||||
|
||||
private static MoveDisplayStyle GetMoveDisplayStyle(MoveDisplayStyle style, EntityContext context) => style switch
|
||||
{
|
||||
//MoveDisplayStyle.Directional when context is EntityContext.Gen9a => MoveDisplayStyle.Directional, TODO ZA
|
||||
_ => MoveDisplayStyle.Fill,
|
||||
};
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ public interface IBattleTemplate : ISpeciesForm, IGigantamaxReadOnly, IDynamaxLe
|
|||
/// <summary>
|
||||
/// <see cref="PKM.HeldItem"/> of the Set entity.
|
||||
/// </summary>
|
||||
/// <remarks>Depends on <see cref="Context"/> for context-specific item lists.</remarks>
|
||||
int HeldItem { get; }
|
||||
|
||||
/// <summary>
|
||||
17
PKHeX.Core/Editing/BattleTemplate/MoveDisplayStyle.cs
Normal file
17
PKHeX.Core/Editing/BattleTemplate/MoveDisplayStyle.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Style to display moves.
|
||||
/// </summary>
|
||||
public enum MoveDisplayStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves are slots 1-4, with no empty slots, and correspond to the rectangular grid without empty spaces.
|
||||
/// </summary>
|
||||
Fill,
|
||||
|
||||
/// <summary>
|
||||
/// Move slots are assigned to the directional pad, and unused directional slots are not displayed.
|
||||
/// </summary>
|
||||
Directional,
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for retrieving teams from URLs.
|
||||
/// </summary>
|
||||
public static class BattleTemplateTeams
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to check if the input text is a valid URL for a team, and if so, retrieves the team data.
|
||||
/// </summary>
|
||||
/// <param name="text">The input text to check.</param>
|
||||
/// <param name="content">When the method returns, contains the retrieved team data if the text is a valid URL; otherwise, null.</param>
|
||||
/// <returns><c>true</c> if the text is a valid URL and the team data was successfully retrieved; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryGetSetLines(string text, [NotNullWhen(true)] out string? content)
|
||||
{
|
||||
if (ShowdownTeam.IsURL(text, out var url))
|
||||
return ShowdownTeam.TryGetSets(url, out content);
|
||||
if (PokepasteTeam.IsURL(text, out url))
|
||||
return PokepasteTeam.TryGetSets(url, out content);
|
||||
content = text;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve sets from the provided text. If the text is a valid URL, it retrieves the team data from the URL.
|
||||
/// </summary>
|
||||
/// <param name="text">The input text to check.</param>
|
||||
/// <returns>An enumerable collection of <see cref="ShowdownSet"/> objects representing the sets.</returns>
|
||||
public static IEnumerable<ShowdownSet> TryGetSets(string text)
|
||||
{
|
||||
var ingest = TryGetSetLines(text, out var many) ? many : text;
|
||||
return ShowdownParsing.GetShowdownSets(ingest);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,12 @@
|
|||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for retrieving Showdown teams from URLs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://pokepast.es/"/>
|
||||
/// </remarks>
|
||||
public static class PokepasteTeam
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -12,8 +18,17 @@ public static class PokepasteTeam
|
|||
/// <param name="team">The numeric identifier of the team.</param>
|
||||
/// <returns>A string containing the full URL to access the team data.</returns>
|
||||
public static string GetURL(ulong team) => $"https://pokepast.es/{team:x16}/raw";
|
||||
|
||||
/// <inheritdoc cref="GetURL"/>
|
||||
/// <remarks>For legacy team indexes (first 255 or so), shouldn't ever be triggered non-test team indexes.</remarks>
|
||||
public static string GetURLOld(int team) => $"https://pokepast.es/{team}/raw";
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the Showdown team data from a specified URL, and reformats it.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to retrieve the team data from.</param>
|
||||
/// <param name="content">When the method returns, contains the processed team data if retrieval and formatting succeed; otherwise, null.</param>
|
||||
/// <returns><c>true</c> if the team data is successfully retrieved and reformatted; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryGetSets(string url, [NotNullWhen(true)] out string? content)
|
||||
{
|
||||
content = null;
|
||||
|
|
@ -29,7 +44,6 @@ public static bool TryGetSets(string url, [NotNullWhen(true)] out string? conten
|
|||
/// </summary>
|
||||
/// <param name="text">The text to evaluate.</param>
|
||||
/// <param name="url">When the method returns, contains the normalized API URL if the text represents a valid Showdown team URL; otherwise, null.</param>
|
||||
/// <param name="hash"></param>
|
||||
/// <returns><c>true</c> if the text is a valid Showdown team URL; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsURL(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
|
||||
{
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using static PKHeX.Core.Species;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
|
@ -11,6 +12,9 @@ public static class ShowdownParsing
|
|||
{
|
||||
private static readonly string[] genderForms = ["", "F", ""];
|
||||
|
||||
/// <inheritdoc cref="ShowdownSet.DefaultListAllocation"/>
|
||||
private const int DefaultListAllocation = ShowdownSet.DefaultListAllocation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Form ID from the input <see cref="name"/>.
|
||||
/// </summary>
|
||||
|
|
@ -147,12 +151,13 @@ public static string SetShowdownFormName(ushort species, string form, int abilit
|
|||
/// Fetches <see cref="ShowdownSet"/> data from the input <see cref="lines"/>.
|
||||
/// </summary>
|
||||
/// <param name="lines">Raw lines containing numerous multi-line set data.</param>
|
||||
/// <param name="localization">Localization data for the set.</param>
|
||||
/// <returns><see cref="ShowdownSet"/> objects until <see cref="lines"/> is consumed.</returns>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines)
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines, BattleTemplateLocalization localization)
|
||||
{
|
||||
// exported sets always have >4 moves; new List will always require 1 resizing, allocate 2x to save 1 reallocation.
|
||||
// intro, nature, ability, (ivs, evs, shiny, level) 4*moves
|
||||
var setLines = new List<string>(8);
|
||||
var setLines = new List<string>(DefaultListAllocation);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
|
|
@ -162,14 +167,54 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines
|
|||
}
|
||||
if (setLines.Count == 0)
|
||||
continue;
|
||||
yield return new ShowdownSet(setLines);
|
||||
yield return new ShowdownSet(setLines, localization);
|
||||
setLines.Clear();
|
||||
}
|
||||
if (setLines.Count != 0)
|
||||
yield return new ShowdownSet(setLines);
|
||||
yield return new ShowdownSet(setLines, localization);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string})"/>
|
||||
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines)
|
||||
{
|
||||
var setLines = new List<string>(DefaultListAllocation);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
setLines.Add(line);
|
||||
continue;
|
||||
}
|
||||
if (setLines.Count == 0)
|
||||
continue;
|
||||
yield return TryParseAnyLanguage(setLines, out var set) ? set : new ShowdownSet(setLines);
|
||||
setLines.Clear();
|
||||
}
|
||||
if (setLines.Count != 0)
|
||||
yield return TryParseAnyLanguage(setLines, out var set) ? set : new ShowdownSet(setLines);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text, BattleTemplateLocalization localization)
|
||||
{
|
||||
int start = 0;
|
||||
do
|
||||
{
|
||||
var span = text.Span;
|
||||
var slice = span[start..];
|
||||
var set = GetShowdownSet(slice, localization, out int length);
|
||||
if (set.Species == 0)
|
||||
break;
|
||||
yield return set;
|
||||
start += length;
|
||||
}
|
||||
while (start < text.Length);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
|
||||
/// <summary>
|
||||
/// Language-unknown version of <see cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>.
|
||||
/// </summary>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text)
|
||||
{
|
||||
int start = 0;
|
||||
|
|
@ -186,17 +231,15 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text
|
|||
while (start < text.Length);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char})"/>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(string text) => GetShowdownSets(text.AsMemory());
|
||||
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char},BattleTemplateLocalization)"/>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(string text, BattleTemplateLocalization localization) => GetShowdownSets(text.AsMemory(), localization);
|
||||
|
||||
private static int GetLength(ReadOnlySpan<char> text)
|
||||
{
|
||||
// Find the end of the Showdown Set lines.
|
||||
// The end is implied when:
|
||||
// - we see a complete whitespace or empty line, or
|
||||
// - we witness four 'move' definition lines.
|
||||
// - we see a complete whitespace or empty line
|
||||
int length = 0;
|
||||
int moveCount = 4;
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
|
@ -208,47 +251,75 @@ private static int GetLength(ReadOnlySpan<char> text)
|
|||
var used = newline + 1;
|
||||
length += used;
|
||||
|
||||
if (slice.IsEmpty || slice.IsWhiteSpace())
|
||||
return length;
|
||||
if (slice.TrimStart()[0] is '-' or '–' && --moveCount == 0)
|
||||
if (slice.IsWhiteSpace())
|
||||
return length;
|
||||
text = text[used..];
|
||||
}
|
||||
}
|
||||
|
||||
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, out int length)
|
||||
/// <summary>
|
||||
/// Attempts to parse the input <see cref="text"/> into a <see cref="ShowdownSet"/> object.
|
||||
/// </summary>
|
||||
/// <param name="text">Input string to parse.</param>
|
||||
/// <param name="localization">Input localization to use.</param>
|
||||
/// <param name="length">Amount of characters consumed from the input string.</param>
|
||||
/// <returns>Parsed <see cref="ShowdownSet"/> object if successful, otherwise might be a best-match with some/all unparsed lines.</returns>
|
||||
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, BattleTemplateLocalization localization, out int length)
|
||||
{
|
||||
length = GetLength(text);
|
||||
var slice = text[..length];
|
||||
var set = new ShowdownSet(slice);
|
||||
var set = new ShowdownSet(slice, localization);
|
||||
while (length < text.Length && text[length] is '\r' or '\n' or ' ')
|
||||
length++;
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSet(ReadOnlySpan{char},BattleTemplateLocalization,out int)"/>
|
||||
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, out int length)
|
||||
{
|
||||
length = GetLength(text);
|
||||
var slice = text[..length];
|
||||
if (!TryParseAnyLanguage(slice, out var set))
|
||||
set = new ShowdownSet(slice); // should never fall back
|
||||
while (length < text.Length && text[length] is '\r' or '\n' or ' ')
|
||||
length++;
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char},BattleTemplateLocalization)"/>
|
||||
public static IEnumerable<ShowdownSet> GetShowdownSets(string text) => GetShowdownSets(text.AsMemory());
|
||||
|
||||
/// <inheritdoc cref="GetShowdownText(PKM, in BattleTemplateExportSettings)"/>
|
||||
public static string GetShowdownText(PKM pk) => GetShowdownText(pk, BattleTemplateExportSettings.Showdown);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the <see cref="PKM"/> data into an importable set format for Pokémon Showdown.
|
||||
/// </summary>
|
||||
/// <param name="pk">PKM to convert to string</param>
|
||||
/// <param name="settings">Import localization/style setting</param>
|
||||
/// <returns>Multi line set data</returns>
|
||||
public static string GetShowdownText(PKM pk)
|
||||
public static string GetShowdownText(PKM pk, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
if (pk.Species == 0)
|
||||
return string.Empty;
|
||||
return new ShowdownSet(pk).Text;
|
||||
var set = new ShowdownSet(pk);
|
||||
set.InterpretAsPreview(pk);
|
||||
return set.GetText(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches ShowdownSet lines from the input <see cref="PKM"/> data.
|
||||
/// </summary>
|
||||
/// <param name="data">Pokémon data to summarize.</param>
|
||||
/// <param name="lang">Localization setting</param>
|
||||
/// <param name="settings">Export localization/style setting</param>
|
||||
/// <returns>Consumable list of <see cref="ShowdownSet.Text"/> lines.</returns>
|
||||
public static IEnumerable<string> GetShowdownText(IEnumerable<PKM> data, string lang = ShowdownSet.DefaultLanguage)
|
||||
public static IEnumerable<string> GetShowdownText(IEnumerable<PKM> data, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
List<string> result = new();
|
||||
var sets = GetShowdownSets(data);
|
||||
foreach (var set in sets)
|
||||
yield return set.LocalizedText(lang);
|
||||
result.Add(set.GetText(settings));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -266,24 +337,123 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<PKM> data)
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
|
||||
public static string GetShowdownSets(IEnumerable<PKM> data, string separator) => string.Join(separator, GetShowdownText(data, BattleTemplateExportSettings.Showdown));
|
||||
|
||||
/// <summary>
|
||||
/// Fetches ShowdownSet lines from the input <see cref="PKM"/> data, and combines it into one string.
|
||||
/// </summary>
|
||||
/// <param name="data">Pokémon data to summarize.</param>
|
||||
/// <param name="separator">Splitter between each set.</param>
|
||||
/// <param name="settings">Import localization/style setting</param>
|
||||
/// <returns>Single string containing all <see cref="ShowdownSet.Text"/> lines.</returns>
|
||||
public static string GetShowdownSets(IEnumerable<PKM> data, string separator) => string.Join(separator, GetShowdownText(data));
|
||||
public static string GetShowdownSets(IEnumerable<PKM> data, string separator, in BattleTemplateExportSettings settings) => string.Join(separator, GetShowdownText(data, settings));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a localized string preview of the provided <see cref="pk"/>.
|
||||
/// </summary>
|
||||
/// <param name="pk">Pokémon data</param>
|
||||
/// <param name="language">Language code</param>
|
||||
/// <param name="settings">Export settings</param>
|
||||
/// <returns>Multi-line string</returns>
|
||||
public static string GetLocalizedPreviewText(PKM pk, string language)
|
||||
public static string GetLocalizedPreviewText(PKM pk, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
var set = new ShowdownSet(pk);
|
||||
set.InterpretAsPreview(pk);
|
||||
return set.LocalizedText(language);
|
||||
return set.GetText(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the input string into a <see cref="ShowdownSet"/> object.
|
||||
/// </summary>
|
||||
/// <param name="message">Input string to parse.</param>
|
||||
/// <param name="set">Parsed <see cref="ShowdownSet"/> object if successful, otherwise might be a best-match with some unparsed lines.</param>
|
||||
/// <returns>True if the input was parsed successfully, false otherwise.</returns>
|
||||
public static bool TryParseAnyLanguage(ReadOnlySpan<char> message, [NotNullWhen(true)] out ShowdownSet? set)
|
||||
{
|
||||
set = null;
|
||||
if (message.Length == 0)
|
||||
return false;
|
||||
|
||||
var invalid = int.MaxValue;
|
||||
var all = BattleTemplateLocalization.GetAll();
|
||||
foreach (var lang in all)
|
||||
{
|
||||
var local = lang.Value;
|
||||
var tmp = new ShowdownSet(message, local);
|
||||
var bad = tmp.InvalidLines.Count;
|
||||
if (bad == 0)
|
||||
{
|
||||
set = tmp;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for invalid lines
|
||||
if (bad >= invalid)
|
||||
continue;
|
||||
|
||||
// Best so far.
|
||||
invalid = bad;
|
||||
set = tmp;
|
||||
}
|
||||
if (set is null)
|
||||
return false;
|
||||
return set.Species != 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryParseAnyLanguage(ReadOnlySpan{char}, out ShowdownSet?)"/>
|
||||
public static bool TryParseAnyLanguage(IReadOnlyList<string> setLines, [NotNullWhen(true)] out ShowdownSet? set)
|
||||
{
|
||||
set = null;
|
||||
if (setLines.Count == 0)
|
||||
return false;
|
||||
|
||||
var invalid = int.MaxValue;
|
||||
var all = BattleTemplateLocalization.GetAll();
|
||||
foreach (var lang in all)
|
||||
{
|
||||
var local = lang.Value;
|
||||
var tmp = new ShowdownSet(setLines, local);
|
||||
var bad = tmp.InvalidLines.Count;
|
||||
if (bad == 0)
|
||||
{
|
||||
set = tmp;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for invalid lines
|
||||
if (bad >= invalid)
|
||||
continue;
|
||||
|
||||
// Best so far.
|
||||
invalid = bad;
|
||||
set = tmp;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to translate the input battle template <see cref="message"/> into a localized string.
|
||||
/// </summary>
|
||||
/// <param name="message">Input string to parse.</param>
|
||||
/// <param name="outputSettings">Export settings</param>
|
||||
/// <param name="translated">Translated string if successful.</param>
|
||||
/// <returns><c>true</c> if the input was translated successfully, <c>false</c> otherwise.</returns>
|
||||
public static bool TryTranslate(ReadOnlySpan<char> message, BattleTemplateExportSettings outputSettings, [NotNullWhen(true)] out string? translated)
|
||||
{
|
||||
translated = null;
|
||||
if (!TryParseAnyLanguage(message, out var set))
|
||||
return false;
|
||||
translated = set.GetText(outputSettings);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryTranslate(ReadOnlySpan{char}, BattleTemplateExportSettings, out string?)"/>
|
||||
public static bool TryTranslate(IReadOnlyList<string> message, BattleTemplateExportSettings outputSettings, [NotNullWhen(true)] out string? translated)
|
||||
{
|
||||
translated = null;
|
||||
if (!TryParseAnyLanguage(message, out var set))
|
||||
return false;
|
||||
translated = set.GetText(outputSettings);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1050
PKHeX.Core/Editing/BattleTemplate/Showdown/ShowdownSet.cs
Normal file
1050
PKHeX.Core/Editing/BattleTemplate/Showdown/ShowdownSet.cs
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -7,6 +7,9 @@ namespace PKHeX.Core;
|
|||
/// <summary>
|
||||
/// Logic for retrieving Showdown teams from URLs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://play.pokemonshowdown.com/"/>
|
||||
/// </remarks>
|
||||
public static class ShowdownTeam
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -82,7 +85,9 @@ public static bool IsURL(ReadOnlySpan<char> text, [NotNullWhen(true)] out string
|
|||
text = text.Trim();
|
||||
if (text.StartsWith("https://psim.us/t/") || // short link
|
||||
text.StartsWith("https://teams.pokemonshowdown.com/"))
|
||||
{
|
||||
return TryCheckWeb(text, out url);
|
||||
}
|
||||
|
||||
if (text.StartsWith("https://play.pokemonshowdown.com/api/getteam?teamid="))
|
||||
return TryCheckAPI(text, out url);
|
||||
338
PKHeX.Core/Editing/BattleTemplate/StatDisplayConfig.cs
Normal file
338
PKHeX.Core/Editing/BattleTemplate/StatDisplayConfig.cs
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for displaying stats.
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
public sealed class StatDisplayConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Stat names are displayed without localization; H:X A:X B:X C:X D:X S:X
|
||||
/// </summary>
|
||||
public static readonly StatDisplayConfig HABCDS = new()
|
||||
{
|
||||
Names = ["H", "A", "B", "C", "D", "S"],
|
||||
Separator = " ",
|
||||
ValueGap = ":",
|
||||
IsLeft = true,
|
||||
AlwaysShow = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed without localization; X/X/X/X/X/X
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Same as <see cref="Raw00"/> but with no leading zeroes.
|
||||
/// </remarks>
|
||||
public static readonly StatDisplayConfig Raw = new()
|
||||
{
|
||||
Names = [],
|
||||
Separator = "/",
|
||||
ValueGap = "",
|
||||
AlwaysShow = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed without localization; XX/XX/XX/XX/XX/XX
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Same as <see cref="Raw"/> but with 2 digits (leading zeroes).
|
||||
/// </remarks>
|
||||
public static readonly StatDisplayConfig Raw00 = new()
|
||||
{
|
||||
Names = [],
|
||||
Separator = "/",
|
||||
ValueGap = "",
|
||||
AlwaysShow = true,
|
||||
MinimumDigits = 2,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of stat display styles that are commonly used and not specific to a localization.
|
||||
/// </summary>
|
||||
public static List<StatDisplayConfig> Custom { get; } = [HABCDS, Raw]; // Raw00 parses equivalent to Raw
|
||||
|
||||
/// <summary>List of stat names to display</summary>
|
||||
public required string[] Names { get; init; }
|
||||
|
||||
/// <summary>Separator between each stat+value declaration</summary>
|
||||
public string Separator { get; init; } = " / ";
|
||||
|
||||
/// <summary>Separator between the stat name and value</summary>
|
||||
public string ValueGap { get; init; } = " ";
|
||||
|
||||
/// <summary><c>true</c> if the text is displayed on the left side of the value</summary>
|
||||
public bool IsLeft { get; init; }
|
||||
|
||||
/// <summary><c>true</c> if the stat is always shown, even if the value is default</summary>
|
||||
public bool AlwaysShow { get; init; }
|
||||
|
||||
/// <summary>Minimum number of digits to show for the stat value.</summary>
|
||||
public int MinimumDigits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the displayed stat name (in visual order) via a case-insensitive search.
|
||||
/// </summary>
|
||||
/// <param name="stat">Stat name, trimmed.</param>
|
||||
/// <returns>-1 if not found, otherwise the index of the stat name.</returns>
|
||||
public int GetStatIndex(ReadOnlySpan<char> stat)
|
||||
{
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
if (stat.Equals(Names[i], StringComparison.OrdinalIgnoreCase))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override string ToString() => string.Join(Separator, Names);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a stat value into a string builder.
|
||||
/// </summary>
|
||||
/// <param name="sb">Result string builder</param>
|
||||
/// <param name="statIndex">Display index of the stat</param>
|
||||
/// <param name="statValue">Stat value</param>
|
||||
/// <param name="valueSuffix">Optional suffix for the value, to display a stat amplification request</param>
|
||||
/// <param name="skipValue"><c>true</c> to skip the value, only displaying the stat name and amplification (if provided)</param>
|
||||
public void Format<T>(StringBuilder sb, int statIndex, T statValue, ReadOnlySpan<char> valueSuffix = default, bool skipValue = false)
|
||||
{
|
||||
var statName = statIndex < Names.Length ? Names[statIndex] : "";
|
||||
var length = GetStatSize(statName, statValue, valueSuffix, skipValue);
|
||||
|
||||
if (sb.Length + length > sb.Capacity)
|
||||
sb.EnsureCapacity(sb.Length + length);
|
||||
Append(sb, statName, statValue, valueSuffix, skipValue);
|
||||
}
|
||||
|
||||
private void Append<T>(StringBuilder sb, ReadOnlySpan<char> statName, T statValue, ReadOnlySpan<char> valueSuffix, bool skipValue)
|
||||
{
|
||||
int start = sb.Length;
|
||||
|
||||
if (!skipValue)
|
||||
{
|
||||
sb.Append(statValue);
|
||||
var end = sb.Length;
|
||||
if (end < MinimumDigits)
|
||||
sb.Insert(start, "0", MinimumDigits - end);
|
||||
}
|
||||
sb.Append(valueSuffix);
|
||||
|
||||
if (IsLeft)
|
||||
{
|
||||
sb.Insert(start, ValueGap);
|
||||
sb.Insert(start, statName);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(ValueGap);
|
||||
sb.Append(statName);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetStatSize<T>(ReadOnlySpan<char> statName, T statValue, ReadOnlySpan<char> valueSuffix, bool skipValue)
|
||||
{
|
||||
var length = statName.Length + ValueGap.Length + valueSuffix.Length;
|
||||
if (!skipValue)
|
||||
length += (int)Math.Max(MinimumDigits, Math.Floor(Math.Log10(Convert.ToDouble(statValue)) + 1));
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the separator character used for parsing.
|
||||
/// </summary>
|
||||
private char GetSeparatorParse() => GetSeparatorParse(Separator);
|
||||
|
||||
private static char GetSeparatorParse(ReadOnlySpan<char> sep) => sep.Length switch
|
||||
{
|
||||
0 => ' ',
|
||||
1 => sep[0],
|
||||
_ => sep.Trim()[0]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Imports a list of stats from a string.
|
||||
/// </summary>
|
||||
/// <param name="message">Input string</param>
|
||||
/// <param name="result">Result storage</param>
|
||||
/// <returns>Parse result</returns>
|
||||
public StatParseResult TryParse(ReadOnlySpan<char> message, Span<int> result)
|
||||
{
|
||||
var separator = GetSeparatorParse();
|
||||
var gap = ValueGap.AsSpan().Trim();
|
||||
// If stats are not labeled, parse with the straightforward parser.
|
||||
if (Names.Length == 0)
|
||||
return TryParseRaw(message, result, separator);
|
||||
else if (IsLeft)
|
||||
return TryParseIsLeft(message, result, separator, gap);
|
||||
else
|
||||
return TryParseRight(message, result, separator, gap);
|
||||
}
|
||||
|
||||
private StatParseResult TryParseIsLeft(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
|
||||
{
|
||||
var rec = new StatParseResult();
|
||||
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
if (message.Length == 0)
|
||||
break;
|
||||
|
||||
var statName = Names[i];
|
||||
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
continue;
|
||||
|
||||
if (index != 0)
|
||||
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
|
||||
|
||||
message = message[statName.Length..].TrimStart();
|
||||
if (valueGap.Length > 0 && message.StartsWith(valueGap))
|
||||
message = message[valueGap.Length..].TrimStart();
|
||||
|
||||
var value = message;
|
||||
|
||||
var indexSeparator = value.IndexOf(separator);
|
||||
if (indexSeparator != -1)
|
||||
value = value[..indexSeparator].Trim();
|
||||
else
|
||||
message = default; // everything remaining belongs in the value we are going to parse.
|
||||
|
||||
if (value.Length != 0)
|
||||
{
|
||||
var amped = TryPeekAmp(ref value, ref rec, i);
|
||||
if (amped && value.Length == 0)
|
||||
rec.MarkParsed(index);
|
||||
else
|
||||
TryParse(result, ref rec, value, i);
|
||||
}
|
||||
|
||||
if (indexSeparator != -1)
|
||||
message = message[(indexSeparator+1)..].TrimStart();
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
|
||||
rec.MarkDirty();
|
||||
rec.FinishParse(Names.Length);
|
||||
return rec;
|
||||
}
|
||||
|
||||
private StatParseResult TryParseRight(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
|
||||
{
|
||||
var rec = new StatParseResult();
|
||||
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
if (message.Length == 0)
|
||||
break;
|
||||
|
||||
var statName = Names[i];
|
||||
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
continue;
|
||||
|
||||
var value = message[..index].Trim();
|
||||
var indexSeparator = value.LastIndexOf(separator);
|
||||
if (indexSeparator != -1)
|
||||
{
|
||||
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
|
||||
value = value[(indexSeparator + 1)..].TrimStart();
|
||||
}
|
||||
|
||||
if (valueGap.Length > 0 && value.EndsWith(valueGap))
|
||||
value = value[..^valueGap.Length];
|
||||
|
||||
if (value.Length != 0)
|
||||
{
|
||||
var amped = TryPeekAmp(ref value, ref rec, i);
|
||||
if (amped && value.Length == 0)
|
||||
rec.MarkParsed(index);
|
||||
else
|
||||
TryParse(result, ref rec, value, i);
|
||||
}
|
||||
|
||||
message = message[(index + statName.Length)..].TrimStart();
|
||||
if (message.StartsWith(separator))
|
||||
message = message[1..].TrimStart();
|
||||
}
|
||||
|
||||
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
|
||||
rec.MarkDirty();
|
||||
rec.FinishParse(Names.Length);
|
||||
return rec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a raw stat string.
|
||||
/// </summary>
|
||||
/// <param name="message">Input string</param>
|
||||
/// <param name="result">Output storage</param>
|
||||
/// <param name="separator">Separator character</param>
|
||||
public static StatParseResult TryParseRaw(ReadOnlySpan<char> message, Span<int> result, char separator)
|
||||
{
|
||||
var rec = new StatParseResult();
|
||||
|
||||
// Expect the message to contain all entries of `result` separated by the separator and an arbitrary amount of spaces permitted.
|
||||
// The message is split by the separator, and each part is trimmed of whitespace.
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
var index = message.IndexOf(separator);
|
||||
|
||||
var value = index != -1 ? message[..index].Trim() : message.Trim();
|
||||
message = message[(index+1)..].TrimStart();
|
||||
|
||||
if (value.Length == 0)
|
||||
{
|
||||
rec.MarkDirty(); // Something is wrong with the message, as we have an empty stat.
|
||||
continue; // Maybe it's a duplicate separator; keep parsing and hope that the required amount are parsed.
|
||||
}
|
||||
|
||||
var amped = TryPeekAmp(ref value, ref rec, i);
|
||||
if (amped && value.Length == 0)
|
||||
rec.MarkParsed(index);
|
||||
else
|
||||
TryParse(result, ref rec, value, i);
|
||||
}
|
||||
|
||||
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
|
||||
rec.MarkDirty();
|
||||
rec.FinishParseOnly(result.Length);
|
||||
return rec;
|
||||
}
|
||||
|
||||
private static void TryParse(Span<int> result, ref StatParseResult rec, ReadOnlySpan<char> value, int statIndex)
|
||||
{
|
||||
if (!int.TryParse(value, out var stat) || stat < 0)
|
||||
{
|
||||
rec.MarkDirty();
|
||||
return;
|
||||
}
|
||||
result[statIndex] = stat;
|
||||
rec.MarkParsed(statIndex);
|
||||
}
|
||||
|
||||
private static bool TryPeekAmp(ref ReadOnlySpan<char> value, ref StatParseResult rec, int statIndex)
|
||||
{
|
||||
var last = value[^1];
|
||||
if (last == '+')
|
||||
{
|
||||
rec.Plus = (sbyte)statIndex;
|
||||
value = value[..^1].TrimEnd();
|
||||
return true;
|
||||
}
|
||||
if (last == '-')
|
||||
{
|
||||
rec.Minus = (sbyte)statIndex;
|
||||
value = value[..^1].TrimEnd();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
37
PKHeX.Core/Editing/BattleTemplate/StatDisplayStyle.cs
Normal file
37
PKHeX.Core/Editing/BattleTemplate/StatDisplayStyle.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Style to display stat names.
|
||||
/// </summary>
|
||||
public enum StatDisplayStyle : sbyte
|
||||
{
|
||||
Custom = -1,
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed in abbreviated (2-3 characters) localized text.
|
||||
/// </summary>
|
||||
Abbreviated,
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed in full localized text.
|
||||
/// </summary>
|
||||
Full,
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed as a single character.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the typical format used by the Japanese community; HABCDS.
|
||||
/// </remarks>
|
||||
HABCDS,
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed without localization; X/X/X/X/X/X
|
||||
/// </summary>
|
||||
Raw,
|
||||
|
||||
/// <summary>
|
||||
/// Stat names are displayed without localization; XX/XX/XX/XX/XX/XX
|
||||
/// </summary>
|
||||
Raw00,
|
||||
}
|
||||
128
PKHeX.Core/Editing/BattleTemplate/StatParseResult.cs
Normal file
128
PKHeX.Core/Editing/BattleTemplate/StatParseResult.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
using System;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Value result object of parsing a stat string.
|
||||
/// </summary>
|
||||
public record struct StatParseResult()
|
||||
{
|
||||
private const uint MaxStatCount = 6; // Number of stats in the game
|
||||
private const sbyte NoStatAmp = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Count of parsed stats.
|
||||
/// </summary>
|
||||
public byte CountParsed { get; private set; } = 0; // could potentially make this a computed value (popcnt), but it's not worth it
|
||||
|
||||
/// <summary>
|
||||
/// Indexes of parsed stats.
|
||||
/// </summary>
|
||||
public byte IndexesParsed { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Stat index of increased stat.
|
||||
/// </summary>
|
||||
public sbyte Plus { get; set; } = NoStatAmp;
|
||||
|
||||
/// <summary>
|
||||
/// Stat index of decreased stat.
|
||||
/// </summary>
|
||||
public sbyte Minus { get; set; } = NoStatAmp;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the parsing was clean (no un-parsed text).
|
||||
/// </summary>
|
||||
public bool IsParseClean { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if all stat indexes available were parsed.
|
||||
/// </summary>
|
||||
public bool IsParsedAllStats { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the stat index as parsed, and updates the count of parsed stats.
|
||||
/// </summary>
|
||||
/// <param name="statIndex">Visual index of the stat to mark as parsed.</param>
|
||||
/// <returns>True if the stat had not been parsed before, false if it was already parsed.</returns>
|
||||
public bool MarkParsed(int statIndex)
|
||||
{
|
||||
// Check if the stat index is valid (0-5)
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)statIndex, MaxStatCount);
|
||||
if (WasParsed(statIndex))
|
||||
return false;
|
||||
// Mark the stat index as parsed
|
||||
IndexesParsed |= (byte)(1 << statIndex);
|
||||
++CountParsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the stat index was parsed.
|
||||
/// </summary>
|
||||
/// <param name="statIndex">Visual index of the stat to check.</param>
|
||||
/// <returns>True if the stat was parsed, false otherwise.</returns>
|
||||
public bool WasParsed(int statIndex)
|
||||
{
|
||||
// Check if the stat index is valid (0-5)
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)statIndex, MaxStatCount);
|
||||
return (IndexesParsed & (1 << statIndex)) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the parsing as finished, and updates the internal state to indicate if all stats were parsed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used when not all stats are required to be parsed.
|
||||
/// </remarks>
|
||||
/// <param name="expect"></param>
|
||||
public void FinishParse(int expect)
|
||||
{
|
||||
if (CountParsed == 0 && !HasAmps)
|
||||
MarkDirty();
|
||||
IsParsedAllStats = CountParsed == expect || IsParseClean;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the parsing as finished, and updates the internal state to indicate if all stats were parsed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used when a specific number of stats is expected.
|
||||
/// </remarks>
|
||||
/// <param name="expect"></param>
|
||||
public void FinishParseOnly(int expect) => IsParsedAllStats = CountParsed == expect;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the parsing as dirty, indicating that the string was not a clean input string (user modified or the syntax doesn't match the spec).
|
||||
/// </summary>
|
||||
public void MarkDirty() => IsParseClean = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if any stat has any amplified (+/-) requested, indicative of nature.
|
||||
/// </summary>
|
||||
public bool HasAmps => Plus != NoStatAmp || Minus != NoStatAmp;
|
||||
|
||||
/// <summary>
|
||||
/// Reorders the speed stat to be in the middle of the stats.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Speed is visually represented as the last stat in the list, but it is actually the 3rd stat stored.
|
||||
/// </remarks>
|
||||
public void TreatAmpsAsSpeedNotLast()
|
||||
{
|
||||
Plus = GetSpeedMiddleIndex(Plus);
|
||||
Minus = GetSpeedMiddleIndex(Minus);
|
||||
}
|
||||
|
||||
// Move speed from index 5 to index 3, and the other stats down to account for HP not being boosted.
|
||||
private static sbyte GetSpeedMiddleIndex(sbyte amp) => amp switch
|
||||
{
|
||||
0 => -1,
|
||||
1 => 0, // Atk
|
||||
2 => 1, // Def
|
||||
3 => 3, // SpA
|
||||
4 => 4, // SpD
|
||||
5 => 2, // Spe
|
||||
_ => amp,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,784 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using static PKHeX.Core.Species;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for exporting and importing <see cref="PKM"/> data in Pokémon Showdown's text format.
|
||||
/// </summary>
|
||||
public sealed class ShowdownSet : IBattleTemplate
|
||||
{
|
||||
private static readonly string[] StatNames = ["HP", "Atk", "Def", "Spe", "SpA", "SpD"];
|
||||
private const string LineSplit = ": ";
|
||||
private const string ItemSplit = " @ ";
|
||||
private const int MAX_SPECIES = (int)MAX_COUNT - 1;
|
||||
internal const string DefaultLanguage = GameLanguage.DefaultLanguage;
|
||||
private static readonly GameStrings DefaultStrings = GameInfo.GetStrings(DefaultLanguage);
|
||||
|
||||
private static ReadOnlySpan<ushort> DashedSpecies =>
|
||||
[
|
||||
(int)NidoranF, (int)NidoranM,
|
||||
(int)HoOh,
|
||||
(int)Jangmoo, (int)Hakamoo, (int)Kommoo,
|
||||
(int)TingLu, (int)ChienPao, (int)WoChien, (int)ChiYu,
|
||||
];
|
||||
|
||||
public ushort Species { get; private set; }
|
||||
public EntityContext Context { get; private set; } = RecentTrainerCache.Context;
|
||||
public string Nickname { get; private set; } = string.Empty;
|
||||
public byte? Gender { get; private set; }
|
||||
public int HeldItem { get; private set; }
|
||||
public int Ability { get; private set; } = -1;
|
||||
public byte Level { get; private set; } = 100;
|
||||
public bool Shiny { get; private set; }
|
||||
public byte Friendship { get; private set; } = 255;
|
||||
public Nature Nature { get; private set; } = Nature.Random;
|
||||
public string FormName { get; private set; } = string.Empty;
|
||||
public byte Form { get; private set; }
|
||||
public int[] EVs { get; } = [00, 00, 00, 00, 00, 00];
|
||||
public int[] IVs { get; } = [31, 31, 31, 31, 31, 31];
|
||||
public sbyte HiddenPowerType { get; private set; } = -1;
|
||||
public MoveType TeraType { get; private set; } = MoveType.Any;
|
||||
public ushort[] Moves { get; } = [0, 0, 0, 0];
|
||||
public bool CanGigantamax { get; private set; }
|
||||
public byte DynamaxLevel { get; private set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Any lines that failed to be parsed.
|
||||
/// </summary>
|
||||
public readonly List<string> InvalidLines = new(0);
|
||||
|
||||
private GameStrings Strings { get; set; } = DefaultStrings;
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new <see cref="ShowdownSet"/> from the input string.
|
||||
/// </summary>
|
||||
/// <param name="input">Single-line string which will be split before loading.</param>
|
||||
public ShowdownSet(ReadOnlySpan<char> input) => LoadLines(input.EnumerateLines());
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new <see cref="ShowdownSet"/> from the input string.
|
||||
/// </summary>
|
||||
/// <param name="lines">Enumerable list of lines.</param>
|
||||
public ShowdownSet(IEnumerable<string> lines) => LoadLines(lines);
|
||||
|
||||
private void LoadLines(SpanLineEnumerator lines)
|
||||
{
|
||||
ParseLines(lines);
|
||||
SanitizeResult();
|
||||
}
|
||||
|
||||
private void LoadLines(IEnumerable<string> lines)
|
||||
{
|
||||
ParseLines(lines);
|
||||
SanitizeResult();
|
||||
}
|
||||
|
||||
private void SanitizeResult()
|
||||
{
|
||||
FormName = ShowdownParsing.SetShowdownFormName(Species, FormName, Ability);
|
||||
Form = ShowdownParsing.GetFormFromString(FormName, Strings, Species, Context);
|
||||
|
||||
// Handle edge case with fixed-gender forms.
|
||||
if (Species is (int)Meowstic or (int)Indeedee or (int)Basculegion or (int)Oinkologne)
|
||||
ReviseGenderedForms();
|
||||
}
|
||||
|
||||
private void ReviseGenderedForms()
|
||||
{
|
||||
if (Gender == 1) // Recognized with (F)
|
||||
{
|
||||
FormName = "F";
|
||||
Form = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
FormName = Form == 1 ? "F" : "M";
|
||||
Gender = Form;
|
||||
}
|
||||
}
|
||||
|
||||
private const int MaxMoveCount = 4;
|
||||
|
||||
// Skip lines that are too short or too long.
|
||||
// Longest line is ~74 (Gen2 EVs)
|
||||
// Length permitted: 3-80
|
||||
// The shortest Pokémon name in Japanese is "ニ" (Ni) which is the name for the Pokémon, Nidoran♂ (male Nidoran). It has only one letter.
|
||||
// We will handle this 1-2 letter edge case only if the line is the first line of the set, in the rare chance we are importing for a non-English language?
|
||||
private const int MinLength = 3;
|
||||
private const int MaxLength = 80;
|
||||
private static bool IsLengthOutOfRange(ReadOnlySpan<char> trim) => IsLengthOutOfRange(trim.Length);
|
||||
private static bool IsLengthOutOfRange(int length) => (uint)(length - MinLength) > MaxLength - MinLength;
|
||||
|
||||
private void ParseLines(SpanLineEnumerator lines)
|
||||
{
|
||||
int movectr = 0;
|
||||
bool first = true;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
ReadOnlySpan<char> trim = line.Trim();
|
||||
if (IsLengthOutOfRange(trim))
|
||||
{
|
||||
// Try for other languages just in case.
|
||||
if (first && trim.Length != 0)
|
||||
{
|
||||
ParseFirstLine(trim);
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
InvalidLines.Add(line.ToString());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
ParseFirstLine(trim);
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (ParseLine(trim, ref movectr))
|
||||
return; // End of moves, end of set data
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseLines(IEnumerable<string> lines)
|
||||
{
|
||||
int movectr = 0;
|
||||
bool first = true;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
ReadOnlySpan<char> trim = line.Trim();
|
||||
if (IsLengthOutOfRange(trim))
|
||||
{
|
||||
// Try for other languages just in case.
|
||||
if (first && trim.Length != 0)
|
||||
{
|
||||
ParseFirstLine(trim);
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
InvalidLines.Add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
ParseFirstLine(trim);
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (ParseLine(trim, ref movectr))
|
||||
return; // End of moves, end of set data
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParseLine(ReadOnlySpan<char> line, ref int movectr)
|
||||
{
|
||||
var moves = Moves.AsSpan();
|
||||
if (line[0] is '-' or '–')
|
||||
{
|
||||
var moveString = ParseLineMove(line);
|
||||
int move = StringUtil.FindIndexIgnoreCase(Strings.movelist, moveString);
|
||||
if (move < 0)
|
||||
InvalidLines.Add($"Unknown Move: {moveString}");
|
||||
else if (moves.Contains((ushort)move))
|
||||
InvalidLines.Add($"Duplicate Move: {moveString}");
|
||||
else
|
||||
moves[movectr++] = (ushort)move;
|
||||
|
||||
return movectr == MaxMoveCount;
|
||||
}
|
||||
|
||||
if (movectr != 0)
|
||||
return true;
|
||||
|
||||
bool valid;
|
||||
var split = line.IndexOf(LineSplit, StringComparison.Ordinal);
|
||||
if (split == -1)
|
||||
{
|
||||
valid = ParseSingle(line); // Nature
|
||||
}
|
||||
else
|
||||
{
|
||||
var left = line[..split].Trim();
|
||||
var right = line[(split + LineSplit.Length)..].Trim();
|
||||
valid = ParseEntry(left, right);
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
InvalidLines.Add(line.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ParseSingle(ReadOnlySpan<char> identifier)
|
||||
{
|
||||
if (!identifier.EndsWith("Nature", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var firstSpace = identifier.IndexOf(' ');
|
||||
if (firstSpace == -1)
|
||||
return false;
|
||||
var nature = identifier[..firstSpace];
|
||||
return (Nature = (Nature)StringUtil.FindIndexIgnoreCase(Strings.natures, nature)).IsFixed();
|
||||
}
|
||||
|
||||
private bool ParseEntry(ReadOnlySpan<char> identifier, ReadOnlySpan<char> value) => identifier switch
|
||||
{
|
||||
"Ability" => (Ability = StringUtil.FindIndexIgnoreCase(Strings.abilitylist, value)) >= 0,
|
||||
"Nature" => (Nature = (Nature)StringUtil.FindIndexIgnoreCase(Strings.natures , value)).IsFixed(),
|
||||
"Shiny" => Shiny = StringUtil.IsMatchIgnoreCase("Yes", value),
|
||||
"Gigantamax" => CanGigantamax = StringUtil.IsMatchIgnoreCase("Yes", value),
|
||||
"Friendship" => ParseFriendship(value),
|
||||
"EVs" => ParseLineEVs(value),
|
||||
"IVs" => ParseLineIVs(value),
|
||||
"Level" => ParseLevel(value),
|
||||
"Dynamax Level" => ParseDynamax(value),
|
||||
"Tera Type" => ParseTeraType(value),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private bool ParseLevel(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!byte.TryParse(value.Trim(), out var val))
|
||||
return false;
|
||||
if ((uint)val is 0 or > 100)
|
||||
return false;
|
||||
Level = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseFriendship(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!byte.TryParse(value.Trim(), out var val))
|
||||
return false;
|
||||
Friendship = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseDynamax(ReadOnlySpan<char> value)
|
||||
{
|
||||
Context = EntityContext.Gen8;
|
||||
var val = Util.ToInt32(value);
|
||||
if ((uint)val > 10)
|
||||
return false;
|
||||
DynamaxLevel = (byte)val;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseTeraType(ReadOnlySpan<char> value)
|
||||
{
|
||||
Context = EntityContext.Gen9;
|
||||
var types = Strings.types;
|
||||
var val = StringUtil.FindIndexIgnoreCase(types, value);
|
||||
if (val < 0)
|
||||
return false;
|
||||
if (val == TeraTypeUtil.StellarTypeDisplayStringIndex)
|
||||
val = TeraTypeUtil.Stellar;
|
||||
TeraType = (MoveType)val;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the standard Text representation of the set details.
|
||||
/// </summary>
|
||||
public string Text => GetText();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized Text representation of the set details.
|
||||
/// </summary>
|
||||
/// <param name="lang">Language code</param>
|
||||
public string LocalizedText(string lang = DefaultLanguage) => LocalizedText(GameLanguage.GetLanguageIndex(lang));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized Text representation of the set details.
|
||||
/// </summary>
|
||||
/// <param name="lang">Language ID</param>
|
||||
private string LocalizedText(int lang)
|
||||
{
|
||||
var strings = GameInfo.GetStrings(lang);
|
||||
return GetText(strings);
|
||||
}
|
||||
|
||||
private string GetText(GameStrings? strings = null)
|
||||
{
|
||||
if (Species is 0 or > MAX_SPECIES)
|
||||
return string.Empty;
|
||||
|
||||
if (strings is not null)
|
||||
Strings = strings;
|
||||
|
||||
var result = GetSetLines();
|
||||
return string.Join(Environment.NewLine, result);
|
||||
}
|
||||
|
||||
public List<string> GetSetLines()
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
// First Line: Name, Nickname, Gender, Item
|
||||
var form = ShowdownParsing.GetShowdownFormName(Species, FormName);
|
||||
result.Add(GetStringFirstLine(form));
|
||||
|
||||
// IVs
|
||||
var maxIV = Context.Generation() < 3 ? 15 : 31;
|
||||
var ivs = GetStringStats(IVs, maxIV);
|
||||
if (ivs.Length != 0)
|
||||
result.Add($"IVs: {string.Join(" / ", ivs)}");
|
||||
|
||||
// EVs
|
||||
var evs = GetStringStats(EVs, 0);
|
||||
if (evs.Length != 0)
|
||||
result.Add($"EVs: {string.Join(" / ", evs)}");
|
||||
|
||||
// Secondary Stats
|
||||
if ((uint)Ability < Strings.Ability.Count)
|
||||
result.Add($"Ability: {Strings.Ability[Ability]}");
|
||||
if (Context == EntityContext.Gen9 && TeraType != MoveType.Any)
|
||||
{
|
||||
if ((uint)TeraType <= TeraTypeUtil.MaxType) // Fairy
|
||||
result.Add($"Tera Type: {Strings.Types[(int)TeraType]}");
|
||||
else if ((uint)TeraType == TeraTypeUtil.Stellar)
|
||||
result.Add($"Tera Type: {Strings.Types[TeraTypeUtil.StellarTypeDisplayStringIndex]}");
|
||||
}
|
||||
|
||||
if (Level != 100)
|
||||
result.Add($"Level: {Level}");
|
||||
if (Shiny)
|
||||
result.Add("Shiny: Yes");
|
||||
if (Context == EntityContext.Gen8 && DynamaxLevel != 10)
|
||||
result.Add($"Dynamax Level: {DynamaxLevel}");
|
||||
if (Context == EntityContext.Gen8 && CanGigantamax)
|
||||
result.Add("Gigantamax: Yes");
|
||||
|
||||
if ((uint)Nature < Strings.Natures.Count)
|
||||
result.Add($"{Strings.Natures[(byte)Nature]} Nature");
|
||||
|
||||
// Moves
|
||||
result.AddRange(GetStringMoves());
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetStringFirstLine(string form)
|
||||
{
|
||||
string specForm = Strings.Species[Species];
|
||||
if (form.Length != 0)
|
||||
specForm += $"-{form.Replace("Mega ", "Mega-")}";
|
||||
else if (Species == (int)NidoranM)
|
||||
specForm = specForm.Replace("♂", "-M");
|
||||
else if (Species == (int)NidoranF)
|
||||
specForm = specForm.Replace("♀", "-F");
|
||||
|
||||
string result = GetSpeciesNickname(specForm);
|
||||
|
||||
// omit genderless or nonspecific
|
||||
if (Gender is 1)
|
||||
result += " (F)";
|
||||
else if (Gender is 0)
|
||||
result += " (M)";
|
||||
|
||||
if (HeldItem > 0)
|
||||
{
|
||||
var items = Strings.GetItemStrings(Context);
|
||||
if ((uint)HeldItem < items.Length)
|
||||
result += $" @ {items[HeldItem]}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetSpeciesNickname(string specForm)
|
||||
{
|
||||
if (Nickname.Length == 0 || Nickname == specForm)
|
||||
return specForm;
|
||||
bool isNicknamed = SpeciesName.IsNicknamedAnyLanguage(Species, Nickname, Context.Generation());
|
||||
if (!isNicknamed)
|
||||
return specForm;
|
||||
return $"{Nickname} ({specForm})";
|
||||
}
|
||||
|
||||
public static string[] GetStringStats<T>(ReadOnlySpan<T> stats, T ignoreValue) where T : IEquatable<T>
|
||||
{
|
||||
var count = stats.Length - stats.Count(ignoreValue);
|
||||
if (count == 0)
|
||||
return [];
|
||||
|
||||
var result = new string[count];
|
||||
int ctr = 0;
|
||||
for (int i = 0; i < stats.Length; i++)
|
||||
{
|
||||
var statIndex = GetStatIndexStored(i);
|
||||
var statValue = stats[statIndex];
|
||||
if (statValue.Equals(ignoreValue))
|
||||
continue; // ignore unused stats
|
||||
var statName = StatNames[statIndex];
|
||||
result[ctr++] = $"{statValue} {statName}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetStringMoves()
|
||||
{
|
||||
var moves = Strings.Move;
|
||||
foreach (var move in Moves)
|
||||
{
|
||||
if (move == 0 || move >= moves.Count)
|
||||
continue;
|
||||
|
||||
if (move != (int)Move.HiddenPower || HiddenPowerType == -1)
|
||||
{
|
||||
yield return $"- {moves[move]}";
|
||||
continue;
|
||||
}
|
||||
|
||||
var type = 1 + HiddenPowerType; // skip Normal
|
||||
var typeName = Strings.Types[type];
|
||||
yield return $"- {moves[move]} [{typeName}]";
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetStatIndexStored(int displayIndex) => displayIndex switch
|
||||
{
|
||||
3 => 4,
|
||||
4 => 5,
|
||||
5 => 3,
|
||||
_ => displayIndex,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Forces some properties to indicate the set for future display values.
|
||||
/// </summary>
|
||||
/// <param name="pk">PKM to convert to string</param>
|
||||
public void InterpretAsPreview(PKM pk)
|
||||
{
|
||||
if (pk.Format <= 2) // Nature preview from IVs
|
||||
Nature = Experience.GetNatureVC(pk.EXP);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the <see cref="PKM"/> data into an importable set format for Pokémon Showdown.
|
||||
/// </summary>
|
||||
/// <param name="pk">PKM to convert to string</param>
|
||||
/// <returns>New ShowdownSet object representing the input <see cref="pk"/></returns>
|
||||
public ShowdownSet(PKM pk)
|
||||
{
|
||||
if (pk.Species == 0)
|
||||
return;
|
||||
|
||||
Context = pk.Context;
|
||||
|
||||
Nickname = pk.Nickname;
|
||||
Species = pk.Species;
|
||||
HeldItem = pk.HeldItem;
|
||||
Ability = pk.Ability;
|
||||
pk.GetEVs(EVs);
|
||||
pk.GetIVs(IVs);
|
||||
|
||||
var moves = Moves.AsSpan();
|
||||
pk.GetMoves(moves);
|
||||
if (moves.Contains((ushort)Move.HiddenPower))
|
||||
HiddenPowerType = (sbyte)HiddenPower.GetType(IVs, Context);
|
||||
|
||||
Nature = pk.StatNature;
|
||||
Gender = pk.Gender < 2 ? pk.Gender : (byte)2;
|
||||
Friendship = pk.CurrentFriendship;
|
||||
Level = pk.CurrentLevel;
|
||||
Shiny = pk.IsShiny;
|
||||
|
||||
if (pk is PK8 g) // Only set Gigantamax if it is a PK8
|
||||
{
|
||||
CanGigantamax = g.CanGigantamax;
|
||||
DynamaxLevel = g.DynamaxLevel;
|
||||
}
|
||||
|
||||
if (pk is ITeraType t)
|
||||
TeraType = t.TeraType;
|
||||
if (pk is IHyperTrain h)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (h.IsHyperTrained(i))
|
||||
IVs[i] = pk.MaxIV;
|
||||
}
|
||||
}
|
||||
|
||||
FormName = ShowdownParsing.GetStringFromForm(Form = pk.Form, Strings, Species, Context);
|
||||
}
|
||||
|
||||
private void ParseFirstLine(ReadOnlySpan<char> first)
|
||||
{
|
||||
int itemSplit = first.IndexOf(ItemSplit, StringComparison.Ordinal);
|
||||
if (itemSplit != -1)
|
||||
{
|
||||
var itemName = first[(itemSplit + ItemSplit.Length)..];
|
||||
var speciesName = first[..itemSplit];
|
||||
|
||||
ParseItemName(itemName);
|
||||
ParseFirstLineNoItem(speciesName);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseFirstLineNoItem(first);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseItemName(ReadOnlySpan<char> itemName)
|
||||
{
|
||||
if (TrySetItem(Context, itemName))
|
||||
return;
|
||||
if (TrySetItem(EntityContext.Gen3, itemName))
|
||||
return;
|
||||
if (TrySetItem(EntityContext.Gen2, itemName))
|
||||
return;
|
||||
InvalidLines.Add($"Unknown Item: {itemName}");
|
||||
|
||||
bool TrySetItem(EntityContext context, ReadOnlySpan<char> span)
|
||||
{
|
||||
var items = Strings.GetItemStrings(context);
|
||||
int item = StringUtil.FindIndexIgnoreCase(items, span);
|
||||
if (item < 0)
|
||||
return false;
|
||||
HeldItem = item;
|
||||
Context = context;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseFirstLineNoItem(ReadOnlySpan<char> line)
|
||||
{
|
||||
// Gender Detection
|
||||
if (line.EndsWith("(M)", StringComparison.Ordinal))
|
||||
{
|
||||
line = line[..^3].TrimEnd();
|
||||
Gender = 0;
|
||||
}
|
||||
else if (line.EndsWith("(F)", StringComparison.Ordinal))
|
||||
{
|
||||
line = line[..^3].TrimEnd();
|
||||
Gender = 1;
|
||||
}
|
||||
|
||||
// Nickname Detection
|
||||
if (line.IndexOf('(') != -1 && line.IndexOf(')') != -1)
|
||||
ParseSpeciesNickname(line);
|
||||
else
|
||||
ParseSpeciesForm(line);
|
||||
}
|
||||
|
||||
private const string Gmax = "-Gmax";
|
||||
|
||||
private bool ParseSpeciesForm(ReadOnlySpan<char> speciesLine)
|
||||
{
|
||||
speciesLine = speciesLine.Trim();
|
||||
if (speciesLine.Length == 0)
|
||||
return false;
|
||||
|
||||
if (speciesLine.EndsWith(Gmax, StringComparison.Ordinal))
|
||||
{
|
||||
CanGigantamax = true;
|
||||
speciesLine = speciesLine[..^Gmax.Length];
|
||||
}
|
||||
|
||||
var speciesIndex = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine);
|
||||
if (speciesIndex > 0)
|
||||
{
|
||||
// success, nothing else !
|
||||
Species = (ushort)speciesIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Form string present.
|
||||
int end = speciesLine.IndexOf('-');
|
||||
if (end < 0)
|
||||
return false;
|
||||
|
||||
speciesIndex = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine[..end]);
|
||||
if (speciesIndex > 0)
|
||||
{
|
||||
Species = (ushort)speciesIndex;
|
||||
FormName = speciesLine[(end + 1)..].ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
// failure to parse, check edge cases
|
||||
foreach (var e in DashedSpecies)
|
||||
{
|
||||
var sn = Strings.Species[e];
|
||||
if (!speciesLine.StartsWith(sn.Replace("♂", "-M").Replace("♀", "-F"), StringComparison.Ordinal))
|
||||
continue;
|
||||
Species = e;
|
||||
FormName = speciesLine[sn.Length..].ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Version Megas
|
||||
end = speciesLine[Math.Max(0, end - 1)..].LastIndexOf('-');
|
||||
if (end < 0)
|
||||
return false;
|
||||
|
||||
speciesIndex = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine[..end]);
|
||||
if (speciesIndex > 0)
|
||||
{
|
||||
Species = (ushort)speciesIndex;
|
||||
FormName = speciesLine[(end + 1)..].ToString();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ParseSpeciesNickname(ReadOnlySpan<char> line)
|
||||
{
|
||||
// Entering into this method requires both ( and ) to be present within the input line.
|
||||
int index = line.LastIndexOf('(');
|
||||
ReadOnlySpan<char> species;
|
||||
ReadOnlySpan<char> nickname;
|
||||
if (index > 1) // parenthesis value after: Nickname (Species), correct.
|
||||
{
|
||||
nickname = line[..index].TrimEnd();
|
||||
species = line[(index + 1)..];
|
||||
if (species.Length != 0 && species[^1] == ')')
|
||||
species = species[..^1];
|
||||
}
|
||||
else // parenthesis value before: (Species) Nickname, incorrect
|
||||
{
|
||||
int start = index + 1;
|
||||
int end = line.LastIndexOf(')');
|
||||
var tmp = line[start..end];
|
||||
if (end < line.Length - 2)
|
||||
{
|
||||
nickname = line[(end + 2)..];
|
||||
species = tmp;
|
||||
}
|
||||
else // (Species), or garbage
|
||||
{
|
||||
species = tmp;
|
||||
nickname = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (ParseSpeciesForm(species))
|
||||
Nickname = nickname.ToString();
|
||||
else if (ParseSpeciesForm(nickname))
|
||||
Nickname = species.ToString();
|
||||
}
|
||||
|
||||
private ReadOnlySpan<char> ParseLineMove(ReadOnlySpan<char> line)
|
||||
{
|
||||
var startSearch = line[1] == ' ' ? 2 : 1;
|
||||
var option = line.IndexOf('/');
|
||||
line = option != -1 ? line[startSearch..option] : line[startSearch..];
|
||||
|
||||
var moveString = line.Trim();
|
||||
|
||||
var hiddenPowerName = Strings.Move[(int)Move.HiddenPower];
|
||||
if (!moveString.StartsWith(hiddenPowerName, StringComparison.OrdinalIgnoreCase))
|
||||
return moveString; // regular move
|
||||
|
||||
if (moveString.Length == hiddenPowerName.Length)
|
||||
return hiddenPowerName;
|
||||
|
||||
// Defined Hidden Power
|
||||
var type = GetHiddenPowerType(moveString[(hiddenPowerName.Length + 1)..]);
|
||||
var types = Strings.types.AsSpan(1, HiddenPower.TypeCount);
|
||||
int hpVal = StringUtil.FindIndexIgnoreCase(types, type); // Get HP Type
|
||||
if (hpVal == -1)
|
||||
return hiddenPowerName;
|
||||
|
||||
HiddenPowerType = (sbyte)hpVal;
|
||||
if (IVs.AsSpan().ContainsAnyExcept(31))
|
||||
{
|
||||
if (!HiddenPower.SetIVsForType(hpVal, IVs, Context))
|
||||
InvalidLines.Add($"Invalid IVs for Hidden Power Type: {type}");
|
||||
}
|
||||
else if (hpVal >= 0)
|
||||
{
|
||||
HiddenPower.SetIVs(hpVal, IVs, Context); // Alter IVs
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidLines.Add($"Invalid Hidden Power Type: {type}");
|
||||
}
|
||||
return hiddenPowerName;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> GetHiddenPowerType(ReadOnlySpan<char> line)
|
||||
{
|
||||
var type = line.Trim();
|
||||
if (type.Length == 0)
|
||||
return type;
|
||||
|
||||
if (type[0] == '(' && type[^1] == ')')
|
||||
return type[1..^1].Trim();
|
||||
if (type[0] == '[' && type[^1] == ']')
|
||||
return type[1..^1].Trim();
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private bool ParseLineEVs(ReadOnlySpan<char> line)
|
||||
{
|
||||
int start = 0;
|
||||
while (true)
|
||||
{
|
||||
var chunk = line[start..];
|
||||
var separator = chunk.IndexOf('/');
|
||||
var len = separator == -1 ? chunk.Length : separator;
|
||||
var tuple = chunk[..len].Trim();
|
||||
if (!AbsorbValue(tuple))
|
||||
InvalidLines.Add($"Invalid EV tuple: {tuple}");
|
||||
if (separator == -1)
|
||||
break; // no more stats
|
||||
start += separator + 1;
|
||||
}
|
||||
return true;
|
||||
|
||||
bool AbsorbValue(ReadOnlySpan<char> text)
|
||||
{
|
||||
var space = text.IndexOf(' ');
|
||||
if (space == -1)
|
||||
return false;
|
||||
var stat = text[(space + 1)..].Trim();
|
||||
var statIndex = StringUtil.FindIndexIgnoreCase(StatNames, stat);
|
||||
if (statIndex == -1)
|
||||
return false;
|
||||
var value = text[..space].Trim();
|
||||
if (!ushort.TryParse(value, out var statValue))
|
||||
return false;
|
||||
EVs[statIndex] = statValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParseLineIVs(ReadOnlySpan<char> line)
|
||||
{
|
||||
int start = 0;
|
||||
while (true)
|
||||
{
|
||||
var chunk = line[start..];
|
||||
var separator = chunk.IndexOf('/');
|
||||
var len = separator == -1 ? chunk.Length : separator;
|
||||
var tuple = chunk[..len].Trim();
|
||||
if (!AbsorbValue(tuple))
|
||||
InvalidLines.Add($"Invalid IV tuple: {tuple}");
|
||||
if (separator == -1)
|
||||
break; // no more stats
|
||||
start += separator + 1;
|
||||
}
|
||||
return true;
|
||||
|
||||
bool AbsorbValue(ReadOnlySpan<char> text)
|
||||
{
|
||||
var space = text.IndexOf(' ');
|
||||
if (space == -1)
|
||||
return false;
|
||||
var stat = text[(space + 1)..].Trim();
|
||||
var statIndex = StringUtil.FindIndexIgnoreCase(StatNames, stat);
|
||||
if (statIndex == -1)
|
||||
return false;
|
||||
var value = text[..space].Trim();
|
||||
if (!byte.TryParse(value, out var statValue))
|
||||
return false;
|
||||
IVs[statIndex] = statValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ public static class LanguageGCRemap
|
|||
/// </summary>
|
||||
public static LanguageID ToLanguageID(this LanguageGC lang) => lang switch
|
||||
{
|
||||
LanguageGC.Hacked => LanguageID.Hacked,
|
||||
LanguageGC.Hacked => LanguageID.None,
|
||||
LanguageGC.Japanese => LanguageID.Japanese,
|
||||
LanguageGC.English => LanguageID.English,
|
||||
LanguageGC.German => LanguageID.German,
|
||||
|
|
@ -73,7 +73,7 @@ public static class LanguageGCRemap
|
|||
/// </summary>
|
||||
public static LanguageGC ToLanguageGC(this LanguageID lang) => lang switch
|
||||
{
|
||||
LanguageID.Hacked => LanguageGC.Hacked,
|
||||
LanguageID.None => LanguageGC.Hacked,
|
||||
LanguageID.Japanese => LanguageGC.Japanese,
|
||||
LanguageID.English => LanguageGC.English,
|
||||
LanguageID.German => LanguageGC.German,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace PKHeX.Core;
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Contiguous series Game Language IDs
|
||||
|
|
@ -9,7 +9,7 @@ public enum LanguageID : byte
|
|||
/// Undefined Language ID, usually indicative of a value not being set.
|
||||
/// </summary>
|
||||
/// <remarks>Gen5 Japanese In-game Trades happen to not have their Language value set, and express Language=0.</remarks>
|
||||
Hacked = 0,
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Japanese (日本語)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ public static int GetLanguageIndex(string lang)
|
|||
/// Language codes supported for loading string resources
|
||||
/// </summary>
|
||||
/// <see cref="ProgramLanguage"/>
|
||||
public static ReadOnlySpan<string> AllSupportedLanguages => LanguageCodes;
|
||||
|
||||
private static readonly string[] LanguageCodes = ["ja", "en", "fr", "it", "de", "es", "ko", "zh-Hans", "zh-Hant"];
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ private LanguageID GetLanguage(LanguageID request)
|
|||
if (Language == LanguageRestriction.International && request is not (English or French or Italian or German or Spanish))
|
||||
return English;
|
||||
|
||||
if (request is Hacked or UNUSED_6 or >= Korean)
|
||||
if (request is None or UNUSED_6 or >= Korean)
|
||||
return English;
|
||||
return request;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ private LanguageID GetLanguage(LanguageID request)
|
|||
if (Language == LanguageRestriction.InternationalNotEnglish && request is not (French or Italian or German or Spanish))
|
||||
return French;
|
||||
|
||||
if (request is Hacked or UNUSED_6 or >= Korean)
|
||||
if (request is None or UNUSED_6 or >= Korean)
|
||||
return English;
|
||||
return request;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ private LanguageID GetSafeLanguageNotEgg(LanguageID language)
|
|||
{
|
||||
if (Language != 0)
|
||||
return (LanguageID) Language;
|
||||
if (language < LanguageID.Korean && language != LanguageID.Hacked)
|
||||
if (language < LanguageID.Korean && language != LanguageID.None)
|
||||
{
|
||||
if (Language == 0 && language is not LanguageID.Japanese)
|
||||
return language;
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public static bool IsValidLanguageID(int currentLanguage, int maxLanguageID, PKM
|
|||
if (currentLanguage > maxLanguageID)
|
||||
return false; // Language not available (yet)
|
||||
|
||||
if (currentLanguage <= (int)LanguageID.Hacked && !(enc is EncounterTrade5BW && EncounterTrade5BW.IsValidMissingLanguage(pk)))
|
||||
if (currentLanguage <= (int)LanguageID.None && !(enc is EncounterTrade5BW && EncounterTrade5BW.IsValidMissingLanguage(pk)))
|
||||
return false; // Missing Language value is not obtainable
|
||||
|
||||
return true; // Language is possible
|
||||
|
|
|
|||
|
|
@ -80,9 +80,29 @@ public static class Language
|
|||
Korean => "ko",
|
||||
ChineseS => "zh-Hans",
|
||||
ChineseT => "zh-Hant",
|
||||
English => "en",
|
||||
_ => GameLanguage.DefaultLanguage,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="LanguageID"/> value from a language code.
|
||||
/// </summary>
|
||||
/// <param name="language">Language code.</param>
|
||||
/// <returns>Language ID.</returns>
|
||||
public static LanguageID GetLanguageValue(string language) => language switch
|
||||
{
|
||||
"ja" => Japanese,
|
||||
"fr" => French,
|
||||
"it" => Italian,
|
||||
"de" => German,
|
||||
"es" => Spanish,
|
||||
"ko" => Korean,
|
||||
"zh-Hans" => ChineseS,
|
||||
"zh-Hant" => ChineseT,
|
||||
"en" => English,
|
||||
_ => GetLanguageValue(GameLanguage.DefaultLanguage),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Main Series language ID from a GameCube (C/XD) language ID.
|
||||
/// </summary>
|
||||
|
|
|
|||
35
PKHeX.Core/Resources/config/battle_de.json
Normal file
35
PKHeX.Core/Resources/config/battle_de.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["KP", "Ang", "Vert", "Init", "SpA", "SpV"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["KP", "Angriff", "Verteidigung", "Initiative", "Sp. Angriff", "Sp. Verteidigung"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "Männlich",
|
||||
"Female": "Weiblich",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "Freundschaft: " },
|
||||
{ "Token": "EVs", "Text": "EVs: " },
|
||||
{ "Token": "IVs", "Text": "DVs: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "GVs: " },
|
||||
{ "Token": "Level", "Text": "Level: " },
|
||||
{ "Token": "Ability", "Text": "Fähigkeit: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "Dynamax Level: " },
|
||||
{ "Token": "TeraType", "Text": "Tera-Typ: " },
|
||||
{ "Token": "Gender", "Text": "Geschlecht: "},
|
||||
{ "Token": "Nickname", "Text": "Spitzname: "},
|
||||
{ "Token": "HeldItem", "Text": "Item: "}
|
||||
],
|
||||
"Right": [
|
||||
{ "Token": "Nature", "Text": " Wesen" }
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "Schillerndes: Ja" },
|
||||
{ "Token": "Gigantamax", "Text": "Gigadynamax: Ja" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_en.json
Normal file
35
PKHeX.Core/Resources/config/battle_en.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["HP", "Atk", "Def", "SpA", "SpD", "Spe"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["HP", "Attack", "Defense", "Sp. Atk", "Sp. Def", "Speed"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "Male",
|
||||
"Female": "Female",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "Friendship: " },
|
||||
{ "Token": "EVs", "Text": "EVs: " },
|
||||
{ "Token": "IVs", "Text": "IVs: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "GVs: " },
|
||||
{ "Token": "Level", "Text": "Level: " },
|
||||
{ "Token": "Ability", "Text": "Ability: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "Dynamax Level: " },
|
||||
{ "Token": "TeraType", "Text": "Tera Type: " },
|
||||
{ "Token": "Gender", "Text": "Gender: "},
|
||||
{ "Token": "Nickname", "Text": "Nickname: "},
|
||||
{ "Token": "HeldItem", "Text": "Held Item: "}
|
||||
],
|
||||
"Right": [
|
||||
{ "Token": "Nature", "Text": " Nature" }
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "Shiny: Yes" },
|
||||
{ "Token": "Gigantamax", "Text": "Gigantamax: Yes" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_es.json
Normal file
35
PKHeX.Core/Resources/config/battle_es.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["PS", "Atq", "Def", "AtS", "DeS", "Vel"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["PS", "Ataque", "Defensa", "Atq. Esp.", "Def. Esp.", "Velocidad"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "Masculino",
|
||||
"Female": "Femenina",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "Felicidad: " },
|
||||
{ "Token": "EVs", "Text": "EVs: " },
|
||||
{ "Token": "IVs", "Text": "IVs: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "GVs: " },
|
||||
{ "Token": "Level", "Text": "Nivel: " },
|
||||
{ "Token": "Ability", "Text": "Habilidad: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "Nivel Dinamax: " },
|
||||
{ "Token": "TeraType", "Text": "Teratipo: " },
|
||||
{ "Token": "Nature", "Text": "Naturaleza " },
|
||||
{ "Token": "Gender", "Text": "Género: "},
|
||||
{ "Token": "Nickname", "Text": "Mote: "},
|
||||
{ "Token": "HeldItem", "Text": "Objeto Equipado: "}
|
||||
],
|
||||
"Right": [
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "Shiny: Sí" },
|
||||
{ "Token": "Gigantamax", "Text": "Gigamax: Sí" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_fr.json
Normal file
35
PKHeX.Core/Resources/config/battle_fr.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["PV", "Atq", "Def", "AtS", "DeS", "Vit"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["PV", "Attaque", "Défense", "Atq. Spé.", "Déf. Spé.", "Vitesse"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "Homme",
|
||||
"Female": "Femme",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "Bonheur : " },
|
||||
{ "Token": "EVs", "Text": "EVs : " },
|
||||
{ "Token": "IVs", "Text": "IVs : " },
|
||||
{ "Token": "AVs", "Text": "AVs : " },
|
||||
{ "Token": "GVs", "Text": "GVs : " },
|
||||
{ "Token": "Level", "Text": "Niveau : " },
|
||||
{ "Token": "Ability", "Text": "Talent : " },
|
||||
{ "Token": "DynamaxLevel", "Text": "Niveau Dynamax : " },
|
||||
{ "Token": "TeraType", "Text": "Type Téra : " },
|
||||
{ "Token": "Nature", "Text": "Nature " },
|
||||
{ "Token": "Gender", "Text": "Gender : "},
|
||||
{ "Token": "Nickname", "Text": "Surnom : "},
|
||||
{ "Token": "HeldItem", "Text": "Objet : "}
|
||||
],
|
||||
"Right": [
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "Chromatique: Oui" },
|
||||
{ "Token": "Gigantamax", "Text": "Gigamax: Oui" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_it.json
Normal file
35
PKHeX.Core/Resources/config/battle_it.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["PS", "Att", "Dif", "AtS", "DiS", "Vel"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["PS", "Attacco", "Difesa", "Attacco Sp.", "Difesa Sp.", "Velocità"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "Maschio",
|
||||
"Female": "Femmina",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "Amicizia: " },
|
||||
{ "Token": "EVs", "Text": "EVs: " },
|
||||
{ "Token": "IVs", "Text": "IVs: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "GVs: " },
|
||||
{ "Token": "Level", "Text": "Livello: " },
|
||||
{ "Token": "Ability", "Text": "Abilità: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "Livello Dynamax: " },
|
||||
{ "Token": "TeraType", "Text": "Teratipo: " },
|
||||
{ "Token": "Nature", "Text": "Natura " },
|
||||
{ "Token": "Gender", "Text": "Genere: "},
|
||||
{ "Token": "Nickname", "Text": "Soprannome: "},
|
||||
{ "Token": "HeldItem", "Text": "Strumento tenuto: "}
|
||||
],
|
||||
"Right": [
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "Cromatico: Si" },
|
||||
{ "Token": "Gigantamax", "Text": "Gigamax: Si" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_ja.json
Normal file
35
PKHeX.Core/Resources/config/battle_ja.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["HP", "攻撃", "防御", "特攻", "特防", "素早さ"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["HP", "攻撃", "防御", "特攻", "特防", "素早さ"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "男性",
|
||||
"Female": "女性",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "なつき度 " },
|
||||
{ "Token": "EVs", "Text": "努力値 " },
|
||||
{ "Token": "IVs", "Text": "個体値 " },
|
||||
{ "Token": "AVs", "Text": "AVs " },
|
||||
{ "Token": "GVs", "Text": "頑張る " },
|
||||
{ "Token": "Level", "Text": "Lv " },
|
||||
{ "Token": "Ability", "Text": "特性 " },
|
||||
{ "Token": "DynamaxLevel", "Text": "ダイマックスレベル " },
|
||||
{ "Token": "TeraType", "Text": "テラスタイプ " },
|
||||
{ "Token": "Gender", "Text": "性別 "},
|
||||
{ "Token": "Nickname", "Text": "ニックネーム "},
|
||||
{ "Token": "HeldItem", "Text": "持ち物 "}
|
||||
],
|
||||
"Right": [
|
||||
{ "Token": "Nature", "Text": "性格" }
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "光ひかる: はい" },
|
||||
{ "Token": "Gigantamax", "Text": "キョダイマックス: はい" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_ko.json
Normal file
35
PKHeX.Core/Resources/config/battle_ko.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["HP", "공격", "방어", "특공", "특방", "스피드"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["HP", "공격", "방어", "특공", "특방", "스피드"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "남성",
|
||||
"Female": "여성",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "친밀도 " },
|
||||
{ "Token": "EVs", "Text": "EVs: " },
|
||||
{ "Token": "IVs", "Text": "IVs: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "노력 레벨을 " },
|
||||
{ "Token": "Level", "Text": "스피드 " },
|
||||
{ "Token": "Ability", "Text": "특성 " },
|
||||
{ "Token": "DynamaxLevel", "Text": "다이맥스 레벨 " },
|
||||
{ "Token": "TeraType", "Text": "테라스탈타입 " },
|
||||
{ "Token": "Gender", "Text": "성별 "},
|
||||
{ "Token": "Nickname", "Text": "이름 "},
|
||||
{ "Token": "HeldItem", "Text": "持ち物 "}
|
||||
],
|
||||
"Right": [
|
||||
{ "Token": "Nature", "Text": "성격" }
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "빛나는: 예" },
|
||||
{ "Token": "Gigantamax", "Text": "거다이맥스: 예" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_zh-hans.json
Normal file
35
PKHeX.Core/Resources/config/battle_zh-hans.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["HP", "攻击", "防御", "特攻", "特防", "速度"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["HP", "攻击", "防御", "特攻", "特防", "速度"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "男",
|
||||
"Female": "女",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "亲密度: " },
|
||||
{ "Token": "EVs", "Text": "努力值: " },
|
||||
{ "Token": "IVs", "Text": "个体值: " },
|
||||
{ "Token": "AVs", "Text": "AVs " },
|
||||
{ "Token": "GVs", "Text": "奋斗等级 " },
|
||||
{ "Token": "Level", "Text": "等级: " },
|
||||
{ "Token": "Ability", "Text": "特性: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "极巨等级: " },
|
||||
{ "Token": "TeraType", "Text": "太晶属性: " },
|
||||
{ "Token": "Nature", "Text": "性格: " },
|
||||
{ "Token": "Gender", "Text": "性别: "},
|
||||
{ "Token": "Nickname", "Text": "昵称: "},
|
||||
{ "Token": "HeldItem", "Text": "持有物: "}
|
||||
],
|
||||
"Right": [
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "發光寶: 是的" },
|
||||
{ "Token": "Gigantamax", "Text": "超极巨: 是的" }
|
||||
]
|
||||
}
|
||||
35
PKHeX.Core/Resources/config/battle_zh-hant.json
Normal file
35
PKHeX.Core/Resources/config/battle_zh-hant.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"StatNames": {
|
||||
"Names": ["HP", "攻擊", "防禦", "特攻", "特防", "速度"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"StatNamesFull": {
|
||||
"Names": ["HP", "攻擊", "防禦", "特攻", "特防", "速度"],
|
||||
"ValueGap": " ",
|
||||
"Separator": " / "
|
||||
},
|
||||
"Male": "男",
|
||||
"Female": "女",
|
||||
"Left": [
|
||||
{ "Token": "Friendship", "Text": "親密度: " },
|
||||
{ "Token": "EVs", "Text": "努力值: " },
|
||||
{ "Token": "IVs", "Text": "個體值: " },
|
||||
{ "Token": "AVs", "Text": "AVs: " },
|
||||
{ "Token": "GVs", "Text": "奋斗等级: " },
|
||||
{ "Token": "Level", "Text": "等級: " },
|
||||
{ "Token": "Ability", "Text": "特性: " },
|
||||
{ "Token": "DynamaxLevel", "Text": "極巨化等級: " },
|
||||
{ "Token": "TeraType", "Text": "太晶屬性: " },
|
||||
{ "Token": "Nature", "Text": "性格: " },
|
||||
{ "Token": "Gender", "Text": "性別: "},
|
||||
{ "Token": "Nickname", "Text": "昵稱: "},
|
||||
{ "Token": "HeldItem", "Text": "携帶物品: "}
|
||||
],
|
||||
"Right": [
|
||||
],
|
||||
"Center": [
|
||||
{ "Token": "Shiny", "Text": "發光寶: 是的" },
|
||||
{ "Token": "Gigantamax", "Text": "超極巨: 是的" }
|
||||
]
|
||||
}
|
||||
|
|
@ -38,7 +38,9 @@ public EmbeddedResourceCache(Assembly assembly)
|
|||
|
||||
private static string GetFileName(string resName)
|
||||
{
|
||||
var period = resName.LastIndexOf('.', resName.Length - 5);
|
||||
var period = resName.LastIndexOf('.');
|
||||
if (resName.Length - period <= 5)
|
||||
period = resName.LastIndexOf('.', period - 1);
|
||||
var start = period + 1;
|
||||
System.Diagnostics.Debug.Assert(start != 0); // should have a period in the name
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public partial class MoveDisplay : UserControl
|
|||
{
|
||||
public MoveDisplay() => InitializeComponent();
|
||||
|
||||
public int Populate(PKM pk, ushort move, EntityContext context, ReadOnlySpan<string> moves, bool valid = true)
|
||||
public int Populate(PKM pk, GameStrings strings, ushort move, EntityContext context, ReadOnlySpan<string> moves, bool valid = true)
|
||||
{
|
||||
if (move == 0 || move >= moves.Length)
|
||||
{
|
||||
|
|
@ -24,7 +24,7 @@ public int Populate(PKM pk, ushort move, EntityContext context, ReadOnlySpan<str
|
|||
if (move == (int)Core.Move.HiddenPower && pk.Context is not EntityContext.Gen8a)
|
||||
{
|
||||
if (HiddenPower.TryGetTypeIndex(pk.HPType, out type))
|
||||
name = $"{name} ({GameInfo.Strings.types[type]}) [{pk.HPPower}]";
|
||||
name = $"{name} ({strings.types[type]}) [{pk.HPPower}]";
|
||||
}
|
||||
|
||||
var size = PokePreview.MeasureSize(name, L_Move.Font);
|
||||
|
|
|
|||
|
|
@ -1331,7 +1331,9 @@ public void ClickShowdownExportCurrentBox(object sender, EventArgs e)
|
|||
private static void ExportShowdownText(SaveFile sav, string success, Func<SaveFile, IEnumerable<PKM>> fetch)
|
||||
{
|
||||
var list = fetch(sav);
|
||||
var result = ShowdownParsing.GetShowdownSets(list, Environment.NewLine + Environment.NewLine);
|
||||
var programLanguage = Language.GetLanguageValue(Main.Settings.Startup.Language);
|
||||
var settings = Main.Settings.BattleTemplate.Export.GetSettings(programLanguage, sav.Context);
|
||||
var result = ShowdownParsing.GetShowdownSets(list, Environment.NewLine + Environment.NewLine, settings);
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
return;
|
||||
if (WinFormsUtil.SetClipboardText(result))
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ private void InitializeComponent()
|
|||
{
|
||||
PAN_All = new System.Windows.Forms.Panel();
|
||||
FLP_List = new System.Windows.Forms.FlowLayoutPanel();
|
||||
L_Stats = new System.Windows.Forms.Label();
|
||||
L_LinesBeforeMoves = new System.Windows.Forms.Label();
|
||||
FLP_Moves = new System.Windows.Forms.FlowLayoutPanel();
|
||||
Move1 = new MoveDisplay();
|
||||
Move2 = new MoveDisplay();
|
||||
Move3 = new MoveDisplay();
|
||||
Move4 = new MoveDisplay();
|
||||
L_Etc = new System.Windows.Forms.Label();
|
||||
L_LinesAfterMoves = new System.Windows.Forms.Label();
|
||||
PAN_Top = new System.Windows.Forms.Panel();
|
||||
FLP_Top = new System.Windows.Forms.FlowLayoutPanel();
|
||||
PB_Ball = new System.Windows.Forms.PictureBox();
|
||||
|
|
@ -68,9 +68,9 @@ private void InitializeComponent()
|
|||
//
|
||||
FLP_List.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
FLP_List.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
FLP_List.Controls.Add(L_Stats);
|
||||
FLP_List.Controls.Add(L_LinesBeforeMoves);
|
||||
FLP_List.Controls.Add(FLP_Moves);
|
||||
FLP_List.Controls.Add(L_Etc);
|
||||
FLP_List.Controls.Add(L_LinesAfterMoves);
|
||||
FLP_List.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
|
||||
FLP_List.Location = new System.Drawing.Point(0, 34);
|
||||
FLP_List.Margin = new System.Windows.Forms.Padding(0);
|
||||
|
|
@ -79,15 +79,15 @@ private void InitializeComponent()
|
|||
FLP_List.TabIndex = 1;
|
||||
FLP_List.WrapContents = false;
|
||||
//
|
||||
// L_Stats
|
||||
// L_LinesBeforeMoves
|
||||
//
|
||||
L_Stats.AutoSize = true;
|
||||
L_Stats.Location = new System.Drawing.Point(2, 4);
|
||||
L_Stats.Margin = new System.Windows.Forms.Padding(2, 4, 0, 0);
|
||||
L_Stats.Name = "L_Stats";
|
||||
L_Stats.Size = new System.Drawing.Size(32, 15);
|
||||
L_Stats.TabIndex = 5;
|
||||
L_Stats.Text = "Stats";
|
||||
L_LinesBeforeMoves.AutoSize = true;
|
||||
L_LinesBeforeMoves.Location = new System.Drawing.Point(2, 4);
|
||||
L_LinesBeforeMoves.Margin = new System.Windows.Forms.Padding(2, 4, 0, 0);
|
||||
L_LinesBeforeMoves.Name = "L_LinesBeforeMoves";
|
||||
L_LinesBeforeMoves.Size = new System.Drawing.Size(36, 17);
|
||||
L_LinesBeforeMoves.TabIndex = 5;
|
||||
L_LinesBeforeMoves.Text = "Stats";
|
||||
//
|
||||
// FLP_Moves
|
||||
//
|
||||
|
|
@ -100,7 +100,7 @@ private void InitializeComponent()
|
|||
FLP_Moves.Controls.Add(Move4);
|
||||
FLP_List.SetFlowBreak(FLP_Moves, true);
|
||||
FLP_Moves.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
|
||||
FLP_Moves.Location = new System.Drawing.Point(0, 23);
|
||||
FLP_Moves.Location = new System.Drawing.Point(0, 25);
|
||||
FLP_Moves.Margin = new System.Windows.Forms.Padding(0, 4, 0, 4);
|
||||
FLP_Moves.Name = "FLP_Moves";
|
||||
FLP_Moves.Size = new System.Drawing.Size(142, 96);
|
||||
|
|
@ -150,15 +150,15 @@ private void InitializeComponent()
|
|||
Move4.Size = new System.Drawing.Size(138, 24);
|
||||
Move4.TabIndex = 4;
|
||||
//
|
||||
// L_Etc
|
||||
// L_LinesAfterMoves
|
||||
//
|
||||
L_Etc.AutoSize = true;
|
||||
L_Etc.Location = new System.Drawing.Point(2, 123);
|
||||
L_Etc.Margin = new System.Windows.Forms.Padding(2, 0, 0, 4);
|
||||
L_Etc.Name = "L_Etc";
|
||||
L_Etc.Size = new System.Drawing.Size(28, 15);
|
||||
L_Etc.TabIndex = 6;
|
||||
L_Etc.Text = "Info";
|
||||
L_LinesAfterMoves.AutoSize = true;
|
||||
L_LinesAfterMoves.Location = new System.Drawing.Point(2, 125);
|
||||
L_LinesAfterMoves.Margin = new System.Windows.Forms.Padding(2, 0, 0, 4);
|
||||
L_LinesAfterMoves.Name = "L_LinesAfterMoves";
|
||||
L_LinesAfterMoves.Size = new System.Drawing.Size(30, 17);
|
||||
L_LinesAfterMoves.TabIndex = 6;
|
||||
L_LinesAfterMoves.Text = "Info";
|
||||
//
|
||||
// PAN_Top
|
||||
//
|
||||
|
|
@ -243,7 +243,7 @@ private void InitializeComponent()
|
|||
|
||||
private System.Windows.Forms.Panel PAN_All;
|
||||
private System.Windows.Forms.FlowLayoutPanel FLP_List;
|
||||
private System.Windows.Forms.Label L_Stats;
|
||||
private System.Windows.Forms.Label L_LinesBeforeMoves;
|
||||
private System.Windows.Forms.Panel PAN_Top;
|
||||
private System.Windows.Forms.Label L_Name;
|
||||
private System.Windows.Forms.PictureBox PB_Ball;
|
||||
|
|
@ -252,7 +252,7 @@ private void InitializeComponent()
|
|||
private MoveDisplay Move2;
|
||||
private MoveDisplay Move3;
|
||||
private MoveDisplay Move4;
|
||||
private System.Windows.Forms.Label L_Etc;
|
||||
private System.Windows.Forms.Label L_LinesAfterMoves;
|
||||
private System.Windows.Forms.FlowLayoutPanel FLP_Moves;
|
||||
private System.Windows.Forms.FlowLayoutPanel FLP_Top;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using PKHeX.Core;
|
||||
|
||||
|
|
@ -30,17 +28,17 @@ public PokePreview()
|
|||
Properties.Resources.gender_2,
|
||||
];
|
||||
|
||||
public void Populate(PKM pk)
|
||||
public void Populate(PKM pk, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
var la = new LegalityAnalysis(pk);
|
||||
int width = PopulateHeader(pk);
|
||||
PopulateMoves(pk, la, ref width);
|
||||
PopulateText(pk, la, width);
|
||||
int width = PopulateHeader(pk, settings);
|
||||
PopulateMoves(pk, la, settings, ref width);
|
||||
PopulateText(pk, la, settings, width);
|
||||
}
|
||||
|
||||
private int PopulateHeader(PKM pk)
|
||||
private int PopulateHeader(PKM pk, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
var name = GetNameTitle(pk);
|
||||
var name = GetNameTitle(pk, settings);
|
||||
var size = MeasureSize(name, L_Name.Font);
|
||||
L_Name.Width = Math.Max(InitialNameWidth, size.Width);
|
||||
L_Name.Text = name;
|
||||
|
|
@ -52,14 +50,19 @@ private int PopulateHeader(PKM pk)
|
|||
return Math.Max(InitialWidth, width);
|
||||
}
|
||||
|
||||
private static string GetNameTitle(PKM pk)
|
||||
private static string GetNameTitle(PKM pk, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
// Don't care about form; the user will be able to see the sprite next to the preview.
|
||||
var nick = pk.Nickname;
|
||||
var all = GameInfo.Strings.Species;
|
||||
var strings = settings.Localization.Strings;
|
||||
var all = strings.Species;
|
||||
var species = pk.Species;
|
||||
if (species >= all.Count)
|
||||
return nick;
|
||||
var expect = all[species];
|
||||
if (settings.IsTokenInExport(BattleTemplateToken.Nickname))
|
||||
return expect; // Nickname will be on another line.
|
||||
|
||||
if (nick.Equals(expect, StringComparison.OrdinalIgnoreCase))
|
||||
return nick;
|
||||
return $"{nick} ({expect})";
|
||||
|
|
@ -87,32 +90,33 @@ private void PopulateGender(PKM pk)
|
|||
PB_Gender.Image = GenderImages[gender];
|
||||
}
|
||||
|
||||
private void PopulateMoves(PKM pk, LegalityAnalysis la, ref int width)
|
||||
private void PopulateMoves(PKM pk, LegalityAnalysis la, in BattleTemplateExportSettings settings, ref int width)
|
||||
{
|
||||
var context = pk.Context;
|
||||
var names = GameInfo.Strings.movelist;
|
||||
var strings = settings.Localization.Strings;
|
||||
var names = strings.movelist;
|
||||
var check = la.Info.Moves;
|
||||
var w1 = Move1.Populate(pk, pk.Move1, context, names, check[0].Valid);
|
||||
var w2 = Move2.Populate(pk, pk.Move2, context, names, check[1].Valid);
|
||||
var w3 = Move3.Populate(pk, pk.Move3, context, names, check[2].Valid);
|
||||
var w4 = Move4.Populate(pk, pk.Move4, context, names, check[3].Valid);
|
||||
var w1 = Move1.Populate(pk, strings, pk.Move1, context, names, check[0].Valid);
|
||||
var w2 = Move2.Populate(pk, strings, pk.Move2, context, names, check[1].Valid);
|
||||
var w3 = Move3.Populate(pk, strings, pk.Move3, context, names, check[2].Valid);
|
||||
var w4 = Move4.Populate(pk, strings, pk.Move4, context, names, check[3].Valid);
|
||||
|
||||
var maxWidth = Math.Max(w1, Math.Max(w2, Math.Max(w3, w4)));
|
||||
width = Math.Max(width, maxWidth + Move1.Margin.Horizontal + interiorMargin);
|
||||
}
|
||||
|
||||
private void PopulateText(PKM pk, LegalityAnalysis la, int width)
|
||||
private void PopulateText(PKM pk, LegalityAnalysis la, in BattleTemplateExportSettings settings, int width)
|
||||
{
|
||||
var (stats, enc) = GetStatsString(pk, la);
|
||||
var settings = Main.Settings.Hover;
|
||||
var (before, after) = GetBeforeAndAfter(pk, la, settings);
|
||||
var hover = Main.Settings.Hover;
|
||||
|
||||
bool hasMoves = pk.MoveCount != 0;
|
||||
FLP_Moves.Visible = hasMoves;
|
||||
var height = FLP_List.Top + interiorMargin;
|
||||
if (hasMoves)
|
||||
height += FLP_Moves.Height + FLP_Moves.Margin.Vertical;
|
||||
ToggleLabel(L_Stats, stats, settings.PreviewShowPaste, ref width, ref height);
|
||||
ToggleLabel(L_Etc, enc, settings.HoverSlotShowEncounter, ref width, ref height);
|
||||
ToggleLabel(L_LinesBeforeMoves, before, hover.PreviewShowPaste, ref width, ref height);
|
||||
ToggleLabel(L_LinesAfterMoves, after, hover.HoverSlotShowEncounter, ref width, ref height);
|
||||
Size = new Size(width, height);
|
||||
}
|
||||
|
||||
|
|
@ -137,78 +141,67 @@ public static Size MeasureSize(ReadOnlySpan<char> text, Font font)
|
|||
return TextRenderer.MeasureText(text, font, new Size(), flags);
|
||||
}
|
||||
|
||||
private static (string Detail, string Encounter) GetStatsString(PKM pk, LegalityAnalysis la)
|
||||
private static (string Before, string After) GetBeforeAndAfter(PKM pk, LegalityAnalysis la, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
var setText = SummaryPreviewer.GetPreviewText(pk, la);
|
||||
var sb = new StringBuilder();
|
||||
var lines = setText.AsSpan().EnumerateLines();
|
||||
if (!lines.MoveNext())
|
||||
throw new ArgumentException("Invalid text format", nameof(pk));
|
||||
var order = settings.Order;
|
||||
// Bifurcate the order into two sections; split via Moves.
|
||||
var moveIndex = settings.GetTokenIndex(BattleTemplateToken.Moves);
|
||||
var before = moveIndex == -1 ? order : order[..moveIndex];
|
||||
var after = moveIndex == -1 ? default : order[(moveIndex + 1)..];
|
||||
if (before.Length > 0 && before[0] == BattleTemplateToken.FirstLine)
|
||||
before = before[1..]; // remove first line token; trust that users don't randomly move it lower in the list.
|
||||
|
||||
var first = lines.Current;
|
||||
var itemIndex = first.IndexOf('@');
|
||||
if (itemIndex != -1) // Held Item
|
||||
{
|
||||
var remaining = first[(itemIndex + 2)..];
|
||||
if (remaining[^1] == ')')
|
||||
remaining = remaining[..^3]; // lop off gender
|
||||
var item = remaining.Trim();
|
||||
if (item.Length != 0)
|
||||
sb.AppendLine($"Held Item: {item}");
|
||||
}
|
||||
var start = SummaryPreviewer.GetPreviewText(pk, settings with { Order = before });
|
||||
var end = SummaryPreviewer.GetPreviewText(pk, settings with { Order = after });
|
||||
if (settings.IsTokenInExport(BattleTemplateToken.IVs, before))
|
||||
TryAppendOtherStats(pk, ref start, settings);
|
||||
else if (settings.IsTokenInExport(BattleTemplateToken.IVs, after))
|
||||
TryAppendOtherStats(pk, ref end, settings);
|
||||
|
||||
if (pk is IGanbaru g)
|
||||
AddGanbaru(g, sb);
|
||||
if (pk is IAwakened a)
|
||||
AddAwakening(a, sb);
|
||||
if (Main.Settings.Hover.HoverSlotShowEncounter)
|
||||
end = SummaryPreviewer.AppendEncounterInfo(la, end);
|
||||
|
||||
while (lines.MoveNext())
|
||||
{
|
||||
var line = lines.Current;
|
||||
if (IsMoveLine(line))
|
||||
{
|
||||
while (lines.MoveNext())
|
||||
{
|
||||
if (!IsMoveLine(lines.Current))
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
sb.Append(line).AppendLine();
|
||||
}
|
||||
|
||||
var detail = sb.ToString();
|
||||
sb.Clear();
|
||||
while (lines.MoveNext())
|
||||
{
|
||||
var line = lines.Current;
|
||||
sb.Append(line).AppendLine();
|
||||
}
|
||||
var enc = sb.ToString();
|
||||
return (detail.TrimEnd(), enc.TrimEnd());
|
||||
|
||||
static bool IsMoveLine(ReadOnlySpan<char> line) => line.Length != 0 && line[0] == '-';
|
||||
return (start, end);
|
||||
}
|
||||
|
||||
private static void AddGanbaru(IGanbaru g, StringBuilder sb)
|
||||
private static void TryAppendOtherStats(PKM pk, ref string line, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
if (pk is IGanbaru g)
|
||||
AppendGanbaru(g, ref line, settings);
|
||||
if (pk is IAwakened a)
|
||||
AppendAwakening(a, ref line, settings);
|
||||
}
|
||||
|
||||
private static void AppendGanbaru(IGanbaru g, ref string line, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
Span<byte> gvs = stackalloc byte[6];
|
||||
g.GetGVs(gvs);
|
||||
TryAdd<byte>(sb, "GVs", gvs);
|
||||
var statNames = settings.Localization.Config.GetStatDisplay(settings.StatsOther);
|
||||
var value = TryAdd<byte>(gvs, statNames);
|
||||
if (value.Length == 0)
|
||||
return;
|
||||
var result = settings.Localization.Config.Push(BattleTemplateToken.GVs, value);
|
||||
line += Environment.NewLine + result;
|
||||
}
|
||||
|
||||
private static void AddAwakening(IAwakened a, StringBuilder sb)
|
||||
private static void AppendAwakening(IAwakened a, ref string line, in BattleTemplateExportSettings settings)
|
||||
{
|
||||
Span<byte> avs = stackalloc byte[6];
|
||||
a.GetAVs(avs);
|
||||
TryAdd<byte>(sb, "AVs", avs);
|
||||
var statNames = settings.Localization.Config.GetStatDisplay(settings.StatsOther);
|
||||
var value = TryAdd<byte>(avs, statNames);
|
||||
if (value.Length == 0)
|
||||
return;
|
||||
var result = settings.Localization.Config.Push(BattleTemplateToken.AVs, value);
|
||||
line += Environment.NewLine + result;
|
||||
}
|
||||
|
||||
private static void TryAdd<T>(StringBuilder sb, [ConstantExpected] string type, ReadOnlySpan<T> stats, T ignore = default) where T : unmanaged, IEquatable<T>
|
||||
private static string TryAdd<T>(ReadOnlySpan<T> stats, StatDisplayConfig statNames, T ignore = default) where T : unmanaged, IEquatable<T>
|
||||
{
|
||||
var chunks = ShowdownSet.GetStringStats(stats, ignore);
|
||||
if (chunks.Length != 0)
|
||||
sb.AppendLine($"{type}: {string.Join(" / ", chunks)}");
|
||||
var chunks = ShowdownSet.GetStringStats(stats, ignore, statNames);
|
||||
if (chunks.Length == 0)
|
||||
return string.Empty;
|
||||
return string.Join(" / ", chunks);
|
||||
}
|
||||
|
||||
/// <summary> Prevent stealing focus from the form that shows this. </summary>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ public static void UpdateSlot(PictureBox pb, ISlotInfo c, PKM p, SaveFile s, boo
|
|||
|
||||
pb.BackColor = Color.Transparent;
|
||||
pb.Image = img;
|
||||
pb.AccessibleDescription = ShowdownParsing.GetLocalizedPreviewText(p, Main.CurrentLanguage);
|
||||
|
||||
var x = Main.Settings;
|
||||
var programLanguage = Language.GetLanguageValue(x.Startup.Language);
|
||||
var cfg = x.BattleTemplate;
|
||||
var settings = cfg.Hover.GetSettings(programLanguage, p.Context);
|
||||
pb.AccessibleDescription = ShowdownParsing.GetLocalizedPreviewText(p, settings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,20 +24,32 @@ public void Show(Control pb, PKM pk)
|
|||
return;
|
||||
}
|
||||
|
||||
var programLanguage = Language.GetLanguageValue(Main.Settings.Startup.Language);
|
||||
var cfg = Main.Settings.BattleTemplate;
|
||||
var settings = cfg.Hover.GetSettings(programLanguage, pk.Context);
|
||||
|
||||
if (Settings.HoverSlotShowPreview && Control.ModifierKeys != Keys.Alt)
|
||||
UpdatePreview(pb, pk);
|
||||
{
|
||||
UpdatePreview(pb, pk, settings);
|
||||
}
|
||||
else if (Settings.HoverSlotShowText)
|
||||
ShowSet.SetToolTip(pb, GetPreviewText(pk, new LegalityAnalysis(pk)));
|
||||
{
|
||||
var text = GetPreviewText(pk, settings);
|
||||
if (Settings.HoverSlotShowEncounter)
|
||||
text = AppendEncounterInfo(new LegalityAnalysis(pk), text);
|
||||
ShowSet.SetToolTip(pb, text);
|
||||
}
|
||||
|
||||
if (Settings.HoverSlotPlayCry)
|
||||
Cry.PlayCry(pk, pk.Context);
|
||||
}
|
||||
|
||||
private void UpdatePreview(Control pb, PKM pk)
|
||||
private void UpdatePreview(Control pb, PKM pk, BattleTemplateExportSettings settings)
|
||||
{
|
||||
_source.Cancel();
|
||||
_source = new();
|
||||
UpdatePreviewPosition(new());
|
||||
Previewer.Populate(pk);
|
||||
Previewer.Populate(pk, settings);
|
||||
Previewer.Show();
|
||||
}
|
||||
|
||||
|
|
@ -80,12 +92,13 @@ public void Clear()
|
|||
Cry.Stop();
|
||||
}
|
||||
|
||||
public static string GetPreviewText(PKM pk, LegalityAnalysis la)
|
||||
public static string GetPreviewText(PKM pk, BattleTemplateExportSettings settings) => ShowdownParsing.GetLocalizedPreviewText(pk, settings);
|
||||
|
||||
public static string AppendEncounterInfo(LegalityAnalysis la, string text)
|
||||
{
|
||||
var text = ShowdownParsing.GetLocalizedPreviewText(pk, Main.Settings.Startup.Language);
|
||||
if (!Main.Settings.Hover.HoverSlotShowEncounter)
|
||||
return text;
|
||||
var result = new List<string> { text, string.Empty };
|
||||
var result = new List<string>(8) { text };
|
||||
if (text.Length != 0) // add a blank line between the set and the encounter info if isn't already a blank line
|
||||
result.Add("");
|
||||
LegalityFormatting.AddEncounterInfo(la, result);
|
||||
return string.Join(Environment.NewLine, result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -524,18 +524,15 @@ private void ClickShowdownImportPKM(object sender, EventArgs e)
|
|||
|
||||
// Get Simulator Data
|
||||
var text = Clipboard.GetText();
|
||||
ShowdownSet set;
|
||||
if (ShowdownTeam.IsURL(text, out var url) && ShowdownTeam.TryGetSets(url, out var content))
|
||||
set = ShowdownParsing.GetShowdownSets(content).FirstOrDefault() ?? new(""); // take only first set
|
||||
else if (PokepasteTeam.IsURL(text, out url) && PokepasteTeam.TryGetSets(url, out content))
|
||||
set = ShowdownParsing.GetShowdownSets(content).FirstOrDefault() ?? new(""); // take only first set
|
||||
else
|
||||
set = new ShowdownSet(text);
|
||||
var sets = BattleTemplateTeams.TryGetSets(text);
|
||||
var set = sets.FirstOrDefault() ?? new(""); // take only first set
|
||||
|
||||
if (set.Species == 0)
|
||||
{ WinFormsUtil.Alert(MsgSimulatorFailClipboard); return; }
|
||||
|
||||
var reformatted = set.Text;
|
||||
var programLanguage = Language.GetLanguageValue(Settings.Startup.Language);
|
||||
var settings = Settings.BattleTemplate.Export.GetSettings(programLanguage, set.Context);
|
||||
var reformatted = set.GetText(settings);
|
||||
if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSimulatorLoad, reformatted))
|
||||
return;
|
||||
|
||||
|
|
@ -555,7 +552,9 @@ private void ClickShowdownExportPKM(object sender, EventArgs e)
|
|||
}
|
||||
|
||||
var pk = PreparePKM();
|
||||
var text = ShowdownParsing.GetShowdownText(pk);
|
||||
var programLanguage = Language.GetLanguageValue(Settings.Startup.Language);
|
||||
var settings = Settings.BattleTemplate.Export.GetSettings(programLanguage, pk.Context);
|
||||
var text = ShowdownParsing.GetShowdownText(pk, settings);
|
||||
bool success = WinFormsUtil.SetClipboardText(text);
|
||||
if (!success || !Clipboard.GetText().Equals(text))
|
||||
WinFormsUtil.Alert(MsgClipboardFailWrite, MsgSimulatorExportFail);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public sealed class PKHeXSettings
|
|||
public SpriteSettings Sprite { get; set; } = new();
|
||||
public SoundSettings Sounds { get; set; } = new();
|
||||
public HoverSettings Hover { get; set; } = new();
|
||||
public BattleTemplateSettings BattleTemplate { get; set; } = new();
|
||||
|
||||
// GUI Specific
|
||||
public DrawConfig Draw { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -110,7 +110,11 @@ public SAV_Database(PKMEditor f1, SAVEditor saveditor)
|
|||
return;
|
||||
|
||||
var pk = Results[index];
|
||||
slot.AccessibleDescription = ShowdownParsing.GetLocalizedPreviewText(pk.Entity, Main.CurrentLanguage);
|
||||
|
||||
var x = Main.Settings;
|
||||
var programLanguage = Language.GetLanguageValue(x.Startup.Language);
|
||||
var settings = x.BattleTemplate.Hover.GetSettings(programLanguage, pk.Entity.Context);
|
||||
slot.AccessibleDescription = ShowdownParsing.GetLocalizedPreviewText(pk.Entity, settings);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ private void LoadSettings(object obj)
|
|||
var tab = new TabPage(p) { Name = $"Tab_{p}" };
|
||||
var pg = new PropertyGrid { SelectedObject = state, Dock = DockStyle.Fill };
|
||||
tab.Controls.Add(pg);
|
||||
pg.ExpandAllGridItems();
|
||||
tabControl1.TabPages.Add(tab);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ public class ShowdownSetTests
|
|||
[Fact]
|
||||
public void SimulatorGetParse()
|
||||
{
|
||||
var settings = new BattleTemplateExportSettings(BattleTemplateConfig.CommunityStandard);
|
||||
|
||||
foreach (ReadOnlySpan<char> setstr in Sets)
|
||||
{
|
||||
var set = new ShowdownSet(setstr).GetSetLines();
|
||||
var set = new ShowdownSet(setstr).GetSetLines(settings);
|
||||
foreach (var line in set)
|
||||
setstr.Contains(line, StringComparison.Ordinal).Should().BeTrue($"Line {line} should be in the set {setstr}");
|
||||
}
|
||||
|
|
@ -163,6 +165,67 @@ public void SimulatorParseEmpty()
|
|||
Assert.False(sets.Any());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SetAllTokenExample)]
|
||||
public void SimulatorTranslate(string message, string languageOriginal = "en")
|
||||
{
|
||||
var settingsOriginal = new BattleTemplateExportSettings(BattleTemplateConfig.CommunityStandard, languageOriginal);
|
||||
if (!ShowdownParsing.TryParseAnyLanguage(message, out var set))
|
||||
throw new Exception("Input failed");
|
||||
|
||||
var all = BattleTemplateLocalization.GetAll();
|
||||
foreach (var l in all)
|
||||
{
|
||||
var languageTarget = l.Key;
|
||||
if (languageTarget == languageOriginal)
|
||||
continue;
|
||||
|
||||
var exportSettings = new BattleTemplateExportSettings(languageTarget);
|
||||
var translated = set.GetText(exportSettings);
|
||||
translated.Should().NotBeNullOrEmpty();
|
||||
translated.Should().NotBe(message);
|
||||
|
||||
// Convert back, should be 1:1
|
||||
if (!ShowdownParsing.TryParseAnyLanguage(translated, out var set2))
|
||||
throw new Exception($"{languageTarget} parse failed");
|
||||
set2.InvalidLines.Should().BeEmpty();
|
||||
set2.Species.Should().Be(set.Species);
|
||||
set2.Form.Should().Be(set.Form);
|
||||
|
||||
var result = set2.GetText(settingsOriginal);
|
||||
result.Should().Be(message);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SetAllTokenExample)]
|
||||
public void SimulatorTranslateHABCDS(string message, string languageOriginal = "en")
|
||||
{
|
||||
var settingsOriginal = new BattleTemplateExportSettings(BattleTemplateConfig.CommunityStandard, languageOriginal);
|
||||
if (!ShowdownParsing.TryParseAnyLanguage(message, out var set))
|
||||
throw new Exception("Input failed");
|
||||
|
||||
var target = new BattleTemplateExportSettings("ja")
|
||||
{
|
||||
StatsIVs = StatDisplayStyle.HABCDS,
|
||||
StatsEVs = StatDisplayStyle.HABCDS,
|
||||
};
|
||||
|
||||
var translated = set.GetText(target);
|
||||
translated.Should().NotBeNullOrEmpty();
|
||||
translated.Should().NotBe(message);
|
||||
|
||||
// Convert back, should be 1:1
|
||||
if (!ShowdownParsing.TryParseAnyLanguage(translated, out var set2))
|
||||
throw new Exception("ja parse failed");
|
||||
set2.InvalidLines.Should().BeEmpty();
|
||||
set2.Species.Should().Be(set.Species);
|
||||
set2.Form.Should().Be(set.Form);
|
||||
|
||||
var result = set2.GetText(settingsOriginal);
|
||||
result.Should().Be(message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SetDuplicateMoves, 3)]
|
||||
public void SimulatorParseDuplicate(string text, int moveCount)
|
||||
|
|
@ -261,6 +324,24 @@ Modest Nature
|
|||
- Hyper Voice
|
||||
""";
|
||||
|
||||
private const string SetAllTokenExample =
|
||||
"""
|
||||
Pikachu (F) @ Oran Berry
|
||||
Ability: Static
|
||||
Level: 69
|
||||
Shiny: Yes
|
||||
Friendship: 42
|
||||
Dynamax Level: 3
|
||||
Gigantamax: Yes
|
||||
EVs: 12 HP / 5 Atk / 6 Def / 17 SpA / 4 SpD / 101 Spe
|
||||
Quirky Nature
|
||||
IVs: 30 HP / 22 Atk / 29 Def / 7 SpA / 1 SpD / 0 Spe
|
||||
- Pound
|
||||
- Sky Attack
|
||||
- Hyperspace Fury
|
||||
- Metronome
|
||||
""";
|
||||
|
||||
private const string SetSmeargle =
|
||||
"""
|
||||
Smeargle @ Focus Sash
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user