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.
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"),
- );
- }
- });
-});