mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-23 02:45:31 -05:00
IGN allowed characters complete list + replace automatically
This commit is contained in:
@@ -17,19 +17,22 @@ describe("inGameNameIsValid", () => {
|
||||
"naïve#1234",
|
||||
"「テスト」#1234",
|
||||
"あ、い。#1234",
|
||||
"テスト〜#1234",
|
||||
"テスト~#1234",
|
||||
"ジャンプ#1234",
|
||||
"ロー・ル#1234",
|
||||
"こゝろ#1234",
|
||||
"すゞ#1234",
|
||||
"ヽヾ#1234",
|
||||
"【Foo】#1234",
|
||||
"〈Foo〉#1234",
|
||||
"《Foo》#1234",
|
||||
"『Foo』#1234",
|
||||
"〔Foo〕#1234",
|
||||
"こ々ろ#1234",
|
||||
"〆〇〃#1234",
|
||||
"〆仝#1234",
|
||||
"※Test#1234",
|
||||
"■□●#1234",
|
||||
"‐―#1234",
|
||||
"¦¨´#1234",
|
||||
"fuse⌒*#2949",
|
||||
"ƒ´`#1234",
|
||||
"Santiago’s#6987",
|
||||
"ȚțDŽDZĈĕ#1234",
|
||||
"ς〒#1234",
|
||||
"○☆Mikurby#2897",
|
||||
"仝#1234",
|
||||
"三#1234",
|
||||
@@ -37,7 +40,7 @@ describe("inGameNameIsValid", () => {
|
||||
"名前テスト1234#ab12c",
|
||||
"한국어#1234",
|
||||
"中文名#1234",
|
||||
"Test!#1234",
|
||||
"Test!?##1234",
|
||||
"Cafe\u0301#1234",
|
||||
];
|
||||
|
||||
@@ -66,10 +69,24 @@ describe("inGameNameIsValid", () => {
|
||||
|
||||
test("rejects characters the Switch keyboard does not allow in names", () => {
|
||||
const invalidNames = [
|
||||
"Santiago's#6987",
|
||||
"test@me#1234",
|
||||
"100%#1234",
|
||||
"a\\b#1234",
|
||||
"●#1234",
|
||||
"fuse⏜*#2949",
|
||||
"⚪⚫#1234",
|
||||
"◻◼#1234",
|
||||
"▲▼#1234",
|
||||
"𝑓#1234",
|
||||
"ˊˋ#1234",
|
||||
"⍑#1234",
|
||||
"•“„‛‚″#1234",
|
||||
"『Foo』《Bar》#1234",
|
||||
"〈〉〔〕〇〃〜#1234",
|
||||
"すゞヽヾ#1234",
|
||||
"Test#1234",
|
||||
"ıẞĘЁ#1234",
|
||||
"ɛᴠᴋї#1234",
|
||||
"♥#1234",
|
||||
];
|
||||
|
||||
@@ -86,6 +103,27 @@ describe("sanitizeInGameName", () => {
|
||||
["한국어#1234", "한국어#1234", "hangul"],
|
||||
["Cafe\u0301#1234", "Café#1234", "decomposed accents are composed"],
|
||||
["100%#1234", "100#1234", "characters the game does not allow"],
|
||||
[
|
||||
"Santiago's#6987",
|
||||
"Santiago’s#6987",
|
||||
"apostrophe becomes the in-game one",
|
||||
],
|
||||
["fuse⏜•#2949", "fuse⌒●#2949", "lookalikes become the in-game character"],
|
||||
["▲Yellow▼#6089", "△Yellow▽#6089", "filled triangles become hollow ones"],
|
||||
[
|
||||
"⍑FireMuffn#2264",
|
||||
"〒FireMuffn#2264",
|
||||
"up tack overbar becomes postal mark",
|
||||
],
|
||||
[
|
||||
"『Foo』《Bar》〜〃#1234",
|
||||
"「Foo」«Bar»~”#1234",
|
||||
"CJK brackets become their in-game lookalikes",
|
||||
],
|
||||
["★ ⦾culus ★#1898", "★ ◎culus ★#1898", "symbol lookalikes"],
|
||||
["Test。ア#1234", "Test。ア#1234", "fullwidth and halfwidth forms"],
|
||||
["ĘąěЁ#1234", "EaeЕ#1234", "accents the game lacks are dropped"],
|
||||
["ìıɛᴋ#1234", "ìiεк#1234", "letter lookalikes"],
|
||||
])("%s -> %s (%s)", (input, expected) => {
|
||||
expect(sanitizeInGameName(input)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -15,20 +15,20 @@ export const IN_GAME_NAME_CHARACTER_CATEGORIES = [
|
||||
id: "symbols",
|
||||
label: "inGameName.categories.symbols",
|
||||
characters: [
|
||||
..."¿¡′‘’‚‛•…″“”„«»←→↑↓⇒⇔˜ˊˋ¢€£¥¤𝑓×÷±∞√¬∀⊂⊃∴∵⌒∂№°¹²³¼½¾♪♭♀♂○⚪⚫◎◻◼◇◆△▲▽▼☆★©®™§¶†⍑※",
|
||||
..."¿¡′‘’…”‐―«»←→↑↓⇒⇔˜´¨¦¢€£¥¤ƒ×÷±∞√¬∀⊂⊃∴∵⌒∂№°¹²³¼½¾♪♭♀♂○●◎□■◇◆△▽☆★©®™§¶†※",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "accented",
|
||||
label: "inGameName.categories.accented",
|
||||
characters: [
|
||||
..."àáâãäåæāăąçćċčðďdždzèéêëēęěğġģħìíîïīįıijķĺļľłÀÁÂÃÄÅÆĀĂĄÇĆĊČÐĎDžDzÈÉÊËĒĘĚĞĠĢĦÌÍÎÏĪĮİIJĶĹĻĽŁñńņňòóôõöøœőŕřšßśşþťţùúûüūůűųýÿźżžÑŃŅŇÒÓÔÕÖØŒŐŔŘŠẞŚŞÞŤŢÙÚÛÜŪŮŰŲÝŸŹŻŽ",
|
||||
..."àáâãäåæāăçćċčĉðďdždzèéêëēĕğġģħìíîïīįijķĺļľłÀÁÂÃÄÅÆĀĂÇĆĊČĈÐĎDŽDZÈÉÊËĒĔĞĠĢĦÌÍÎÏĪĮİIJĶĹĻĽŁñńņňòóôõöøœőŕřšßśşþťțùúûüūůűųýÿźżžÑŃŅŇÒÓÔÕÖØŒŐŔŘŠŚŞÞŤȚÙÚÛÜŪŮŰŲÝŸŹŻŽ",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "greek",
|
||||
label: "inGameName.categories.greek",
|
||||
characters: [..."αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"],
|
||||
characters: [..."αβγδεζηθικλμνξοπρσςτυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"],
|
||||
},
|
||||
{
|
||||
id: "cyrillic",
|
||||
@@ -38,17 +38,17 @@ export const IN_GAME_NAME_CHARACTER_CATEGORIES = [
|
||||
{
|
||||
id: "hiragana",
|
||||
label: "inGameName.categories.hiragana",
|
||||
characters: [...range(0x3041, 0x3096), ..."ゝゞ"],
|
||||
characters: [...range(0x3041, 0x308f), ..."をんゝ"],
|
||||
},
|
||||
{
|
||||
id: "katakana",
|
||||
label: "inGameName.categories.katakana",
|
||||
characters: [...range(0x30a1, 0x30fa), ..."ヽヾ"],
|
||||
characters: [...range(0x30a1, 0x30ef), ..."ヲンヴヵヶ"],
|
||||
},
|
||||
{
|
||||
id: "cjk-symbols",
|
||||
label: "inGameName.categories.cjkSymbols",
|
||||
characters: [..."、。「」『』【】〈〉《》〔〕〜・ー々〆〇〃"],
|
||||
characters: [..."、。「」【】・ー~々〆〒仝"],
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
id: string;
|
||||
@@ -60,28 +60,152 @@ const PICKER_CHARACTERS = IN_GAME_NAME_CHARACTER_CATEGORIES.flatMap(
|
||||
(category) => category.characters,
|
||||
);
|
||||
|
||||
const ASCII_NOT_VALID = new Set(["%", "@", "\\"]);
|
||||
const ASCII_NOT_VALID = new Set(["'", "%", "@", "\\"]);
|
||||
const ASCII_CHARACTERS = range(0x20, 0x7e).filter(
|
||||
(character) => !ASCII_NOT_VALID.has(character),
|
||||
);
|
||||
|
||||
/** Japanese keyboard symbols, left out of the picker as they look like their ASCII counterparts. */
|
||||
const FULLWIDTH_SYMBOLS = [
|
||||
..."!#$&()*+,-./:;<=>?[]^_{|}¥",
|
||||
];
|
||||
|
||||
const ALLOWED_CHARACTERS = new Set<string>([
|
||||
...ASCII_CHARACTERS,
|
||||
...PICKER_CHARACTERS,
|
||||
...FULLWIDTH_SYMBOLS,
|
||||
]);
|
||||
|
||||
/** Every script a Switch keyboard can produce, incl. kanji/hanzi and hangul which the picker can't enumerate. */
|
||||
const ALLOWED_SCRIPTS_REGEXP =
|
||||
/^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}\p{Script=Hangul}]$/u;
|
||||
/** Kanji/hanzi and hangul, too numerous for the picker. */
|
||||
const IDEOGRAPH_OR_HANGUL_REGEXP =
|
||||
/^[\p{Unified_Ideograph}\p{Script=Hangul}]$/u;
|
||||
|
||||
/** Punctuation & symbols not covered by the scripts above. */
|
||||
const ALLOWED_CODE_POINT_RANGES = [
|
||||
[0x3000, 0x303f], // CJK symbols and punctuation
|
||||
[0x3099, 0x309c], // Kana voiced sound marks
|
||||
[0x30fb, 0x30fc], // Katakana middle dot & prolonged sound mark
|
||||
[0xff01, 0xff60], // Fullwidth forms
|
||||
[0xffe0, 0xffe6], // Fullwidth signs
|
||||
] as const;
|
||||
/** Lookalikes mapped to the in-game character. Anything else falls back to its compatibility or accentless form. */
|
||||
const CHARACTER_REPLACEMENTS = new Map([
|
||||
["'", "’"],
|
||||
["\t", " "],
|
||||
["⏜", "⌒"],
|
||||
["⁀", "⌒"],
|
||||
["⌢", "⌒"],
|
||||
["◠", "⌒"],
|
||||
["𝑓", "ƒ"],
|
||||
["◼", "■"],
|
||||
["▪", "■"],
|
||||
["⬛", "■"],
|
||||
["◻", "□"],
|
||||
["☐", "□"],
|
||||
["⬜", "□"],
|
||||
["▢", "□"],
|
||||
["⚪", "○"],
|
||||
["◯", "○"],
|
||||
["⚫", "●"],
|
||||
["⬤", "●"],
|
||||
["⏺", "●"],
|
||||
["•", "●"],
|
||||
["⦾", "◎"],
|
||||
["⊚", "◎"],
|
||||
["⭗", "◎"],
|
||||
["⦿", "◎"],
|
||||
["◉", "◎"],
|
||||
["▲", "△"],
|
||||
["▵", "△"],
|
||||
["🔺", "△"],
|
||||
["▼", "▽"],
|
||||
["∇", "▽"],
|
||||
["🔻", "▽"],
|
||||
["▿", "▽"],
|
||||
["♦", "◆"],
|
||||
["♢", "◇"],
|
||||
["◊", "◇"],
|
||||
["⬦", "◇"],
|
||||
["⭐", "★"],
|
||||
["⭑", "★"],
|
||||
["🌟", "★"],
|
||||
["✫", "★"],
|
||||
["✬", "★"],
|
||||
["✭", "★"],
|
||||
["✯", "★"],
|
||||
["✩", "☆"],
|
||||
["✰", "☆"],
|
||||
["✝", "†"],
|
||||
["✞", "†"],
|
||||
["🎵", "♪"],
|
||||
["🎶", "♪"],
|
||||
["♫", "♪"],
|
||||
["♩", "♪"],
|
||||
["⨯", "×"],
|
||||
["✖", "×"],
|
||||
["✕", "×"],
|
||||
["✘", "×"],
|
||||
["⟹", "⇒"],
|
||||
["⇛", "⇒"],
|
||||
["⬆", "↑"],
|
||||
["⬇", "↓"],
|
||||
["➡", "→"],
|
||||
["⟶", "→"],
|
||||
["≪", "«"],
|
||||
["≫", "»"],
|
||||
["《", "«"],
|
||||
["》", "»"],
|
||||
["『", "「"],
|
||||
["』", "」"],
|
||||
["〜", "~"],
|
||||
["〃", "”"],
|
||||
["⍑", "〒"],
|
||||
["·", "・"],
|
||||
["‧", "・"],
|
||||
["∙", "・"],
|
||||
["⋅", "・"],
|
||||
["ˊ", "´"],
|
||||
["ˋ", "`"],
|
||||
["˚", "°"],
|
||||
["º", "°"],
|
||||
["∘", "°"],
|
||||
["“", "”"],
|
||||
["„", "”"],
|
||||
["‛", "‘"],
|
||||
["‚", ","],
|
||||
["″", '"'],
|
||||
["–", "‐"],
|
||||
["—", "―"],
|
||||
["−", "-"],
|
||||
["∼", "~"],
|
||||
["Ţ", "Ț"],
|
||||
["ţ", "ț"],
|
||||
["Ș", "Ş"],
|
||||
["ș", "ş"],
|
||||
["Đ", "Ð"],
|
||||
["ı", "i"],
|
||||
["ẞ", "ß"],
|
||||
["ї", "ï"],
|
||||
["І", "I"],
|
||||
["і", "i"],
|
||||
["ѕ", "s"],
|
||||
["∅", "Ø"],
|
||||
["∆", "Δ"],
|
||||
["∑", "Σ"],
|
||||
["Ʌ", "Λ"],
|
||||
["Ө", "Θ"],
|
||||
["Ɵ", "Θ"],
|
||||
["ɵ", "θ"],
|
||||
["⍺", "α"],
|
||||
["ɑ", "α"],
|
||||
["ɛ", "ε"],
|
||||
["⍳", "ι"],
|
||||
["ɩ", "ι"],
|
||||
["ƞ", "η"],
|
||||
["ᴋ", "к"],
|
||||
["ᴍ", "м"],
|
||||
["ᴛ", "т"],
|
||||
["ᴠ", "v"],
|
||||
["ʙ", "в"],
|
||||
["ᴄ", "c"],
|
||||
["ʜ", "н"],
|
||||
["ᴏ", "o"],
|
||||
["ᴎ", "и"],
|
||||
["ᴙ", "я"],
|
||||
["ɸ", "φ"],
|
||||
]);
|
||||
|
||||
const IN_GAME_NAME_REGEXP = new RegExp(
|
||||
`^(.+)#([0-9a-z]{${IN_GAME_NAME.DISCRIMINATOR_MIN_LENGTH},${IN_GAME_NAME.DISCRIMINATOR_MAX_LENGTH}})$`,
|
||||
@@ -93,9 +217,9 @@ export function inGameNameLength(value: string): number {
|
||||
return [...value].length;
|
||||
}
|
||||
|
||||
/** Normalizes and drops every character the game does not allow. */
|
||||
/** Normalizes, swaps lookalikes for the in-game character and drops every character the game does not allow. */
|
||||
export function sanitizeInGameName(value: string): string {
|
||||
return [...normalizeInGameName(value)].filter(characterIsAllowed).join("");
|
||||
return [...normalizeInGameName(value)].map(toAllowedCharacters).join("");
|
||||
}
|
||||
|
||||
/** Unicode normalization applied before validating or storing. */
|
||||
@@ -119,15 +243,39 @@ export function inGameNameIsValid(value: string): boolean {
|
||||
}
|
||||
|
||||
function characterIsAllowed(character: string): boolean {
|
||||
if (ALLOWED_CHARACTERS.has(character)) return true;
|
||||
if (ALLOWED_SCRIPTS_REGEXP.test(character)) return true;
|
||||
|
||||
const codePoint = character.codePointAt(0)!;
|
||||
return ALLOWED_CODE_POINT_RANGES.some(
|
||||
([from, to]) => codePoint >= from && codePoint <= to,
|
||||
return (
|
||||
ALLOWED_CHARACTERS.has(character) ||
|
||||
IDEOGRAPH_OR_HANGUL_REGEXP.test(character)
|
||||
);
|
||||
}
|
||||
|
||||
function toAllowedCharacters(character: string): string {
|
||||
if (characterIsAllowed(character)) return character;
|
||||
|
||||
const replacement = CHARACTER_REPLACEMENTS.get(character);
|
||||
if (replacement) return replacement;
|
||||
|
||||
const compatibilityForm = [...character.normalize("NFKC")]
|
||||
.map((part) => CHARACTER_REPLACEMENTS.get(part) ?? part)
|
||||
.join("");
|
||||
if ([...compatibilityForm].every(characterIsAllowed)) {
|
||||
return compatibilityForm;
|
||||
}
|
||||
|
||||
return withoutDiacritics(character) ?? "";
|
||||
}
|
||||
|
||||
function withoutDiacritics(character: string): string | null {
|
||||
const decomposed = [...character.normalize("NFD")];
|
||||
while (decomposed.length > 1) {
|
||||
decomposed.pop();
|
||||
const candidate = decomposed.join("").normalize("NFC");
|
||||
if (characterIsAllowed(candidate)) return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function range(from: number, to: number): string[] {
|
||||
const characters: string[] = [];
|
||||
for (let codePoint = from; codePoint <= to; codePoint++) {
|
||||
|
||||
10
changelog/2026-10-01-in-game-name-characters.md
Normal file
10
changelog/2026-10-01-in-game-name-characters.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
navItem: u
|
||||
type: bug
|
||||
---
|
||||
In-game names now should accept exactly the characters the game allows
|
||||
|
||||
- More symbols such as ■, □, ● and ― are now allowed
|
||||
- Lookalike characters the game doesn't have were removed from the picker
|
||||
- Existing names using them were switched to the matching in-game character where possible
|
||||
- Typing a lookalike converts it automatically (e.g. ' becomes ’)
|
||||
273
migrations/20261001043951-in-game-name-lookalike-characters.ts
Normal file
273
migrations/20261001043951-in-game-name-lookalike-characters.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Characters stored in-game names use that the game doesn't have -> the in-game character. */
|
||||
const REPLACEMENTS = new Map<string, string>([
|
||||
["\u0009", " "],
|
||||
["'", "’"],
|
||||
["\u00a0", " "],
|
||||
["µ", "μ"],
|
||||
["·", "・"],
|
||||
["º", "°"],
|
||||
["Ą", "A"],
|
||||
["ą", "a"],
|
||||
["Đ", "Ð"],
|
||||
["Ę", "E"],
|
||||
["ę", "e"],
|
||||
["Ě", "E"],
|
||||
["ě", "e"],
|
||||
["ı", "i"],
|
||||
["ō", "o"],
|
||||
["Ţ", "Ț"],
|
||||
["ţ", "ț"],
|
||||
["ƞ", "η"],
|
||||
["Ɵ", "Θ"],
|
||||
["ơ", "o"],
|
||||
["ǎ", "a"],
|
||||
["ǒ", "o"],
|
||||
["Ș", "Ş"],
|
||||
["ș", "ş"],
|
||||
["Ȩ", "E"],
|
||||
["ȩ", "e"],
|
||||
["Ʌ", "Λ"],
|
||||
["ɑ", "α"],
|
||||
["ɛ", "ε"],
|
||||
["ɩ", "ι"],
|
||||
["ɵ", "θ"],
|
||||
["ɸ", "φ"],
|
||||
["ʙ", "в"],
|
||||
["ʜ", "н"],
|
||||
["ˊ", "´"],
|
||||
["ˋ", "`"],
|
||||
["˚", "°"],
|
||||
["ά", "α"],
|
||||
["έ", "ε"],
|
||||
["ή", "η"],
|
||||
["ί", "ι"],
|
||||
["ύ", "υ"],
|
||||
["ϕ", "φ"],
|
||||
["І", "I"],
|
||||
["ё", "е"],
|
||||
["ѕ", "s"],
|
||||
["і", "i"],
|
||||
["ї", "ï"],
|
||||
["Ө", "Θ"],
|
||||
["ᴄ", "c"],
|
||||
["ᴋ", "к"],
|
||||
["ᴍ", "м"],
|
||||
["ᴎ", "и"],
|
||||
["ᴏ", "o"],
|
||||
["ᴙ", "я"],
|
||||
["ᴛ", "т"],
|
||||
["ᴠ", "v"],
|
||||
["ᵃ", "a"],
|
||||
["ᵒ", "o"],
|
||||
["Ṭ", "T"],
|
||||
["ẞ", "ß"],
|
||||
["\u2009", " "],
|
||||
["–", "‐"],
|
||||
["—", "―"],
|
||||
["‚", ","],
|
||||
["‛", "‘"],
|
||||
["“", "”"],
|
||||
["„", "”"],
|
||||
["•", "●"],
|
||||
["‧", "・"],
|
||||
["″", '"'],
|
||||
["⁀", "⌒"],
|
||||
["⁰", "0"],
|
||||
["⁺", "+"],
|
||||
["₃", "3"],
|
||||
["ₒ", "o"],
|
||||
["ℏ", "ħ"],
|
||||
["ℓ", "l"],
|
||||
["⇛", "⇒"],
|
||||
["∅", "Ø"],
|
||||
["∆", "Δ"],
|
||||
["∇", "▽"],
|
||||
["∑", "Σ"],
|
||||
["−", "-"],
|
||||
["∘", "°"],
|
||||
["∙", "・"],
|
||||
["∼", "~"],
|
||||
["≪", "«"],
|
||||
["≫", "»"],
|
||||
["⊚", "◎"],
|
||||
["⋅", "・"],
|
||||
["⌢", "⌒"],
|
||||
["⍑", "〒"],
|
||||
["⍳", "ι"],
|
||||
["⍺", "α"],
|
||||
["⏜", "⌒"],
|
||||
["⏺", "●"],
|
||||
["Ⓘ", "I"],
|
||||
["Ⓞ", "O"],
|
||||
["Ⓡ", "R"],
|
||||
["⓪", "0"],
|
||||
["▢", "□"],
|
||||
["▪", "■"],
|
||||
["▲", "△"],
|
||||
["▵", "△"],
|
||||
["▼", "▽"],
|
||||
["▿", "▽"],
|
||||
["◉", "◎"],
|
||||
["◊", "◇"],
|
||||
["◠", "⌒"],
|
||||
["◯", "○"],
|
||||
["◻", "□"],
|
||||
["◼", "■"],
|
||||
["☐", "□"],
|
||||
["♢", "◇"],
|
||||
["♦", "◆"],
|
||||
["♩", "♪"],
|
||||
["♫", "♪"],
|
||||
["⚪", "○"],
|
||||
["⚫", "●"],
|
||||
["✕", "×"],
|
||||
["✖", "×"],
|
||||
["✘", "×"],
|
||||
["✝", "†"],
|
||||
["✞", "†"],
|
||||
["✩", "☆"],
|
||||
["✫", "★"],
|
||||
["✬", "★"],
|
||||
["✭", "★"],
|
||||
["✯", "★"],
|
||||
["✰", "☆"],
|
||||
["➡", "→"],
|
||||
["⟶", "→"],
|
||||
["⟹", "⇒"],
|
||||
["⦾", "◎"],
|
||||
["⦿", "◎"],
|
||||
["⨯", "×"],
|
||||
["⬆", "↑"],
|
||||
["⬇", "↓"],
|
||||
["⬛", "■"],
|
||||
["⬜", "□"],
|
||||
["⬤", "●"],
|
||||
["⬦", "◇"],
|
||||
["⭐", "★"],
|
||||
["⭑", "★"],
|
||||
["⭗", "◎"],
|
||||
["\u3000", " "],
|
||||
["〃", "”"],
|
||||
["《", "«"],
|
||||
["》", "»"],
|
||||
["『", "「"],
|
||||
["』", "」"],
|
||||
["〜", "~"],
|
||||
["ゞ", "ゝ"],
|
||||
["㊦", "下"],
|
||||
["㋛", "シ"],
|
||||
["︵", "("],
|
||||
["﹁", "「"],
|
||||
["﹒", "."],
|
||||
["﹤", "<"],
|
||||
["﹥", ">"],
|
||||
["0", "0"],
|
||||
["1", "1"],
|
||||
["3", "3"],
|
||||
["8", "8"],
|
||||
["C", "C"],
|
||||
["O", "O"],
|
||||
["`", "`"],
|
||||
["。", "。"],
|
||||
["「", "「"],
|
||||
["」", "」"],
|
||||
["、", "、"],
|
||||
["・", "・"],
|
||||
["ア", "ア"],
|
||||
["カ", "カ"],
|
||||
["シ", "シ"],
|
||||
["ソ", "ソ"],
|
||||
["タ", "タ"],
|
||||
["チ", "チ"],
|
||||
["ヒ", "ヒ"],
|
||||
["メ", "メ"],
|
||||
["モ", "モ"],
|
||||
["¢", "¢"],
|
||||
["𝐅", "F"],
|
||||
["𝐌", "M"],
|
||||
["𝐚", "a"],
|
||||
["𝐜", "c"],
|
||||
["𝐞", "e"],
|
||||
["𝐫", "r"],
|
||||
["𝐬", "s"],
|
||||
["𝑓", "ƒ"],
|
||||
["𝑥", "x"],
|
||||
["𝒂", "a"],
|
||||
["𝒇", "f"],
|
||||
["𝒐", "o"],
|
||||
["𝒕", "t"],
|
||||
["𝓒", "C"],
|
||||
["𝓔", "E"],
|
||||
["𝓜", "M"],
|
||||
["𝓦", "W"],
|
||||
["𝓨", "Y"],
|
||||
["𝓪", "a"],
|
||||
["𝓮", "e"],
|
||||
["𝓱", "h"],
|
||||
["𝓲", "i"],
|
||||
["𝓴", "k"],
|
||||
["𝓸", "o"],
|
||||
["𝓽", "t"],
|
||||
["𝕆", "O"],
|
||||
["𝕖", "e"],
|
||||
["𝕟", "n"],
|
||||
["𝕣", "r"],
|
||||
["𝘢", "a"],
|
||||
["𝘧", "f"],
|
||||
["𝘮", "m"],
|
||||
["𝘳", "r"],
|
||||
["𝘵", "t"],
|
||||
["𝙼", "M"],
|
||||
["𝛂", "α"],
|
||||
["𝜁", "ζ"],
|
||||
["𝜄", "ι"],
|
||||
["𝜶", "α"],
|
||||
["𝝰", "α"],
|
||||
["𝝸", "ι"],
|
||||
["𝞶", "ν"],
|
||||
["🌟", "★"],
|
||||
["🎵", "♪"],
|
||||
["🎶", "♪"],
|
||||
["🔺", "△"],
|
||||
["🔻", "▽"],
|
||||
]);
|
||||
|
||||
const NAME_MAX_LENGTH = 10;
|
||||
|
||||
const INVISIBLE_CHARACTER_REGEXP = /^(?:\p{Cf}|\p{Variation_Selector})$/u;
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
const rows = await db
|
||||
.selectFrom("User")
|
||||
.select([sql<number>`rowid`.as("rowId"), "inGameName"])
|
||||
.where("inGameName", "is not", null)
|
||||
.execute();
|
||||
|
||||
for (const row of rows) {
|
||||
const replaced = withoutOverflowingTrailingSpaces(
|
||||
[...row.inGameName]
|
||||
.filter((character) => !INVISIBLE_CHARACTER_REGEXP.test(character))
|
||||
.map((character) => REPLACEMENTS.get(character) ?? character)
|
||||
.join(""),
|
||||
);
|
||||
if (replaced === row.inGameName) continue;
|
||||
|
||||
await db
|
||||
.updateTable("User")
|
||||
.set({ inGameName: replaced })
|
||||
.where(sql`rowid`, "=", row.rowId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
function withoutOverflowingTrailingSpaces(inGameName: string) {
|
||||
const discriminatorIndex = inGameName.lastIndexOf("#");
|
||||
if (discriminatorIndex === -1) return inGameName;
|
||||
|
||||
const name = inGameName.slice(0, discriminatorIndex);
|
||||
if ([...name].length <= NAME_MAX_LENGTH) return inGameName;
|
||||
|
||||
return `${name.trimEnd()}${inGameName.slice(discriminatorIndex)}`;
|
||||
}
|
||||
@@ -106,14 +106,13 @@ const NAME_GREEK = "ια"; // ι: "Rιppιng_H", α: "◇Dαrz™" (special-symb
|
||||
* The rest of the in-game name editor's symbol pickers (sendou.ink's
|
||||
* IN_GAME_NAME_CHARACTER_CATEGORIES "symbols" + "cjk-symbols"), minus what
|
||||
* nameCharset() already carries and chars the Blitz cmap doesn't map
|
||||
* (ˊˋ𝑓⁀⚪⚫◻◼⍑; nameSymbols() re-checks at build time). "•" stays out: BlitzMain's
|
||||
* (nameSymbols() checks at build time). "•" stays out: BlitzMain's
|
||||
* bullet is a 4px dot, the on-screen circle comes from "●" via RENDER_ALIASES.
|
||||
* Only the fullwidth "~" (U+FF5E), the form fixture labels attest — the wave
|
||||
* dash "〜" (U+301C) is a pixel-identical homoglyph that would duel it.
|
||||
* Scoreboard-names only (not death-tag) until attested, like NAME_GREEK.
|
||||
*/
|
||||
const NAME_SYMBOLS =
|
||||
"′‘’‚‛…″“”„←→↑↓⇒⇔˜€∞√∀⊂⊃∴∵∂№♭♀♂◎◇◆△▲▽▼†※™『』【】〈〉《》〔〕々〆〇〃~";
|
||||
const NAME_SYMBOLS = "′‘’…”‐―←→↑↓⇒⇔˜€ƒ∞√∀⊂⊃∴∵∂№♭♀♂◎□■◇◆△▽†※™【】々〆〒仝~";
|
||||
|
||||
/**
|
||||
* Render the key char but emit it as the value: in-game names show "•" as a
|
||||
|
||||
Reference in New Issue
Block a user