Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
130653b
feat(utf8): put the encoding boundary at XML load, not at each field …
kvirund Aug 5, 2026
09a15ee
feat(utf8): make the client encoding boundary work in both runtimes (…
kvirund Aug 5, 2026
97c73df
fix(utf8): spell the code-page tables as byte escapes, not string lit…
kvirund Aug 5, 2026
428fb73
feat(utf8): take every data source through the encoding boundary (C3,…
kvirund Aug 5, 2026
fedc324
fix(utf8): parse player names per character, not per byte (#3681)
kvirund Aug 5, 2026
fc05b29
fix(utf8): convert the player index when it is read (#3681)
kvirund Aug 5, 2026
366b4d2
fix(utf8): fold case per character in the player-name index (#3681)
kvirund Aug 5, 2026
406c623
fix(utf8): fold the name per character when building the player index…
kvirund Aug 5, 2026
9041513
feat: report the internal encoding, and keep the revision in containe…
kvirund Aug 6, 2026
152faca
feat(utf8): take boards and the changelog through the encoding bounda…
kvirund Aug 6, 2026
d559658
test(utf8): cover board loading, which had only a stub test (#3681)
kvirund Aug 6, 2026
65dcb22
feat(utf8): транслитерация вместо потери символа на границе KOI8-R (#…
kvirund Aug 6, 2026
33c8bc6
chore(version): свернуть вывод версии, кодировка -- в строку компилят…
kvirund Aug 6, 2026
8bb3e08
feat(utf8): нарядный экран приветствия для UTF-8-клиентов (#3681)
kvirund Aug 6, 2026
133b79f
fix(utf8): справка читается через границу кодировки (#3681)
kvirund Aug 6, 2026
d24336a
feat(utf8): одна шапка на все кодировки, деградирует через словарь (#…
kvirund Aug 6, 2026
6f0cd7c
fix(utf8): колонки в "закл все" считаются в символах (#3681)
kvirund Aug 6, 2026
70482d9
fix(utf8): без лигатуры в шапке -- 'ae' на символ длиннее и рамку пер…
kvirund Aug 6, 2026
e539e4a
fix(utf8): динамическая справка портилась на сортировке (#3681)
kvirund Aug 6, 2026
0593b9d
feat(utf8): карта рисуется псевдографикой (#3681)
kvirund Aug 6, 2026
6ed5fc2
fix(map): карта обрывалась на первой пустой строке
kvirund Aug 6, 2026
ccf0944
feat(map): по два начертания на клетку, символы вместо букв для посто…
kvirund Aug 6, 2026
ba5f1ea
revert(map): вернуть исходные символы карты
kvirund Aug 6, 2026
579def7
fix(utf8): конфиги перекодировались дважды и росли на каждой загрузке…
kvirund Aug 6, 2026
a4a89a7
test(utf8): цикл "загрузили - записали" для конфига закреплён тестом …
kvirund Aug 6, 2026
7eaaa0f
fix(utf8): запись на диск идёт в кодировке мира, а не в нативной (#3681)
kvirund Aug 7, 2026
43378a9
fix(utf8): имена файлов строятся из байтов KOI8-R, а не из нативных (…
kvirund Aug 7, 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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
/lib/cfg/** working-tree-encoding=KOI8-R eol=lf
/bin.win/** working-tree-encoding=windows-1251 eol=crlf
*.dll -diff -working-tree-encoding
/lib/text/greeting.utf8 working-tree-encoding=UTF-8 eol=lf
25 changes: 24 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ ARG WITH_OTEL=false
ARG WITH_ADMIN_API=false
ARG WITH_YAML=true
ARG WITH_SQLITE=false
# Кодировка рантайма (issue #3681). utf8 = флип: исходники перечекаучиваются в UTF-8
# (в git они и так UTF-8, KOI8-R даёт лишь working-tree-encoding), рантайм -- символьный.
ARG INTERNAL_ENCODING=koi8r
# Ревизия и счётчик коммитов: внутри контейнера git бесполезен (контекст может быть worktree,
# где .git -- файл-ссылка наружу, или распакованный архив), поэтому пробрасываем их снаружи:
# --build-arg GIT_REV=$(git rev-parse --short HEAD) \
# --build-arg GIT_COUNT=$(git rev-list --count HEAD)
ARG GIT_REV=
ARG GIT_COUNT=

RUN apk add --no-cache \
build-base make meson ninja git cmake samurai \
Expand Down Expand Up @@ -61,6 +70,19 @@ RUN if [ "$WITH_SQLITE" = "true" ]; then apk add --no-cache sqlite-dev; fi
WORKDIR /mud/mud
COPY . /mud/mud

# Флип кодировки исходников. Перекодируем дерево на месте, а не через git checkout: контекст
# сборки может быть worktree (там .git -- файл-ссылка) или вообще экспортированным архивом.
# Конвертируем только C++ -- .py в tests/ уже в UTF-8 (см. .gitattributes).
RUN if [ "$INTERNAL_ENCODING" = "utf8" ]; then \
find src tests -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
! -path 'src/third_party_libs/*' \
-exec sh -c 'iconv -f KOI8-R -t UTF-8 "$1" > "$1.u8" && mv "$1.u8" "$1"' _ {} \; && \
file src/utils/utils.cpp | grep -q UTF-8 ; \
fi

ENV BYLINS_GIT_REV=${GIT_REV} \
BYLINS_COMMIT_COUNT=${GIT_COUNT}

RUN OTEL_OPT=$([ "$WITH_OTEL" = "true" ] && echo system || echo disabled); \
ADMIN_OPT=$([ "$WITH_ADMIN_API" = "true" ] && echo true || echo false); \
YAML_OPT=$([ "$WITH_YAML" = "true" ] && echo builtin || echo disabled); \
Expand All @@ -69,7 +91,8 @@ RUN OTEL_OPT=$([ "$WITH_OTEL" = "true" ] && echo system || echo disabled); \
-Dbuild_tests=false -Dbuild_profile=${BUILD_TYPE} \
--unity=on -Dunity_size=45 \
-Dadmin_api=${ADMIN_OPT} -Dotel=${OTEL_OPT} \
-Dyaml=${YAML_OPT} -Dsqlite=${SQLITE_OPT} && \
-Dyaml=${YAML_OPT} -Dsqlite=${SQLITE_OPT} \
-Dinternal_encoding=${INTERNAL_ENCODING} && \
meson compile -C build circle:executable

# ───────────────────────── Этап 2: рантайм ─────────────────────────
Expand Down
22 changes: 22 additions & 0 deletions lib/text/greeting.utf8
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

╭──────────────────────────────────────────────────────────────────────────╮
│ Based on CircleMUD v3.0 — created by Jeremy Elson │
│ DikuMUD Gamma 0.0 — Sebastian Hammer, Michael Seifert, │
│ Hans Henrik Staerfeldt, Tom Madsen, Katja Nyboe │
╰──────────────────────────────────────────────────────────────────────────╯

███████
██
██ ████ ████ ████ ████ ████ ████ ████ ████ ████
███████ ██████ ██ ██ ██ ██ ████ ██ ██ ██████ ██
██ ██ ██ ██ ██ ██ ██ ████ ██ ████████ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██
███████ ███████ ████ ███ ███ ████ ████ ████ ████ ███████ ████

✦ ПО ПРЕДАНИЯМ РУССКИХ НАРОДНЫХ СКАЗОК И БЫЛИН ✦

По административным вопросам — Стрибог ‹stribog@bylins.su›
Сайт: www.bylins.su
Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики

Введите имя персонажа (или «новый», чтобы создать нового):
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ main_sources = files(
'src/utils/utf8.cpp',
'src/utils/native_text.cpp',
'src/utils/utils_encoding.cpp',
'src/utils/translit_koi8.cpp',
'src/gameplay/mechanics/weather.cpp',
'src/engine/olc/zedit.cpp',
'src/utils/utils_string.cpp',
Expand Down
8 changes: 7 additions & 1 deletion src/administration/names.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* $Revision$ *
************************************************************************ */

#include "utils/native_text.h"
#include "names.h"
#include "utils/grammar/gender.h"

Expand Down Expand Up @@ -169,9 +170,14 @@ static void NewNames::save() {
return;
}

// Граница записи: список уходит на диск в кодировке мира (сейчас KOI8-R), зеркально чтению
// (issue #3681).
std::ostringstream out;
for (NewNameListType::const_iterator it = NewNameList.begin(); it != NewNameList.end(); ++it)
file << it->first << "\n";
out << it->first << "\n";

const std::string on_disk = native_text::to_disk(out.str());
file.write(on_disk.data(), static_cast<std::streamsize>(on_disk.size()));
file.close();
}

Expand Down
3 changes: 3 additions & 0 deletions src/engine/boot/boot_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ enum SetStuffMode {
#define PLAYER_Z_PREFIX LIB_PLRS "" LIB_Z

#define HELP_PAGE_FILE LIB_TEXT_HELP "screen" // for HELP <CR>
// Rich UTF-8 login screen, sent verbatim to UTF-8 clients (issue #3681). Optional: when the
// file is absent the plain greeting from system_msg.xml is used instead.
#define GREETING_UTF8_FILE LIB_TEXT "greeting.utf8"

#define PROXY_FILE LIB_MISC "proxy" // register proxy list
#define XNAME_FILE LIB_MISC "xnames" // invalid name substrings
Expand Down
37 changes: 26 additions & 11 deletions src/engine/boot/boot_data_files.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class DataFile : public BaseDataFile {
void close() override;
[[nodiscard]] const std::string &file_name() const { return m_file_name; }
[[nodiscard]] const auto &file() const { return m_file; }
void get_one_line(char *buf);
void get_one_line(char *buf, std::size_t buf_size);

private:
FILE *m_file;
Expand All @@ -45,14 +45,26 @@ void DataFile::close() {
fclose(m_file);
}

void DataFile::get_one_line(char *buf) {
if (fgets(buf, READ_SIZE, file()) == nullptr) {
void DataFile::get_one_line(char *buf, std::size_t buf_size) {
char raw[READ_SIZE];
if (fgets(raw, sizeof(raw), file()) == nullptr) {
mudlog("SYSERR: error reading help file: not terminated with $?", DEF, kLvlImmortal, SYSLOG, true);
buf[0] = '$';
buf[1] = 0;
return;
}
buf[strlen(buf) - 1] = '\0'; // take off the trailing \n
size_t len = strlen(raw);
while (len > 0 && (raw[len - 1] == '\n' || raw[len - 1] == '\r')) {
raw[--len] = '\0'; // take off the trailing \r\n
}

// Граница кодировки (issue #3681): файлы справки лежат на диске в KOI8-R, а движок под
// UTF-8 ждёт нативный текст -- без этого ключи разделов не совпадали бы с тем, что ввёл
// игрок, а тело раздела уезжало бы клиенту байтами чужой кодировки. Строка может при этом
// вырасти (KOI8-R байт -> до трёх байт UTF-8), поэтому размер буфера передаётся явно.
const std::string native = native_text::from_disk_line(raw);
strncpy(buf, native.c_str(), buf_size - 1);
buf[buf_size - 1] = '\0';
}

class DiscreteFile : public DataFile {
Expand Down Expand Up @@ -1730,20 +1742,23 @@ class HelpFile : public DataFile {
};

bool HelpFile::load_help() {
// В нативной кодировке строка может быть втрое длиннее прочитанной с диска (KOI8-R байт
// разворачивается в UTF-8 максимум в три), поэтому буферы с запасом (issue #3681).
constexpr size_t kLineBufSize = 3 * READ_SIZE + 1;
#if defined(CIRCLE_MACINTOSH)
static char key[READ_SIZE + 1], next_key[READ_SIZE + 1], entry[32384]; // ?
static char key[kLineBufSize], next_key[kLineBufSize], entry[32384]; // ?
#else
char key[READ_SIZE + 1], next_key[READ_SIZE + 1], entry[32384];
char key[kLineBufSize], next_key[kLineBufSize], entry[32384];
#endif
char line[READ_SIZE + 1];
char line[kLineBufSize];
const char *scan;

// get the first keyword line
get_one_line(key);
get_one_line(key, sizeof(key));
while (*key != '$') // read in the corresponding help entry
{
snprintf(entry, sizeof(entry), "%s\r\n", key);
get_one_line(line);
get_one_line(line, sizeof(line));
while (*line != '#') {
// если вдруг файл внезапно закончился и '#' так и не встретился
// логаем ошибку и заканчиваем парсинг во избежание зацикливания
Expand All @@ -1755,7 +1770,7 @@ bool HelpFile::load_help() {
}
size_t entry_len = strlen(entry);
snprintf(entry + entry_len, sizeof(entry) - entry_len, "%s\r\n", line);
get_one_line(line);
get_one_line(line, sizeof(line));
}
// Assign read level
int min_level = 0;
Expand All @@ -1773,7 +1788,7 @@ bool HelpFile::load_help() {
}

// get next keyword line (or $)
get_one_line(key);
get_one_line(key, sizeof(key));
}

return true;
Expand Down
39 changes: 28 additions & 11 deletions src/engine/core/iosystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#include "engine/core/iosystem.h"
#include "utils/native_text.h"
#include <cstring>
#include <string_view>
#include "gameplay/core/experience.h"
Expand Down Expand Up @@ -431,7 +432,11 @@ int process_input(DescriptorData *t) {
// Буква задана СТРОКОВЫМ литералом: он байт-прозрачен, поэтому один и тот
// же код верен и для KOI8-R (1 байт), и для UTF-8 (2 байта) -- в отличие от
// символьного литерала, который под UTF-8 не помещается в char (issue #3681).
static const std::string_view kYaLetter = "я";
// В этой точке буфер содержит KOI8-R (таблицы легаси-кодировок выше отдают
// именно его), поэтому букву тоже берём в KOI8-R. Под KOI8-R-рантаймом
// to_koi8 - тождество, под UTF-8 - перекодировка литерала.
static const std::string kYaStorage = native_text::to_koi8("я");
const std::string_view kYaLetter = kYaStorage;
if (*(write_point - 1) == 'z' && space_left + 1 >= kYaLetter.size()) {
--write_point;
++space_left;
Expand All @@ -446,20 +451,32 @@ int process_input(DescriptorData *t) {

*write_point = '\0';

// Приводим собранную строку к нативной кодировке движка (issue #3681). После разбора
// выше в tmp лежит либо UTF-8 (клиент UTF-8), либо KOI8-R (все остальные кодировки
// клиентов - их таблицы отдают именно KOI8-R).
if (t->keytable == kCodePageUTF8) {
int i;
char utf8_tmp[kMaxSockBuf * 2 * 3];
size_t len_i, len_o;
if (!native_text::native_is_utf8()) {
int i;
char utf8_tmp[kMaxSockBuf * 2 * 3];
size_t len_i, len_o;

len_i = strlen(tmp);
len_i = strlen(tmp);

for (i = 0; i < kMaxSockBuf * 2 * 3; i++) {
utf8_tmp[i] = 0;
for (i = 0; i < kMaxSockBuf * 2 * 3; i++) {
utf8_tmp[i] = 0;
}
codepages::utf8_to_koi(tmp, utf8_tmp);
len_o = strlen(utf8_tmp);
strncpy(tmp, utf8_tmp, kMaxInputLength - 1);
space_left = space_left + len_i - len_o;
}
codepages::utf8_to_koi(tmp, utf8_tmp);
len_o = strlen(utf8_tmp);
strncpy(tmp, utf8_tmp, kMaxInputLength - 1);
space_left = space_left + len_i - len_o;
// иначе клиент прислал уже нативную кодировку - трогать нечего
} else if (native_text::native_is_utf8()) {
const size_t len_i = strlen(tmp);
const std::string native = native_text::from_koi8(tmp);
strncpy(tmp, native.c_str(), kMaxInputLength - 1);
tmp[kMaxInputLength - 1] = '\0';
space_left = space_left + len_i - strlen(tmp);
}

if ((space_left <= 0) && (ptr < nl_pos)) {
Expand Down
5 changes: 4 additions & 1 deletion src/engine/db/db.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,10 @@ void ZoneTrafficSave() {
}
void zone_traffic_load() {
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(ZONE_TRAFFIC_FILE);
// Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже
// буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество.
const std::string xml_db = native_text::read_data_file(ZONE_TRAFFIC_FILE);
pugi::xml_parse_result result = doc.load_buffer(xml_db.data(), xml_db.size());
if (!result) {
snprintf(buf, kMaxStringLength, "...%s", result.description());
mudlog(buf, CMP, kLvlImmortal, SYSLOG, true);
Expand Down
26 changes: 15 additions & 11 deletions src/engine/db/help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -586,12 +586,12 @@ std::string OutRecipiesHelp(ECharClass ch_class) {
utils::SortKoiString(skills_list);
for (auto it : skills_list) {
tmpstr = !(++columns % 2) ? "\r\n" : "\t";
out << "\t" << std::left << std::setw(30) << it << tmpstr;
out << "\t" << fmt::format("{:<30}", it) << tmpstr;
}
utils::SortKoiString(skills_list2);
for (auto it : skills_list2) {
tmpstr = !(++columns2 % 2) ? "\r\n" : "\t";
out2 << "\t" << "&C" << std::left << std::setw(30) << it << "&n" << tmpstr;
out2 << "\t" << "&C" << fmt::format("{:<30}", it) << "&n" << tmpstr;
}
if (out.str().back() == '\t')
out << "\r\n";
Expand Down Expand Up @@ -711,20 +711,20 @@ std::string OutSkillsHelp(ECharClass ch_class) {
}
}
if (num == 1) {
skills_list2.push_back(utils::SubstKtoW(skill.GetName()));
skills_list2.push_back(skill.GetName());
continue;
}
skills_list.push_back(utils::SubstKtoW(skill.GetName()));
skills_list.push_back(skill.GetName());
}
std::sort(skills_list.begin(), skills_list.end());
for (auto it : skills_list) {
tmpstr = !(++columns % 2) ? "\r\n" : "\t";
out << "\t" << std::left << std::setw(30) << utils::SubstWtoK(it) << tmpstr;
out << "\t" << fmt::format("{:<30}", it) << tmpstr;
}
std::sort(skills_list2.begin(), skills_list2.end());
for (auto it : skills_list2) {
tmpstr = !(++columns2 % 2) ? "\r\n" : "\t";
out2 << "\t" << "&C" << std::left << std::setw(30) << utils::SubstWtoK(it) << "&n" << tmpstr;
out2 << "\t" << "&C" << fmt::format("{:<30}", it) << "&n" << tmpstr;
}
if (out.str().back() == '\t')
out << "\r\n";
Expand Down Expand Up @@ -970,17 +970,17 @@ std::string OutFeatureHelp(ECharClass ch_class) {
utils::SortKoiString(feat_list);
for (auto it : feat_list) {
tmpstr = !(++columns % 2) ? "\r\n" : "\t";
out << "\t" << std::left << std::setw(30) << it << tmpstr;
out << "\t" << fmt::format("{:<30}", it) << tmpstr;
}
utils::SortKoiString(feat_list2);
for (auto it : feat_list2) {
tmpstr = !(++columns2 % 2) ? "\r\n" : "\t";
out2 << "\t" << "&C" << std::left << std::setw(30) << it << "&n" << tmpstr;
out2 << "\t" << "&C" << fmt::format("{:<30}", it) << "&n" << tmpstr;
}
utils::SortKoiString(feat_list3);
for (auto it : feat_list3) {
tmpstr = !(++columns3 % 2) ? "\r\n" : "\t";
out3 << "\t" << "&C" << std::left << std::setw(30) << it << "&n" << tmpstr;
out3 << "\t" << "&C" << fmt::format("{:<30}", it) << "&n" << tmpstr;
}
if (out.str().back() == '\t')
out << "\r\n";
Expand Down Expand Up @@ -1285,8 +1285,12 @@ void SetsHelp() {
str_out = "все";
else if (count_class == 0)
str_out = "никто";
if (str_out.back() == '\n')
str_out.back() = '\0';
// Именно pop_back(), а не запись '\0': присваивание оставляло NUL ВНУТРИ строки,
// не меняя её длины. Байтовая таблица это терпела, а libfort в режиме utf8_table
// на встроенном нуле уводит расчёт ширины в переполнение и роняет процесс.
if (str_out.back() == '\n') {
str_out.pop_back();
}
table << str_out << table_wrapper::kSeparator << table_wrapper::kEndRow;
}
table.SetColumnAlign(0, table_wrapper::align::kRight);
Expand Down
7 changes: 5 additions & 2 deletions src/engine/db/obj_save.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2093,7 +2093,10 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) {
Crash_delete_files(iplayer);
return false;
}
file.write(obj_content.data(), static_cast<std::streamsize>(obj_content.size()));
// Граница записи: рента уходит на диск в кодировке мира (сейчас KOI8-R), зеркально
// чтению -- иначе первое сохранение переводит файл в UTF-8 (issue #3681).
const std::string on_disk = native_text::to_disk(obj_content);
file.write(on_disk.data(), static_cast<std::streamsize>(on_disk.size()));
file.close();
#ifndef _WIN32
if (chmod(fname, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) < 0) {
Expand All @@ -2104,7 +2107,7 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) {
#endif
utils::CExecutionTimer crc_timer;
FileCRC::update_from_content(ch->get_uid(), FileCRC::kTextObjs,
obj_content.data(), obj_content.size());
on_disk.data(), on_disk.size());
crc_sec = crc_timer.delta().count();
g_obj_crash_save_hash[ch->get_uid()] = content_hash;
}
Expand Down
Loading
Loading