Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cfdddc0
feat(utf8): add character-semantic UTF-8 helpers + migration audit (#…
kvirund Aug 3, 2026
3ec73ba
feat(utf8): internal_encoding flag + route format_text via native_tex…
kvirund Aug 3, 2026
a35f164
feat(utf8): route pager next_page + string_add truncation via native_…
kvirund Aug 3, 2026
c1a4f88
feat(utf8): route str_cmp/strn_cmp case folding via native_text (#3681)
kvirund Aug 3, 2026
bfd9268
feat(utf8): make isname walk characters instead of bytes (#3681)
kvirund Aug 3, 2026
7339f6f
feat(utf8): character-aware argument splitting and name declension (#…
kvirund Aug 3, 2026
dfbd79e
test(utf8): regression tests for the byte-vs-character text routines …
kvirund Aug 3, 2026
f209945
feat(utf8): character-aware ctype scanning, closing track A2 (#3681)
kvirund Aug 3, 2026
d602bbe
feat(utf8): character-width column padding, first A3 subsystems (#3681)
kvirund Aug 3, 2026
8a479f5
feat(utf8): character-width columns across the remaining game screens…
kvirund Aug 3, 2026
e6757c4
feat(utf8): character-width columns in god commands, closing track A3…
kvirund Aug 3, 2026
dd4c646
Merge branch 'master' into kvirund/utf8-migration-plan-16b188
bylins Aug 3, 2026
5b81a4b
docs: fix the declared C++ standard (C++17 -> C++20)
kvirund Aug 4, 2026
feabc22
refactor(utf8): use fmt for column widths instead of the pad_right he…
kvirund Aug 4, 2026
b5243d1
feat(utf8): character iteration API + the case sites the first sweep …
kvirund Aug 4, 2026
d54e3b3
perf(utf8): make case folding cost what the byte loop cost (#3681)
kvirund Aug 4, 2026
bc18a06
feat(utf8): encoding-neutral Russian menu keys (A4 groundwork, #3681)
kvirund Aug 4, 2026
b80b9e9
feat(utf8): convert all Cyrillic menu keys to encoding-neutral consta…
kvirund Aug 4, 2026
ac9b843
fix(utf8): keep save-file names identical across the flip (C1a, #3681)
kvirund Aug 4, 2026
bda07da
ci(utf8): rehearse the encoding flip on every build (#3681)
kvirund Aug 4, 2026
52bfc9a
feat(utf8): funnel world text through the encoding boundary (#3681)
kvirund Aug 4, 2026
b0cf4a8
Merge branch 'master' into kvirund/utf8-migration-plan-16b188
kvirund Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,39 @@ jobs:
- name: Run tests
run: meson test -C build

utf8-flip:
name: GCC / UTF-8 flip rehearsal
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install base dependencies
uses: ./.github/actions/install-linux-deps
with:
add-universe: 'true'

# Репетиция флипа кодировки (issue #3681). Исходники и рантайм переключаются ВМЕСТЕ:
# блобы в git и так лежат в UTF-8, поэтому снятие working-tree-encoding + повторный
# checkout даёт UTF-8-исходники, а -Dinternal_encoding=utf8 переводит на символьную
# семантику рантайм. Задача держит UTF-8-ветку компилируемой и протестированной до
# самого флипа -- иначе она не собирается вообще ничем.
- name: Check out sources as UTF-8
run: |
sed -i -E 's|^(/(src\|tests)/\*\* +)working-tree-encoding=KOI8-R |\1|' .gitattributes
rm -rf src tests
git checkout -- src tests
file src/utils/utils.cpp | grep -q UTF-8 || { echo "sources did not become UTF-8"; exit 1; }

- name: Configure meson
run: meson setup build --wipe -Dbuild_profile=release -Dinternal_encoding=utf8 -Dunity_size=45

- name: Build
run: meson compile -C build tests

- name: Run tests
run: meson test -C build

gcc15:
name: GCC 15 / Base
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
При написании кода вы должны следовать правилу "одна команда - одна строка".
Желательно это же правило распространять и на объявления переменных.

Былины используют стандарт C++17.
Былины используют стандарт C++20 (задан в meson.build: cpp_std=c++20).

Тела конструкций if, if ... else for, while, do ... while всегда должны быть
заключены в символы '{', '}'.
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.31
0.1.32
7 changes: 7 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ if host_system == 'windows'
endif
endif

# Native runtime string encoding (issue #3681). Default koi8r keeps byte semantics unchanged.
if get_option('internal_encoding') == 'utf8'
project_args += '-DINTERNAL_ENCODING_UTF8'
endif

linker_choice = get_option('linker')
if linker_choice != ''
add_project_link_arguments('-fuse-ld=' + linker_choice, language: 'cpp')
Expand Down Expand Up @@ -640,6 +645,8 @@ main_sources = files(
'src/gameplay/mechanics/title.cpp',
'src/gameplay/statistics/top.cpp',
'src/utils/utils.cpp',
'src/utils/utf8.cpp',
'src/utils/native_text.cpp',
'src/utils/utils_encoding.cpp',
'src/gameplay/mechanics/weather.cpp',
'src/engine/olc/zedit.cpp',
Expand Down
4 changes: 4 additions & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ option('lua_formatter', type: 'boolean', value: true, description: 'Build the em
option('with_asan', type: 'boolean', value: false, description: 'Build with Address Sanitizer')
option('use_pch', type: 'boolean', value: true, description: 'Use precompiled headers for circle.library')

# KOI8-R -> UTF-8 migration (issue #3681). Selects the engine's native runtime string encoding.
# koi8r = current byte semantics (default); utf8 = character semantics via the utf8:: helpers.
option('internal_encoding', type: 'combo', choices: ['koi8r', 'utf8'], value: 'koi8r', description: 'Engine-native runtime string encoding: koi8r (byte semantics, current) or utf8 (character semantics)')

# Options for Admin API and Web features
option('admin_api', type: 'boolean', value: false, description: 'Enable Admin API and JSON-related features')

Expand Down
5 changes: 3 additions & 2 deletions src/administration/accounts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* 2018 (c) bodrich
*/
#include "accounts.h"
#include "utils/native_text.h"
#include "password.h"
#include "engine/entities/zone.h"
#include <sstream>
Expand Down Expand Up @@ -85,7 +86,7 @@ void Account::show_players(CharData *ch) {
ss << "Данные аккаунта: " << this->email << "\r\n";
for (auto &x : this->players_list) {
std::string name = GetNameByUnique(x);
name[0] = UPPER(name[0]);
native_text::capitalize_first(name);
ss << count << ") " << name << "\r\n";
count++;
}
Expand All @@ -101,7 +102,7 @@ void Account::list_players(DescriptorData *d) {
for (auto &x : this->players_list) {
std::string name = GetNameByUnique(x);
iosystem::write_to_output((std::to_string(count) + ") ").c_str(), d);
name[0] = UPPER(name[0]);
native_text::capitalize_first(name);
iosystem::write_to_output(name.c_str(), d);
iosystem::write_to_output("\r\n", d);
count++;
Expand Down
3 changes: 2 additions & 1 deletion src/engine/boot/boot_data_files.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "boot_data_files.h"
#include "utils/native_text.h"

#include "engine/db/obj_prototypes.h"
#include "engine/scripting/dg_olc.h"
Expand Down Expand Up @@ -397,7 +398,7 @@ void WorldFile::parse_room(int virtual_nr) {
world[room_realnum]->zone_rn = zone;
world[room_realnum]->vnum = virtual_nr;
std::string tmpstr = fread_string();
tmpstr[0] = UPPER(tmpstr[0]);
native_text::capitalize_first(tmpstr);
world[room_realnum]->set_name(tmpstr);
// if (zone_table[zone].RnumRoomsLocation.first == -1) {
// zone_table[zone].RnumRoomsLocation.first = room_realnum;
Expand Down
14 changes: 12 additions & 2 deletions src/engine/core/iosystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

#include "engine/core/iosystem.h"
#include <cstring>
#include <string_view>
#include "gameplay/core/experience.h"
#include "administration/privilege.h"
#include "utils/utils_encoding.h"
Expand Down Expand Up @@ -426,8 +428,16 @@ int process_input(DescriptorData *t) {
// Увы, это кое-что ломает, напр. wizhelp, или "г я использую zMUD"
if (t->state == EConState::kPlaying || (t->state == EConState::kExdesc)) {
if (t->keytable == kCodePageWinzZ || t->keytable == kCodePageWinzOld) {
if (*(write_point - 1) == 'z') {
*(write_point - 1) = 'я';
// Буква задана СТРОКОВЫМ литералом: он байт-прозрачен, поэтому один и тот
// же код верен и для KOI8-R (1 байт), и для UTF-8 (2 байта) -- в отличие от
// символьного литерала, который под UTF-8 не помещается в char (issue #3681).
static const std::string_view kYaLetter = "я";
if (*(write_point - 1) == 'z' && space_left + 1 >= kYaLetter.size()) {
--write_point;
++space_left;
std::memcpy(write_point, kYaLetter.data(), kYaLetter.size());
write_point += kYaLetter.size();
space_left -= kYaLetter.size();
}
}
}
Expand Down
13 changes: 5 additions & 8 deletions src/engine/db/db.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <filesystem>
#include "utils/native_text.h"
#include "gameplay/affects/affect_messages.h"
#include "gameplay/abilities/feats.h" // issue.perk-action-patching: BuildTalentPatchIndex
#include "utils/utils_encoding.h"
Expand Down Expand Up @@ -3595,7 +3596,7 @@ Rooms::~Rooms() {

int get_filename(const char *orig_name, char *filename, int mode) {
const char *prefix, *middle, *suffix;
char name[64], *ptr;
char name[64];

if (orig_name == nullptr || *orig_name == '\0' || filename == nullptr) {
log("SYSERR: NULL pointer or empty string passed to get_filename(), %p or %p.", orig_name, filename);
Expand Down Expand Up @@ -3630,13 +3631,9 @@ int get_filename(const char *orig_name, char *filename, int mode) {
default: return (0);
}

strcpy(name, orig_name);
for (ptr = name; *ptr; ptr++) {
if (*ptr == 'Ё' || *ptr == 'ё')
*ptr = '9';
else
*ptr = LOWER(codepages::AtoL(*ptr));
}
// Транслитерация вынесена в native_text: имя файла игрока обязано совпадать до и после
// смены кодировки, иначе сохранёнки перестанут находиться (issue #3681).
strcpy(name, native_text::translit_to_filename(orig_name).c_str());

switch (LOWER(*name)) {
case 'a':
Expand Down
3 changes: 2 additions & 1 deletion src/engine/db/obj_save.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// * AutoEQ by Burkhard Knopf <burkhard.knopf@informatik.tu-clausthal.de>

#include "engine/core/char_handler.h"
#include "utils/native_text.h"
#include "gameplay/mechanics/equipment.h"
#include "obj_save.h"
#include "gameplay/mechanics/groups.h"
Expand Down Expand Up @@ -1628,7 +1629,7 @@ int Crash_load(CharData *ch) {
}

std::string cap = obj->get_PName(grammar::ECase::kNom);
cap[0] = UPPER(cap[0]);
native_text::capitalize_first(cap);

// Предмет разваливается от старости
if (obj->get_timer() <= 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/engine/db/sqlite_world_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#ifdef HAVE_SQLITE

#include "sqlite_world_data_source.h"
#include "utils/native_text.h"
#include "utils/utils_encoding.h"
#include "db.h"
#include "obj_prototypes.h"
Expand Down Expand Up @@ -1256,7 +1257,7 @@ std::vector<LoadedRoom> SqliteWorldDataSource::LoadRooms(const std::vector<int>
auto room = new RoomData;
room->vnum = vnum;
// Apply UPPER to first character (same as Legacy loader)
if (!name.empty()) { name[0] = UPPER(name[0]); }
if (!name.empty()) { native_text::capitalize_first(name); }
room->set_name(name);

if (!description.empty())
Expand Down
8 changes: 5 additions & 3 deletions src/engine/db/yaml_world_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#ifdef HAVE_YAML

#include "yaml_world_data_source.h"
#include "utils/native_text.h"
#include "utils/utils_encoding.h"
#include "dictionary_loader.h"
#include "db.h"
Expand Down Expand Up @@ -700,8 +701,9 @@ std::string YamlWorldDataSource::GetText(const YAML::Node &node, const std::stri
{
if (node[key])
{
// YAML files are already in KOI8-R, no conversion needed
std::string text = node[key].as<std::string>();
// Файлы мира лежат на диске в KOI8-R; переводим в нативную кодировку движка
// (под KOI8-R это тождество, под UTF-8 - перекодировка). Issue #3681.
std::string text = native_text::from_koi8(node[key].as<std::string>());

// Convert line endings if configured for DOS format
if (m_convert_lf_to_crlf) {
Expand Down Expand Up @@ -1425,7 +1427,7 @@ RoomData* YamlWorldDataSource::ParseRoomNode(const YAML::Node &root, int vnum, i
room->zone_rn = zone_rnum;

std::string name = GetText(root, "name", "Untitled Room");
if (!name.empty()) { name[0] = UPPER(name[0]); }
if (!name.empty()) { native_text::capitalize_first(name); }
room->set_name(name);

std::string description = GetText(root, "description", "");
Expand Down
Loading
Loading