From 9e9708468c50252db9b8f8a57be1d49b24e43646 Mon Sep 17 00:00:00 2001 From: Katokoda <79760461+Katokoda@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:44:37 +0200 Subject: [PATCH] Fix/nation names special caracters (#4195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4165 ## Description: This PR update the test checking validity of Nation Names to include the new character constraint explained below. It also fixes the 10 Nations that invalid characters (that did not render correctly on the map). **The new character constraint** According to testing, the game map renders correctly all safe Extended-ASCII characters (non colored in www.ascii-code.com = [0x20–0x7E] or [0xA0-0xFF]). Other characters, when present in Nation Names, are rendered correctly in the rest of the game but not on the map, where they are trimmed to the last byte, which is then interpreted as Extended-ASCII and rendered if possible. **How to quickly check my assertion** 1. Change the file resources/maps/world/manifest.json, renaming one of the countries to "a.á.आ!š!慢!". 2. Start a game on the world map without any bots 3. Verify that the nation name is well displayed in its overlay but is shown as "a.á.!a!b!" on the map. (characters before a point are preserved, but characters before an exclamation mark are missing/changed). 4. run `npm run test` and notice that the NationName test fails and lists the three non-valid characters. Explanation: The string is represented in UNICODE-16 as \u0061\u002e\u00e1\u002e\u0906\u0021\u0161\u0021\u6162\u0021. Which, when we keep only the right-most byte of each character gives: 61 2e e1 2e 06 21 61 21 62 21 And, converted in Extended-ASCII gives: a.á.�!a!b! (which matches the showed name if we discard the control character). **The 10 Nations which needed a fix** Utqiaġvik from the Bearing Strait. Ar Rayyān from the Strait of Hormuz. 6 Nations in the Bosphorus Straits. 2 Easter-egg Nations from Luna. The 8 real-world Nations were adapted by simply removing the diacritics (after confirmation from a speaker of arabic and turkish, but sadly none for the Utqiaġvik Nation). The Secret Base from Luna was renamed "T0Þ $e¢®ët Mi|¡tªr¥ ß@§£", all within Extended-ASCII, keeping the same spirit as the original name. However, the Monolith Nation (previously named ▊, without any flag) has changed quite a lot and needs some explanation. **Easter-egg Nation Monolith** The new name is "ΜΟΝΟʟΙȚΗ", which is entirely outside of the valid character zone but in a way that entirely disappears on the map (as the आ character in the example above). This means that on the map, the Nation has no name and only its Monolith-flag. However, in all other places (leaderboard, overlay, alliances, warnings, etc.) the name is displayed correctly. The included test excludes this precise name from its violation list. image The Monolith Nation without its name but with a Monolith flag. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: Katokoda --- map-generator/assets/maps/beringsea/info.json | 2 +- .../assets/maps/bosphorusstraits/info.json | 12 +-- map-generator/assets/maps/luna/info.json | 6 +- .../assets/maps/straitofhormuz/info.json | 2 +- resources/flags/custom/luna_monolith.svg | 8 ++ resources/maps/beringsea/manifest.json | 2 +- resources/maps/bosphorusstraits/manifest.json | 12 +-- resources/maps/luna/manifest.json | 6 +- resources/maps/straitofhormuz/manifest.json | 2 +- tests/NationName.test.ts | 78 +++++++++++++++++++ tests/NationNameLength.test.ts | 52 ------------- 11 files changed, 108 insertions(+), 74 deletions(-) create mode 100644 resources/flags/custom/luna_monolith.svg create mode 100644 tests/NationName.test.ts delete mode 100644 tests/NationNameLength.test.ts diff --git a/map-generator/assets/maps/beringsea/info.json b/map-generator/assets/maps/beringsea/info.json index 6707a6a43..1fe0e2f65 100644 --- a/map-generator/assets/maps/beringsea/info.json +++ b/map-generator/assets/maps/beringsea/info.json @@ -8,7 +8,7 @@ }, { "coordinates": [1725, 158], - "name": "Utqiaġvik", + "name": "Utqiagvik", "flag": "Alaska" }, { diff --git a/map-generator/assets/maps/bosphorusstraits/info.json b/map-generator/assets/maps/bosphorusstraits/info.json index 2cd6dddea..0d95175a7 100644 --- a/map-generator/assets/maps/bosphorusstraits/info.json +++ b/map-generator/assets/maps/bosphorusstraits/info.json @@ -33,7 +33,7 @@ }, { "coordinates": [534, 425], - "name": "Kadıköy", + "name": "Kadiköy", "flag": "tr" }, { @@ -78,27 +78,27 @@ }, { "coordinates": [459, 157], - "name": "Sarıyer", + "name": "Sariyer", "flag": "tr" }, { "coordinates": [477, 297], - "name": "Beşiktaş", + "name": "Besiktas", "flag": "tr" }, { "coordinates": [171, 379], - "name": "Avcılar", + "name": "Avcilar", "flag": "tr" }, { "coordinates": [308, 412], - "name": "Bakırköy", + "name": "Bakirköy", "flag": "tr" }, { "coordinates": [263, 283], - "name": "Başakşehir", + "name": "Basaksehir", "flag": "tr" }, { diff --git a/map-generator/assets/maps/luna/info.json b/map-generator/assets/maps/luna/info.json index 954369978..8c8168b8c 100644 --- a/map-generator/assets/maps/luna/info.json +++ b/map-generator/assets/maps/luna/info.json @@ -125,12 +125,12 @@ { "coordinates": [755, 3035], "flag": "", - "name": "T▆p░S▅cr▅t░M▊l▊t▅r▆░B▅s▅" + "name": "T0Þ $e¢®ët Mi|¡tªr¥ ß@§£" }, { "coordinates": [628, 921], - "flag": "", - "name": "▊" + "flag": "custom/luna_monolith", + "name": "ΜΟΝΟʟΙȚΗ" } ], "teamGameSpawnAreas": { diff --git a/map-generator/assets/maps/straitofhormuz/info.json b/map-generator/assets/maps/straitofhormuz/info.json index 27fdbecdc..42d4c172f 100644 --- a/map-generator/assets/maps/straitofhormuz/info.json +++ b/map-generator/assets/maps/straitofhormuz/info.json @@ -83,7 +83,7 @@ }, { "coordinates": [159, 756], - "name": "Ar Rayyān", + "name": "Ar Rayyan", "flag": "qa" }, { diff --git a/resources/flags/custom/luna_monolith.svg b/resources/flags/custom/luna_monolith.svg new file mode 100644 index 000000000..d188194c8 --- /dev/null +++ b/resources/flags/custom/luna_monolith.svg @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/resources/maps/beringsea/manifest.json b/resources/maps/beringsea/manifest.json index 7e9b3464a..0f4fcf2a2 100644 --- a/resources/maps/beringsea/manifest.json +++ b/resources/maps/beringsea/manifest.json @@ -24,7 +24,7 @@ { "coordinates": [1725, 158], "flag": "Alaska", - "name": "Utqiaġvik" + "name": "Utqiagvik" }, { "coordinates": [2152, 270], diff --git a/resources/maps/bosphorusstraits/manifest.json b/resources/maps/bosphorusstraits/manifest.json index db673d280..d3a3ce453 100644 --- a/resources/maps/bosphorusstraits/manifest.json +++ b/resources/maps/bosphorusstraits/manifest.json @@ -49,7 +49,7 @@ { "coordinates": [534, 425], "flag": "tr", - "name": "Kadıköy" + "name": "Kadiköy" }, { "coordinates": [559, 568], @@ -94,27 +94,27 @@ { "coordinates": [459, 157], "flag": "tr", - "name": "Sarıyer" + "name": "Sariyer" }, { "coordinates": [477, 297], "flag": "tr", - "name": "Beşiktaş" + "name": "Besiktas" }, { "coordinates": [171, 379], "flag": "tr", - "name": "Avcılar" + "name": "Avcilar" }, { "coordinates": [308, 412], "flag": "tr", - "name": "Bakırköy" + "name": "Bakirköy" }, { "coordinates": [263, 283], "flag": "tr", - "name": "Başakşehir" + "name": "Basaksehir" }, { "coordinates": [402, 272], diff --git a/resources/maps/luna/manifest.json b/resources/maps/luna/manifest.json index f86e0bacb..47d7f263e 100644 --- a/resources/maps/luna/manifest.json +++ b/resources/maps/luna/manifest.json @@ -134,12 +134,12 @@ { "coordinates": [755, 3035], "flag": "", - "name": "T▆p░S▅cr▅t░M▊l▊t▅r▆░B▅s▅" + "name": "T0Þ $e¢®ët Mi|¡tªr¥ ß@§£" }, { "coordinates": [628, 921], - "flag": "", - "name": "▊" + "flag": "custom/luna_monolith", + "name": "ΜΟΝΟʟΙȚΗ" } ], "teamGameSpawnAreas": { diff --git a/resources/maps/straitofhormuz/manifest.json b/resources/maps/straitofhormuz/manifest.json index 514aaf22e..406956896 100644 --- a/resources/maps/straitofhormuz/manifest.json +++ b/resources/maps/straitofhormuz/manifest.json @@ -99,7 +99,7 @@ { "coordinates": [159, 756], "flag": "qa", - "name": "Ar Rayyān" + "name": "Ar Rayyan" }, { "coordinates": [1103, 647], diff --git a/tests/NationName.test.ts b/tests/NationName.test.ts new file mode 100644 index 000000000..26ef3fe03 --- /dev/null +++ b/tests/NationName.test.ts @@ -0,0 +1,78 @@ +import fs from "fs"; +import { globSync } from "glob"; + +type Nation = { + name?: string; +}; + +type Manifest = { + nations?: Nation[]; +}; + +describe("Map manifests: nation name constraints", () => { + test("All nations' names must be ≤ 27 printable Extended-ASCII characters", () => { + const manifestPaths = globSync("resources/maps/**/manifest.json"); + + expect(manifestPaths.length).toBeGreaterThan(0); + + const violations: string[] = []; + + for (const manifestPath of manifestPaths) { + try { + const raw = fs.readFileSync(manifestPath, "utf8"); + const manifest = JSON.parse(raw) as Manifest; + + (manifest.nations ?? []).forEach((nation, idx) => { + const name = nation?.name; + if (typeof name !== "string") { + violations.push( + `${manifestPath} -> nations[${idx}].name is not a string`, + ); + return; + } + if (name.length > 27) { + violations.push( + `${manifestPath} -> nations[${idx}].name "${name}" has length ${name.length} (> 27)`, + ); + return; + } + if (name === "ΜΟΝΟʟΙȚΗ") { + // This exception handles the without-name easter-egg Nation in Luna. + // The MONOLITH nation have UNICODE characters that DO NOT render in the game-map. + // Precisely: each bytes of the UNICODE 16-bit code + // falls **outside** of the Extended-ASCII render-zone: [0x20–0x7E] and [0xA0-0xFF]. + // This magic trick makes its flag stand out, alone, over it's population count. + // However the name renders correctly in other texts (leaderboard, overlay, alliances, alerts, etc.). + return; + } + // Allow only printable safe-extended-ASCII characters + // within [0x20-0x7E] or [0xA0-0xFF], as in https://www.ascii-code.com/. + const excludededCharacters = [...name].filter( + (c) => + c.charCodeAt(0) < 0x20 || + (0x7e < c.charCodeAt(0) && c.charCodeAt(0) < 0xa0) || + 0xff < c.charCodeAt(0), + ); + if (0 < excludededCharacters.length) { + violations.push( + `${manifestPath} -> nations[${idx}].name "${name}" has ${excludededCharacters.length} non valid characters: ${excludededCharacters}`, + ); + return; + } + }); + } catch (err) { + violations.push( + `Failed to parse ${manifestPath}: ${(err as Error).message}`, + ); + } + } + + if (violations.length > 0) { + throw new Error( + "Nation name violations:\n" + + violations.join("\n") + + "\nAll characters must be within non-colored region of the Extended-ASCII table: https://www.ascii-code.com/", + ); + } + }); +}); diff --git a/tests/NationNameLength.test.ts b/tests/NationNameLength.test.ts deleted file mode 100644 index 9d7df858f..000000000 --- a/tests/NationNameLength.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from "fs"; -import { globSync } from "glob"; - -type Nation = { - name?: string; -}; - -type Manifest = { - nations?: Nation[]; -}; - -describe("Map manifests: nation name length constraint", () => { - test("All nations' names must be ≤ 27 characters", () => { - const manifestPaths = globSync("resources/maps/**/manifest.json"); - - expect(manifestPaths.length).toBeGreaterThan(0); - - const violations: string[] = []; - - for (const manifestPath of manifestPaths) { - try { - const raw = fs.readFileSync(manifestPath, "utf8"); - const manifest = JSON.parse(raw) as Manifest; - - (manifest.nations ?? []).forEach((nation, idx) => { - const name = nation?.name; - if (typeof name !== "string") { - violations.push( - `${manifestPath} -> nations[${idx}].name is not a string`, - ); - return; - } - if (name.length > 27) { - violations.push( - `${manifestPath} -> nations[${idx}].name "${name}" has length ${name.length} (> 27)`, - ); - } - }); - } catch (err) { - violations.push( - `Failed to parse ${manifestPath}: ${(err as Error).message}`, - ); - } - } - - if (violations.length > 0) { - throw new Error( - "Nation name length violations:\n" + violations.join("\n"), - ); - } - }); -});