После перехода на UTF-8 (#3681) остался класс мест, где ширина колонки считается в байтах: форматы вида %-20.20s, %25s, %60s. Русская буква занимает два байта, поэтому такое поле ведёт себя не так, как задумано.
Два симптома
Колонка не выравнивается. Ширина без точности (%25s, %-20s) только дополняет строку до N байт. Русское слово из 12 символов — это 23 байта, дополнять нечем, поле переполняется, и всё, что правее, съезжает. Ровно это видно в выводе экипировки, где рядом стоят предметы с русскими и латинскими именами.
Строка рвётся посреди буквы. Ширина с точностью (%-20.20s) режет по N байт, и разрез попадает в середину двухбайтовой буквы:
'золотое кольцо' -> символов 14, байт 27
%-20.20s -> 'золотое ко' + половина буквы 'л'
Клиент получает битый байт и рисует «кракозябру» или проглатывает символ — в зависимости от клиента. Похоже, отсюда же жалоба Веледара на кривые строки.
Как это выглядит на живых данных
Меню магии в oedit (%-30.30s, четыре колонки) уходит клиенту так:
1) легкое исцелени� 2) серьезное исцел� 3) критическое исц� 4) исцеление
5) групповое исцел� 6) великое исцелен� 7) клич мощи 8) увеличить жизнь
А задумано так:
1) легкое исцеление 2) серьезное исцеление 3) критическое исцеление 4) исцеление
5) групповое исцеление 6) великое исцеление 7) клич мощи 8) увеличить жизнь
Оба дефекта в одной строке: длинные имена режутся посреди буквы, короткие добиваются до 30 байт (15 символов вместо 30) и колонка пляшет. До флипа всё было ровно: в KOI8-R байт равнялся символу.
По трём меню, где удалось взять реальные названия из lib/cfg/messages/ru:
| меню |
имён |
колонка пляшет |
обрезается |
разрез в середине буквы |
магия, %-30.30s |
248 |
186 |
62 |
60 |
умения, %-20.20s |
93 |
41 |
52 |
23 |
способности, %-20.20s |
156 |
30 |
126 |
86 |
Инвентаризация
Прошёл по всем форматам с шириной у %s (tools/audit_utf8_migration.py --category printf-width плюс свой проход):
|
мест |
| всего форматов с шириной |
99 |
из них do_toggle.cpp — %-3s под ON/OFF |
41 (не трогать, аргумент ASCII) |
| остальные |
58 |
| из них с русским аргументом (имя, описание, причина, метка) |
35 |
Разбивка по областям:
- OLC-меню —
oedit.cpp (14), medit.cpp (5), olc.cpp, zedit.cpp, redit.cpp, obj_sets_olc.cpp, dg_olc.cpp: %-20.20s и %25s под названия умений, заклинаний, способностей, типов зон. Видят билдеры и иммы, каждый день.
- Команды богов —
do_tabulate.cpp (6, %60s под имя предмета), do_stat.cpp (6), do_show.cpp (4), dg_scripts.cpp (4, имена и значения переменных триггеров).
- Игрокам —
im.cpp:1021 (%30s под название рецепта), modify.cpp:1014 (%30s под название заклинания), do_levels.cpp (звания), title.cpp:277 (%.*s при разборе титула).
- Списки банов —
ban.cpp:316/352/394, %-25.25s под имя и %-16.16s под причину: причина по-русски, режется по байтам.
Чем чинить
В движке уже есть готовое:
native_text::pad_right(std::string_view, std::size_t width) — дополнение до ширины в символах. Им уже пользуются: do_toggle.cpp для «краткий/полный», do_stat.cpp:625 (там и комментарий стоит: «Ширина поля -- в символах: printf меряет %-21s в байтах»).
native_text::char_offset(s, n) — байтовое смещение n-го символа, то есть s.substr(0, char_offset(s, n)) обрезает по символам, не разрывая букву.
То есть правка механическая: "%-20.20s" + аргумент → "%s" + native_text::pad_right(utils::RemoveColors? ...) с обрезкой через char_offset. Отдельно стоит решить, что делать с цветовыми кодами &G: они тоже занимают байты, но не занимают ширину, — там, где имя может быть цветным, ширину надо мерить после снятия кодов.
Что предлагаю
- OLC-меню — там бардак виден глазами и билдеры смотрят на него каждый день. Правка однотипная, хорошо ложится одним проходом по файлу.
- То, что видят игроки:
im.cpp, modify.cpp, do_levels.cpp, title.cpp.
- Списки банов и команды богов — последними.
do_toggle.cpp в список не входит: там 41 формат, но все под ON/OFF, это ASCII и работает верно. Трогать его не нужно, иначе диф раздуется на ровном месте.
Мелочь по инструменту
tools/audit_utf8_migration.py декодирует сниппеты из KOI8-R — после флипа он печатает вместо кода мусор. Сама выборка мест верная, читать неудобно: одну строку в нём поправить.
После перехода на UTF-8 (#3681) остался класс мест, где ширина колонки считается в байтах: форматы вида
%-20.20s,%25s,%60s. Русская буква занимает два байта, поэтому такое поле ведёт себя не так, как задумано.Два симптома
Колонка не выравнивается. Ширина без точности (
%25s,%-20s) только дополняет строку до N байт. Русское слово из 12 символов — это 23 байта, дополнять нечем, поле переполняется, и всё, что правее, съезжает. Ровно это видно в выводе экипировки, где рядом стоят предметы с русскими и латинскими именами.Строка рвётся посреди буквы. Ширина с точностью (
%-20.20s) режет по N байт, и разрез попадает в середину двухбайтовой буквы:Клиент получает битый байт и рисует «кракозябру» или проглатывает символ — в зависимости от клиента. Похоже, отсюда же жалоба Веледара на кривые строки.
Как это выглядит на живых данных
Меню магии в
oedit(%-30.30s, четыре колонки) уходит клиенту так:А задумано так:
Оба дефекта в одной строке: длинные имена режутся посреди буквы, короткие добиваются до 30 байт (15 символов вместо 30) и колонка пляшет. До флипа всё было ровно: в KOI8-R байт равнялся символу.
По трём меню, где удалось взять реальные названия из
lib/cfg/messages/ru:%-30.30s%-20.20s%-20.20sИнвентаризация
Прошёл по всем форматам с шириной у
%s(tools/audit_utf8_migration.py --category printf-widthплюс свой проход):do_toggle.cpp—%-3sподON/OFFРазбивка по областям:
oedit.cpp(14),medit.cpp(5),olc.cpp,zedit.cpp,redit.cpp,obj_sets_olc.cpp,dg_olc.cpp:%-20.20sи%25sпод названия умений, заклинаний, способностей, типов зон. Видят билдеры и иммы, каждый день.do_tabulate.cpp(6,%60sпод имя предмета),do_stat.cpp(6),do_show.cpp(4),dg_scripts.cpp(4, имена и значения переменных триггеров).im.cpp:1021(%30sпод название рецепта),modify.cpp:1014(%30sпод название заклинания),do_levels.cpp(звания),title.cpp:277(%.*sпри разборе титула).ban.cpp:316/352/394,%-25.25sпод имя и%-16.16sпод причину: причина по-русски, режется по байтам.Чем чинить
В движке уже есть готовое:
native_text::pad_right(std::string_view, std::size_t width)— дополнение до ширины в символах. Им уже пользуются:do_toggle.cppдля «краткий/полный»,do_stat.cpp:625(там и комментарий стоит: «Ширина поля -- в символах: printf меряет %-21s в байтах»).native_text::char_offset(s, n)— байтовое смещение n-го символа, то естьs.substr(0, char_offset(s, n))обрезает по символам, не разрывая букву.То есть правка механическая:
"%-20.20s"+ аргумент →"%s"+native_text::pad_right(utils::RemoveColors? ...)с обрезкой черезchar_offset. Отдельно стоит решить, что делать с цветовыми кодами&G: они тоже занимают байты, но не занимают ширину, — там, где имя может быть цветным, ширину надо мерить после снятия кодов.Что предлагаю
im.cpp,modify.cpp,do_levels.cpp,title.cpp.do_toggle.cppв список не входит: там 41 формат, но все подON/OFF, это ASCII и работает верно. Трогать его не нужно, иначе диф раздуется на ровном месте.Мелочь по инструменту
tools/audit_utf8_migration.pyдекодирует сниппеты из KOI8-R — после флипа он печатает вместо кода мусор. Сама выборка мест верная, читать неудобно: одну строку в нём поправить.