From 5cdc44fe06dd360bce2f87407707381ae17718b4 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 02:55:16 +0200 Subject: [PATCH 001/110] feat(utf8): add character-semantic UTF-8 helpers + migration audit (#3681) First step of the KOI8-R -> UTF-8 migration (track A0). Adds an encoding-agnostic UTF-8 layer that later turns byte semantics into character semantics -- no runtime behaviour change yet; nothing in the engine calls it. - src/utils/utf8.{h,cpp}: decode/encode, is_valid, length, byte_offset, char_at, substr and ASCII+Cyrillic (incl. Yo) case folding. Code-point based, no external deps (no iconv/ICU); ASCII passes through unchanged. - tests/utf8.cpp: 13 GTest cases (empty, incomplete sequences, 4-byte, BOM, overlong, surrogates, range edges, Cyrillic length/substr/case folding). - tools/audit_utf8_migration.py: triage of byte-vs-char sites, categorised and prioritised by files that contain Russian text. New sources are pure ASCII (UTF-8 test data spelled as \xNN escapes), so they satisfy the KOI8-R working-tree attribute unchanged. --- meson.build | 1 + src/utils/utf8.cpp | 257 ++++++++++++++++++++++++++++++++++ src/utils/utf8.h | 68 +++++++++ tests/meson.build | 1 + tests/utf8.cpp | 166 ++++++++++++++++++++++ tools/audit_utf8_migration.py | 182 ++++++++++++++++++++++++ 6 files changed, 675 insertions(+) create mode 100644 src/utils/utf8.cpp create mode 100644 src/utils/utf8.h create mode 100644 tests/utf8.cpp create mode 100755 tools/audit_utf8_migration.py diff --git a/meson.build b/meson.build index 6c15aae6a..cd41da304 100644 --- a/meson.build +++ b/meson.build @@ -645,6 +645,7 @@ main_sources = files( 'src/gameplay/mechanics/title.cpp', 'src/gameplay/statistics/top.cpp', 'src/utils/utils.cpp', + 'src/utils/utf8.cpp', 'src/utils/utils_encoding.cpp', 'src/gameplay/mechanics/weather.cpp', 'src/engine/olc/zedit.cpp', diff --git a/src/utils/utf8.cpp b/src/utils/utf8.cpp new file mode 100644 index 000000000..c50486e17 --- /dev/null +++ b/src/utils/utf8.cpp @@ -0,0 +1,257 @@ +/** +\file utf8.cpp - a part of the Bylins engine. +\brief Implementation of the character-semantic UTF-8 helpers declared in utf8.h (issue #3681). +*/ + +#include "utf8.h" + +namespace utf8 { + +namespace { + +// Result of decoding one position: the code point, the byte length consumed, and whether the +// sequence was well-formed. On a malformed byte, `valid` is false, `len` is 1 and `cp` is the +// raw byte -- so scanners always advance and lenient callers can pass the byte through untouched. +struct Decoded { + char32_t cp; + std::size_t len; + bool valid; +}; + +// Decode per the Unicode 3-7 grammar: the first continuation byte has a lead-specific range +// (which is what rejects overlong forms and surrogates), the rest are plain 0x80..0xBF. +Decoded decode_core(std::string_view s, std::size_t pos) { + const std::size_t n = s.size(); + const unsigned char c0 = static_cast(s[pos]); + if (c0 < 0x80) { + return {c0, 1, true}; + } + + int len = 0; + char32_t cp = 0; + unsigned char b1_lo = 0x80; + unsigned char b1_hi = 0xBF; + if (c0 >= 0xC2 && c0 <= 0xDF) { + len = 2; + cp = c0 & 0x1F; + } else if (c0 == 0xE0) { + len = 3; + cp = c0 & 0x0F; + b1_lo = 0xA0; + } else if (c0 >= 0xE1 && c0 <= 0xEC) { + len = 3; + cp = c0 & 0x0F; + } else if (c0 == 0xED) { + len = 3; + cp = c0 & 0x0F; + b1_hi = 0x9F; + } else if (c0 >= 0xEE && c0 <= 0xEF) { + len = 3; + cp = c0 & 0x0F; + } else if (c0 == 0xF0) { + len = 4; + cp = c0 & 0x07; + b1_lo = 0x90; + } else if (c0 >= 0xF1 && c0 <= 0xF3) { + len = 4; + cp = c0 & 0x07; + } else if (c0 == 0xF4) { + len = 4; + cp = c0 & 0x07; + b1_hi = 0x8F; + } else { + // 0xC0, 0xC1, 0xF5..0xFF or a stray continuation byte: cannot start a sequence. + return {c0, 1, false}; + } + + if (pos + static_cast(len) > n) { + return {c0, 1, false}; + } + + const unsigned char b1 = static_cast(s[pos + 1]); + if (b1 < b1_lo || b1 > b1_hi) { + return {c0, 1, false}; + } + cp = (cp << 6) | (b1 & 0x3F); + + for (int k = 2; k < len; ++k) { + const unsigned char b = static_cast(s[pos + static_cast(k)]); + if (b < 0x80 || b > 0xBF) { + return {c0, 1, false}; + } + cp = (cp << 6) | (b & 0x3F); + } + + return {cp, static_cast(len), true}; +} + +} // namespace + +int sequence_length(unsigned char c) { + if (c < 0x80) { + return 1; + } + if (c >= 0xC0 && c <= 0xDF) { + return 2; + } + if (c >= 0xE0 && c <= 0xEF) { + return 3; + } + if (c >= 0xF0 && c <= 0xF7) { + return 4; + } + // Continuation byte (0x80..0xBF) or an out-of-range lead (0xF8..0xFF): not a valid start. + return 1; +} + +std::size_t decode(std::string_view s, std::size_t pos, char32_t &cp) { + if (pos >= s.size()) { + cp = 0; + return 0; + } + const Decoded d = decode_core(s, pos); + cp = d.cp; + return d.len; +} + +std::size_t encode(char32_t cp, std::string &out) { + if (cp <= 0x7F) { + out.push_back(static_cast(cp)); + return 1; + } + if (cp <= 0x7FF) { + out.push_back(static_cast(0xC0 | (cp >> 6))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + return 2; + } + if (cp >= 0xD800 && cp <= 0xDFFF) { + return 0; // surrogate half: not a Unicode scalar value + } + if (cp <= 0xFFFF) { + out.push_back(static_cast(0xE0 | (cp >> 12))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + return 3; + } + if (cp <= 0x10FFFF) { + out.push_back(static_cast(0xF0 | (cp >> 18))); + out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + return 4; + } + return 0; +} + +bool is_valid(std::string_view s) { + std::size_t pos = 0; + const std::size_t n = s.size(); + while (pos < n) { + const Decoded d = decode_core(s, pos); + if (!d.valid) { + return false; + } + pos += d.len; + } + return true; +} + +std::size_t length(std::string_view s) { + std::size_t count = 0; + std::size_t pos = 0; + const std::size_t n = s.size(); + while (pos < n) { + pos += decode_core(s, pos).len; + ++count; + } + return count; +} + +std::size_t byte_offset(std::string_view s, std::size_t index) { + std::size_t pos = 0; + const std::size_t n = s.size(); + while (index > 0 && pos < n) { + pos += decode_core(s, pos).len; + --index; + } + return pos; +} + +std::string_view char_at(std::string_view s, std::size_t index) { + const std::size_t start = byte_offset(s, index); + if (start >= s.size()) { + return {}; + } + const std::size_t len = decode_core(s, start).len; + return s.substr(start, len); +} + +std::string substr(std::string_view s, std::size_t pos, std::size_t count) { + const std::size_t start = byte_offset(s, pos); + if (count == std::string_view::npos) { + return std::string(s.substr(start)); + } + const std::size_t stop = byte_offset(s, pos + count); + return std::string(s.substr(start, stop - start)); +} + +char32_t to_lower(char32_t cp) { + if (cp >= 'A' && cp <= 'Z') { + return cp + 0x20; + } + if (cp >= 0x0410 && cp <= 0x042F) { // U+0410..U+042F (upper) -> U+0430..U+044F (lower) + return cp + 0x20; + } + if (cp == 0x0401) { // U+0401 (Yo) -> U+0451 (yo) + return 0x0451; + } + return cp; +} + +char32_t to_upper(char32_t cp) { + if (cp >= 'a' && cp <= 'z') { + return cp - 0x20; + } + if (cp >= 0x0430 && cp <= 0x044F) { // U+0430..U+044F (lower) -> U+0410..U+042F (upper) + return cp - 0x20; + } + if (cp == 0x0451) { // U+0451 (yo) -> U+0401 (Yo) + return 0x0401; + } + return cp; +} + +namespace { + +// Shared body for the whole-string case folders: decode, fold each code point, re-encode. +// Malformed bytes (valid == false) are copied through verbatim so nothing is silently dropped. +std::string fold_string(std::string_view s, char32_t (*fold)(char32_t)) { + std::string out; + out.reserve(s.size()); + std::size_t pos = 0; + const std::size_t n = s.size(); + while (pos < n) { + const Decoded d = decode_core(s, pos); + if (d.valid) { + encode(fold(d.cp), out); + } else { + out.push_back(s[pos]); + } + pos += d.len; + } + return out; +} + +} // namespace + +std::string to_lower(std::string_view s) { + return fold_string(s, to_lower); +} + +std::string to_upper(std::string_view s) { + return fold_string(s, to_upper); +} + +} // namespace utf8 + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/utf8.h b/src/utils/utf8.h new file mode 100644 index 000000000..7991d4c04 --- /dev/null +++ b/src/utils/utf8.h @@ -0,0 +1,68 @@ +/** +\file utf8.h - a part of the Bylins engine. +\brief Character-semantic helpers over UTF-8 strings (issue #3681, "Plan Napoleon"). + +This is the encoding-agnostic building block for the KOI8-R -> UTF-8 migration: code that +must reason about *characters* (code points) rather than bytes -- length, substring, indexed +access, case folding -- lives here. Everything is plain UTF-8 in / UTF-8 out with no external +dependency (no iconv/ICU). ASCII passes through untouched, so the helpers are also correct for +pure-ASCII input regardless of the ambient encoding. + +Case folding covers exactly what the legacy a_ucc/a_lcc tables covered: ASCII A-Z and the +Russian Cyrillic block (U+0410..U+044F plus Yo, U+0401/U+0451). Any other code point passes +through unchanged. +*/ + +#ifndef BYLINS_SRC_UTILS_UTF8_H_ +#define BYLINS_SRC_UTILS_UTF8_H_ + +#include +#include +#include + +namespace utf8 { + +// Number of bytes the UTF-8 sequence starting with lead byte `c` claims to span. +// Returns 1 for ASCII and for any byte that cannot start a sequence, so a caller that +// advances by the result always makes forward progress. +int sequence_length(unsigned char c); + +// Decode the code point starting at byte position `pos` in `s`. +// Writes the code point (or the raw byte, on a malformed sequence) to `cp` and returns the +// number of bytes consumed: >=1 while `pos` is in range, 0 once `pos >= s.size()`. +// Malformed sequences never stall: they yield the single offending byte and a length of 1. +std::size_t decode(std::string_view s, std::size_t pos, char32_t &cp); + +// Append `cp` to `out` as UTF-8. Returns the number of bytes written, or 0 for a value that is +// not a valid Unicode scalar (surrogate half or > U+10FFFF), in which case `out` is untouched. +std::size_t encode(char32_t cp, std::string &out); + +// Strict, whole-string well-formedness check per the Unicode Table 3-7 byte-sequence grammar +// (rejects overlong forms, surrogates, code points above U+10FFFF and stray continuation bytes). +bool is_valid(std::string_view s); + +// Number of code points. Malformed bytes count as one code point each; never throws. +std::size_t length(std::string_view s); + +// Byte offset of the `index`-th code point, or `s.size()` when `index` is past the end. +std::size_t byte_offset(std::string_view s, std::size_t index); + +// The bytes of the `index`-th code point, as a view into `s`. Empty when `index` is out of range. +std::string_view char_at(std::string_view s, std::size_t index); + +// std::string::substr, but `pos`/`count` are counted in code points instead of bytes. +std::string substr(std::string_view s, std::size_t pos, std::size_t count = std::string_view::npos); + +// Single-code-point case folding (ASCII + Russian Cyrillic incl. Yo); other values pass through. +char32_t to_lower(char32_t cp); +char32_t to_upper(char32_t cp); + +// Whole-string case folding. Malformed bytes are copied through verbatim. +std::string to_lower(std::string_view s); +std::string to_upper(std::string_view s); + +} // namespace utf8 + +#endif // BYLINS_SRC_UTILS_UTF8_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/meson.build b/tests/meson.build index 3397d3251..e0df07fcd 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -43,6 +43,7 @@ test_sources = files( 'utils.string.cpp', 'utils.encoding.cpp', 'accounts.storage.cpp', + 'utf8.cpp', 'fight.penalties.cpp', 'bonus.command.parser.cpp', 'quested.cpp', diff --git a/tests/utf8.cpp b/tests/utf8.cpp new file mode 100644 index 000000000..d31f8be8f --- /dev/null +++ b/tests/utf8.cpp @@ -0,0 +1,166 @@ +// Unit tests for the character-semantic UTF-8 helpers (src/utils/utf8.*, issue #3681). +// +// This file is intentionally pure ASCII: every non-ASCII string is spelled out as explicit +// UTF-8 byte escapes so the test data is independent of the source file's ambient encoding +// (which is KOI8-R today and UTF-8 after the migration flip). Adjacent string literals are +// concatenated so an \xNN escape is never followed by a literal hex digit. + +#include "utils/utf8.h" + +#include + +#include +#include + +namespace { + +// "Privet" (Cyrillic) -- 6 letters, 12 bytes. +const char *const kPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; +// "PRIVET" (all upper) and "privet" (all lower). +const char *const kPrivetUpper = "\xD0\x9F\xD0\xA0\xD0\x98\xD0\x92\xD0\x95\xD0\xA2"; +const char *const kPrivetLower = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; + +std::string S(std::string_view v) { + return std::string(v); +} + +} // namespace + +TEST(Utf8, EmptyString) { + EXPECT_EQ(utf8::length(""), 0u); + EXPECT_TRUE(utf8::is_valid("")); + EXPECT_EQ(utf8::substr("", 0), ""); + EXPECT_EQ(utf8::substr("", 3, 5), ""); + EXPECT_EQ(S(utf8::char_at("", 0)), ""); + EXPECT_EQ(utf8::to_lower(std::string_view("")), ""); + EXPECT_EQ(utf8::byte_offset("", 4), 0u); +} + +TEST(Utf8, AsciiSemanticsMatchBytes) { + EXPECT_EQ(utf8::length("Hello"), 5u); + EXPECT_TRUE(utf8::is_valid("Hello, world!")); + EXPECT_EQ(utf8::to_lower(std::string_view("HeLLo")), "hello"); + EXPECT_EQ(utf8::to_upper(std::string_view("HeLLo")), "HELLO"); + EXPECT_EQ(utf8::substr("Hello", 1, 3), "ell"); + EXPECT_EQ(S(utf8::char_at("Hello", 1)), "e"); + EXPECT_EQ(S(utf8::char_at("Hello", 5)), ""); +} + +TEST(Utf8, CyrillicLengthIsCodePointsNotBytes) { + EXPECT_EQ(std::strlen(kPrivet), 12u); // sanity: the fixture really is 12 bytes + EXPECT_EQ(utf8::length(kPrivet), 6u); + EXPECT_TRUE(utf8::is_valid(kPrivet)); +} + +TEST(Utf8, CyrillicCaseFolding) { + EXPECT_EQ(utf8::to_lower(std::string_view(kPrivetUpper)), kPrivetLower); + EXPECT_EQ(utf8::to_upper(std::string_view(kPrivetLower)), kPrivetUpper); + // "Privet, Mir" -> "privet, mir" (mixed Cyrillic + ASCII punctuation). + const char *const mixed = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" ", " "\xD0\x9C\xD0\xB8\xD1\x80"; + const char *const mixed_lower = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" ", " "\xD0\xBC\xD0\xB8\xD1\x80"; + EXPECT_EQ(utf8::to_lower(std::string_view(mixed)), mixed_lower); +} + +TEST(Utf8, YoLetter) { + const char *const kYoUpper = "\xD0\x81"; // U+0401 (Yo) + const char *const kYoLower = "\xD1\x91"; // U+0451 (yo) + EXPECT_EQ(utf8::length(kYoUpper), 1u); + EXPECT_EQ(std::strlen(kYoUpper), 2u); + EXPECT_EQ(utf8::to_lower(std::string_view(kYoUpper)), kYoLower); + EXPECT_EQ(utf8::to_upper(std::string_view(kYoLower)), kYoUpper); + EXPECT_EQ(utf8::to_lower(0x0401u), 0x0451u); + EXPECT_EQ(utf8::to_upper(0x0451u), 0x0401u); +} + +TEST(Utf8, SubstrAndIndexOnCyrillic) { + // chars: [0]P [1]r [2]i [3]v [4]e [5]t + EXPECT_EQ(utf8::substr(kPrivet, 1, 3), "\xD1\x80\xD0\xB8\xD0\xB2"); // "riv" (chars 1..3) + EXPECT_EQ(utf8::substr(kPrivet, 4), "\xD0\xB5\xD1\x82"); // "et" to end + EXPECT_EQ(utf8::substr(kPrivet, 10), ""); // pos past end + EXPECT_EQ(S(utf8::char_at(kPrivet, 0)), "\xD0\x9F"); // char "P" + EXPECT_EQ(S(utf8::char_at(kPrivet, 5)), "\xD1\x82"); // char "t" + EXPECT_EQ(S(utf8::char_at(kPrivet, 6)), ""); // out of range + EXPECT_EQ(utf8::byte_offset(kPrivet, 2), 4u); + EXPECT_EQ(utf8::byte_offset(kPrivet, 6), 12u); + EXPECT_EQ(utf8::byte_offset(kPrivet, 100), 12u); +} + +TEST(Utf8, FourByteAndBom) { + const char *const kGrin = "\xF0\x9F\x98\x80"; // U+1F600 + EXPECT_EQ(utf8::length(kGrin), 1u); + EXPECT_EQ(std::strlen(kGrin), 4u); + EXPECT_TRUE(utf8::is_valid(kGrin)); + EXPECT_EQ(S(utf8::char_at(kGrin, 0)), kGrin); + EXPECT_EQ(utf8::sequence_length(0xF0), 4); + + const char *const kBom = "\xEF\xBB\xBF"; // U+FEFF + EXPECT_EQ(utf8::length(kBom), 1u); + EXPECT_TRUE(utf8::is_valid(kBom)); + char32_t cp = 0; + EXPECT_EQ(utf8::decode(kBom, 0, cp), 3u); + EXPECT_EQ(cp, 0xFEFFu); +} + +TEST(Utf8, SequenceLength) { + EXPECT_EQ(utf8::sequence_length(0x41), 1); // 'A' + EXPECT_EQ(utf8::sequence_length(0xD0), 2); + EXPECT_EQ(utf8::sequence_length(0xE0), 3); + EXPECT_EQ(utf8::sequence_length(0xF0), 4); + EXPECT_EQ(utf8::sequence_length(0x80), 1); // stray continuation + EXPECT_EQ(utf8::sequence_length(0xFF), 1); // out-of-range lead +} + +TEST(Utf8, DecodeBoundaries) { + char32_t cp = 0xABCD; + EXPECT_EQ(utf8::decode("", 0, cp), 0u); // empty + EXPECT_EQ(cp, 0u); + EXPECT_EQ(utf8::decode("A", 1, cp), 0u); // pos at end + EXPECT_EQ(utf8::decode("A", 0, cp), 1u); + EXPECT_EQ(cp, static_cast('A')); +} + +TEST(Utf8, EncodeRoundTrip) { + std::string out; + EXPECT_EQ(utf8::encode('A', out), 1u); + EXPECT_EQ(out, "A"); + out.clear(); + EXPECT_EQ(utf8::encode(0x041Fu, out), 2u); // U+041F (P) + EXPECT_EQ(out, "\xD0\x9F"); + out.clear(); + EXPECT_EQ(utf8::encode(0x1F600u, out), 4u); // U+1F600 + EXPECT_EQ(out, "\xF0\x9F\x98\x80"); + out.clear(); + EXPECT_EQ(utf8::encode(0xD800u, out), 0u); // surrogate rejected + EXPECT_TRUE(out.empty()); + EXPECT_EQ(utf8::encode(0x110000u, out), 0u); // above U+10FFFF + EXPECT_TRUE(out.empty()); +} + +TEST(Utf8, RejectsMalformed) { + EXPECT_FALSE(utf8::is_valid("\x80")); // lone continuation + EXPECT_FALSE(utf8::is_valid("\xD0")); // truncated 2-byte + EXPECT_FALSE(utf8::is_valid("Hi\xD0")); // truncated at end + EXPECT_FALSE(utf8::is_valid("\xC0\x80")); // overlong NUL (0xC0 lead) + EXPECT_FALSE(utf8::is_valid("\xC0\xAF")); // overlong '/' + EXPECT_FALSE(utf8::is_valid("\xE0\x80\xAF")); // overlong 3-byte + EXPECT_FALSE(utf8::is_valid("\xED\xA0\x80")); // U+D800 surrogate + EXPECT_FALSE(utf8::is_valid("\xF4\x90\x80\x80")); // U+110000, above range + EXPECT_FALSE(utf8::is_valid("\xF5\x80\x80\x80")); // 0xF5 lead +} + +TEST(Utf8, AcceptsRangeEdges) { + EXPECT_TRUE(utf8::is_valid("\xED\x9F\xBF")); // U+D7FF, just below surrogates + EXPECT_TRUE(utf8::is_valid("\xEE\x80\x80")); // U+E000, just above surrogates + EXPECT_TRUE(utf8::is_valid("\xF4\x8F\xBF\xBF")); // U+10FFFF, top of range + EXPECT_TRUE(utf8::is_valid("\xC2\x80")); // U+0080, smallest 2-byte +} + +TEST(Utf8, LenientCountingAndFolding) { + // Malformed bytes are counted as one code point each and passed through by the folders, + // so nothing is dropped when the helpers meet non-UTF-8 (e.g. legacy KOI8-R) data. + EXPECT_EQ(utf8::length("\x80\x80"), 2u); + EXPECT_EQ(utf8::to_lower(std::string_view("\x80\x80")), "\x80\x80"); + EXPECT_EQ(utf8::to_upper(std::string_view("A\xFF" "Z")), "A\xFF" "Z"); +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tools/audit_utf8_migration.py b/tools/audit_utf8_migration.py new file mode 100755 index 000000000..02cdd798c --- /dev/null +++ b/tools/audit_utf8_migration.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Audit byte-vs-char assumptions ahead of the KOI8-R -> UTF-8 migration (issue #3681). + +The engine currently assumes "1 byte == 1 character" in three shapes: length/size used as a +character count, fixed byte-offset access/truncation, and per-byte case/classification. Under +UTF-8 every one of those breaks on multibyte (Cyrillic) text. This script scans the C++ sources, +classifies the suspect call sites into categories, and *prioritises files that actually contain +non-ASCII (Russian) bytes* -- those are where a byte-vs-char bug is observable. + +It is a triage aid, not a linter: every hit needs a human to decide whether it means bytes +(fine) or characters (needs utf8::). Source files are KOI8-R today, so snippets are decoded from +KOI8-R for readable output. + +Usage: + tools/audit_utf8_migration.py [PATHS ...] # default: src + tools/audit_utf8_migration.py --full # list hits in ASCII-only files too + tools/audit_utf8_migration.py --category substr # only one category (repeatable) + tools/audit_utf8_migration.py --list-categories +""" + +import argparse +import os +import re +import sys + +# Each category: (regex over a latin-1-decoded line, one-line description). +# The regexes are deliberately broad -- false positives are cheap, missed sites are not. +CATEGORIES = { + "printf-width": ( + re.compile(r"%[-+ 0#]*(?:\d+|\*)(?:\.(?:\d+|\*))?s|%\.(?:\d+|\*)s"), + "printf width/precision on a string (%-20s, %.*s) -- pads/truncates by bytes", + ), + "substr": ( + re.compile(r"\.substr\s*\("), + "substr() -- byte offsets, can cut a code point in half", + ), + "rel-index": ( + re.compile(r"\[[^\]]*-\s*[123]\s*\]"), + "indexing relative to length (str[len-1]) -- last byte, not last character", + ), + "strchr-cyr": ( + re.compile(r'strchr\s*\(\s*"([^"]*)"'), + "strchr() over a literal that contains Cyrillic -- matches a byte, not a letter", + ), + "case-deref": ( + re.compile(r"\b(?:UPPER|LOWER)\s*\(\s*(?:\*|\w+\s*\[)"), + "UPPER/LOWER on a dereferenced/indexed byte -- mangles a UTF-8 lead byte", + ), + "fixed-copy": ( + re.compile(r"\b(?:strn?cpy|strncat)\s*\("), + "strcpy/strncpy/strncat into a fixed buffer -- byte length may split a character", + ), + "strlen": ( + re.compile(r"\bstrlen\s*\("), + "strlen() -- byte count; suspect only when used as a character/display count", + ), + "size-length": ( + re.compile(r"\.(?:size|length)\s*\(\s*\)"), + "std::string size()/length() -- byte count used as a character count", + ), +} + +# High-volume categories: reported in the summary, but only listed for Cyrillic-bearing files +# unless --full, to keep the output actionable. +HIGH_VOLUME = {"strlen", "size-length"} + +SOURCE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx") + + +def has_cyrillic(raw: bytes) -> bool: + """True if the file carries any high-bit byte (KOI8-R Russian text lives at >= 0x80).""" + return any(b >= 0x80 for b in raw) + + +def literal_has_high_byte(fragment: str) -> bool: + return any(ord(c) >= 0x80 for c in fragment) + + +def decode_snippet(line: str) -> str: + """`line` is latin-1 (bytes 1:1); re-render it from KOI8-R so Russian reads correctly.""" + return line.encode("latin-1", "replace").decode("koi8-r", "replace").rstrip() + + +def iter_source_files(paths): + for path in paths: + if os.path.isfile(path): + yield path + continue + for root, _dirs, files in os.walk(path): + if "third_party_libs" in root: + continue + for name in files: + if name.endswith(SOURCE_EXTENSIONS): + yield os.path.join(root, name) + + +def scan_file(path, wanted): + with open(path, "rb") as handle: + raw = handle.read() + cyrillic = has_cyrillic(raw) + text = raw.decode("latin-1") + hits = [] # (category, lineno, snippet) + for lineno, line in enumerate(text.splitlines(), 1): + for category, (pattern, _desc) in CATEGORIES.items(): + if category not in wanted: + continue + match = pattern.search(line) + if not match: + continue + if category == "strchr-cyr" and not literal_has_high_byte(match.group(1)): + continue + hits.append((category, lineno, decode_snippet(line))) + return cyrillic, hits + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Audit byte-vs-char sites for the UTF-8 migration.") + parser.add_argument("paths", nargs="*", default=["src"], help="files or directories (default: src)") + parser.add_argument("--category", action="append", dest="categories", + help="restrict to a category (repeatable); default: all") + parser.add_argument("--full", action="store_true", + help="also list hits in ASCII-only files and high-volume categories") + parser.add_argument("--list-categories", action="store_true", help="print category names and exit") + args = parser.parse_args(argv) + + if args.list_categories: + for name, (_re, desc) in CATEGORIES.items(): + print(f"{name:<14} {desc}") + return 0 + + wanted = set(args.categories) if args.categories else set(CATEGORIES) + unknown = wanted - set(CATEGORIES) + if unknown: + parser.error(f"unknown category: {', '.join(sorted(unknown))}") + + # counts[category] = [hits_in_cyrillic_files, hits_in_ascii_files] + counts = {name: [0, 0] for name in CATEGORIES} + listing = [] # (priority, path, category, lineno, snippet) + + for path in sorted(iter_source_files(args.paths)): + cyrillic, hits = scan_file(path, wanted) + for category, lineno, snippet in hits: + counts[category][0 if cyrillic else 1] += 1 + show = cyrillic or args.full + if category in HIGH_VOLUME and not args.full: + show = False + if show: + listing.append((0 if cyrillic else 1, path, category, lineno, snippet)) + + print("=" * 78) + print("byte-vs-char audit (Cyr = files with Russian text -> where bugs are observable)") + print("=" * 78) + print(f"{'category':<14}{'Cyr':>8}{'ASCII':>8} description") + print("-" * 78) + for name, (_re, desc) in CATEGORIES.items(): + if name not in wanted: + continue + c_cyr, c_ascii = counts[name] + flag = " [high-volume]" if name in HIGH_VOLUME else "" + print(f"{name:<14}{c_cyr:>8}{c_ascii:>8} {desc}{flag}") + print("-" * 78) + total_cyr = sum(c[0] for c in counts.values()) + total_ascii = sum(c[1] for c in counts.values()) + print(f"{'TOTAL':<14}{total_cyr:>8}{total_ascii:>8}") + print() + + listing.sort(key=lambda row: (row[0], row[1], row[3])) + current_file = None + for _priority, path, category, lineno, snippet in listing: + if path != current_file: + current_file = path + print(f"\n### {path}") + print(f" {lineno:>6} [{category}] {snippet}") + + if not args.full: + print("\n(high-volume categories and ASCII-only files hidden; re-run with --full)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 8c7b16fe6b1e77134e8f00dce86d970a050c7505 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 03:06:29 +0200 Subject: [PATCH 002/110] feat(utf8): internal_encoding flag + route format_text via native_text (#3681) Track A1 of the KOI8-R -> UTF-8 migration. Introduces the build-time `internal_encoding` switch and a native-encoding dispatch layer, then routes the text reformatter through it. No behaviour change on the default KOI8-R build (char_count == byte count, capitalize_first == UPPER of the first byte, truncate_offset == maxlen); the utf8 build gets code-point semantics. - meson_options.txt / meson.build: internal_encoding=koi8r|utf8, the utf8 value defines INTERNAL_ENCODING_UTF8. - src/utils/native_text.{h,cpp}: char_count / capitalize_first / truncate_offset with a byte-identical KOI8-R branch and a utf8:: branch, plus native_is_utf8(). - src/utils/utils.cpp: format_text() width accounting, first-letter capitalisation and maxlen truncation no longer assume 1 byte == 1 character. - tests/native_text.cpp: adaptive GTest suite branching on native_is_utf8(). --- meson.build | 6 +++ meson_options.txt | 4 ++ src/utils/native_text.cpp | 102 ++++++++++++++++++++++++++++++++++++++ src/utils/native_text.h | 50 +++++++++++++++++++ src/utils/utils.cpp | 9 ++-- tests/meson.build | 1 + tests/native_text.cpp | 72 +++++++++++++++++++++++++++ 7 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 src/utils/native_text.cpp create mode 100644 src/utils/native_text.h create mode 100644 tests/native_text.cpp diff --git a/meson.build b/meson.build index cd41da304..0327c0fe5 100644 --- a/meson.build +++ b/meson.build @@ -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') @@ -646,6 +651,7 @@ main_sources = files( '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', diff --git a/meson_options.txt b/meson_options.txt index bf8598bb5..f870dc113 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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') diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp new file mode 100644 index 000000000..aabc54166 --- /dev/null +++ b/src/utils/native_text.cpp @@ -0,0 +1,102 @@ +/** +\file native_text.cpp - a part of the Bylins engine. +\brief Native-encoding character helpers declared in native_text.h (issue #3681). + +Two implementations selected by the INTERNAL_ENCODING_UTF8 build macro. The KOI8-R branch is kept +byte-for-byte identical to the open-coded logic these helpers replace so that routing call sites +through them changes nothing until the encoding flip. +*/ + +#include "native_text.h" + +#ifdef INTERNAL_ENCODING_UTF8 +#include "utf8.h" +#else +#include "utils.h" // UPPER() / a_ucc() -- the KOI8-R case table +#endif + +#include + +namespace native_text { + +#ifdef INTERNAL_ENCODING_UTF8 + +bool native_is_utf8() { + return true; +} + +std::size_t char_count(const char *begin, const char *end) { + return utf8::length(std::string_view(begin, static_cast(end - begin))); +} + +std::size_t char_count(std::string_view s) { + return utf8::length(s); +} + +void capitalize_first(char *s) { + if (s == nullptr || *s == '\0') { + return; + } + const std::string_view sv(s); + char32_t cp = 0; + const std::size_t len = utf8::decode(sv, 0, cp); + if (len == 0) { + return; + } + const char32_t upper = utf8::to_upper(cp); + if (upper == cp) { + return; + } + std::string encoded; + if (utf8::encode(upper, encoded) == len) { + for (std::size_t i = 0; i < len; ++i) { + s[i] = encoded[i]; + } + } +} + +std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { + if (max_bytes >= s.size()) { + return s.size(); + } + std::size_t pos = 0; + while (true) { + char32_t cp = 0; + const std::size_t len = utf8::decode(s, pos, cp); + if (len == 0 || pos + len > max_bytes) { + break; + } + pos += len; + } + return pos; +} + +#else // KOI8-R: 1 byte == 1 character + +bool native_is_utf8() { + return false; +} + +std::size_t char_count(const char *begin, const char *end) { + return static_cast(end - begin); +} + +std::size_t char_count(std::string_view s) { + return s.size(); +} + +void capitalize_first(char *s) { + if (s != nullptr && *s != '\0') { + *s = static_cast(UPPER(static_cast(*s))); + } +} + +std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { + return max_bytes < s.size() ? max_bytes : s.size(); +} + +#endif + +} // namespace native_text + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/native_text.h b/src/utils/native_text.h new file mode 100644 index 000000000..f334a0bca --- /dev/null +++ b/src/utils/native_text.h @@ -0,0 +1,50 @@ +/** +\file native_text.h - a part of the Bylins engine. +\brief Character-semantic operations in the engine's *native runtime encoding* (issue #3681). + +The migration flips the engine's internal string encoding from KOI8-R (1 byte == 1 character) to +UTF-8 (multibyte). Code that must reason about characters -- counting display width, capitalising +a letter, truncating without splitting a character -- should go through this thin dispatch layer +instead of assuming bytes. The active encoding is chosen at build time by the `internal_encoding` +Meson option, which defines INTERNAL_ENCODING_UTF8 for the UTF-8 build: + + * KOI8-R (default, current behaviour): every helper is byte-for-byte identical to the open-coded + byte logic it replaces, so routing call sites through it is a no-op. + * UTF-8: helpers operate on code points via the utf8:: primitives. + +This lets the whole byte-vs-char refactor land and ship on KOI8-R (safely, as a no-op) ahead of +the encoding flip. Once the flip is permanent the KOI8-R branch and this indirection are removed. +*/ + +#ifndef BYLINS_SRC_UTILS_NATIVE_TEXT_H_ +#define BYLINS_SRC_UTILS_NATIVE_TEXT_H_ + +#include +#include + +namespace native_text { + +// True iff the engine was built with UTF-8 as its native runtime encoding. Reflects the flag the +// library itself was compiled with (not the translation unit that calls this), so callers/tests +// can branch reliably regardless of their own compile flags. +bool native_is_utf8(); + +// Number of display characters in a byte range / view. KOI8-R: the byte count. UTF-8: the number +// of code points (malformed bytes counted as one each, so it never stalls on legacy data). +std::size_t char_count(const char *begin, const char *end); +std::size_t char_count(std::string_view s); + +// Uppercase the first character of the null-terminated string `s` in place (ASCII + Russian +// Cyrillic incl. Yo). No-op on an empty string, on a non-cased first character, or in the (never +// occurring for these alphabets) case where the uppercase form has a different byte length. +void capitalize_first(char *s); + +// Largest byte offset <= max_bytes that lands on a character boundary, so cutting the string +// there never splits a multibyte character. KOI8-R: min(max_bytes, s.size()). +std::size_t truncate_offset(std::string_view s, std::size_t max_bytes); + +} // namespace native_text + +#endif // BYLINS_SRC_UTILS_NATIVE_TEXT_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 21371ef08..85c5a6c85 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -13,6 +13,7 @@ ************************************************************************ */ #include "utils.h" +#include "native_text.h" #include "utils/grammar/declensions.h" #include @@ -266,7 +267,7 @@ void format_text(const utils::AbstractStringWriter::shared_ptr &writer, flow++; } - if ((total_chars + (flow - start) + 1) > 79) { + if ((total_chars + native_text::char_count(start, flow) + 1) > 79) { strcpy(pos, "\r\n"); total_chars = 0; pos += 2; @@ -280,11 +281,11 @@ void format_text(const utils::AbstractStringWriter::shared_ptr &writer, } } - total_chars += flow - start; + total_chars += native_text::char_count(start, flow); strncpy(pos, start, flow - start); if (cap_next) { cap_next = false; - *pos = UPPER(*pos); + native_text::capitalize_first(pos); } pos += flow - start; } @@ -304,7 +305,7 @@ void format_text(const utils::AbstractStringWriter::shared_ptr &writer, strcpy(pos, "\r\n"); if (static_cast(pos - formatted) > maxlen) { - formatted[maxlen] = '\0'; + formatted[native_text::truncate_offset(formatted, maxlen)] = '\0'; } writer->set_string(formatted); } diff --git a/tests/meson.build b/tests/meson.build index e0df07fcd..3ece7787f 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -44,6 +44,7 @@ test_sources = files( 'utils.encoding.cpp', 'accounts.storage.cpp', 'utf8.cpp', + 'native_text.cpp', 'fight.penalties.cpp', 'bonus.command.parser.cpp', 'quested.cpp', diff --git a/tests/native_text.cpp b/tests/native_text.cpp new file mode 100644 index 000000000..f5cdefece --- /dev/null +++ b/tests/native_text.cpp @@ -0,0 +1,72 @@ +// Unit tests for the native-encoding character helpers (src/utils/native_text.*, issue #3681). +// +// Pure ASCII: non-ASCII fixtures are spelled as UTF-8 byte escapes. Expectations that differ +// between the KOI8-R and UTF-8 builds branch on native_text::native_is_utf8() (which reflects the +// flag the library was built with), so this one test file is correct under either build. + +#include "utils/native_text.h" + +#include + +#include +#include + +namespace { + +// "Privet": 6 Cyrillic code points, 12 UTF-8 bytes. +const char *const kPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; + +} // namespace + +TEST(NativeText, CharCountAscii) { + EXPECT_EQ(native_text::char_count("Hello"), 5u); + EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 4), native_text::native_is_utf8() ? 2u : 4u); +} + +TEST(NativeText, CharCountReflectsEncoding) { + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::char_count(kPrivet), 6u); + EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 12), 6u); + } else { + EXPECT_EQ(native_text::char_count(kPrivet), 12u); + EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 12), 12u); + } +} + +TEST(NativeText, CapitalizeAscii) { + char buf[] = "hello"; + native_text::capitalize_first(buf); + EXPECT_STREQ(buf, "Hello"); + + char empty[] = ""; + native_text::capitalize_first(empty); // must not touch the terminator + EXPECT_STREQ(empty, ""); + + char already[] = "X"; + native_text::capitalize_first(already); + EXPECT_STREQ(already, "X"); +} + +TEST(NativeText, CapitalizeCyrillicUtf8Only) { + if (!native_text::native_is_utf8()) { + GTEST_SKIP() << "Cyrillic-as-UTF-8 fixtures are only meaningful under the UTF-8 build"; + } + char buf[] = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; // "privet" + native_text::capitalize_first(buf); + EXPECT_STREQ(buf, "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); // "Privet" +} + +TEST(NativeText, TruncateOffset) { + const std::string_view p(kPrivet, 12); + EXPECT_EQ(native_text::truncate_offset(p, 100), 12u); // past end -> full size + EXPECT_EQ(native_text::truncate_offset(p, 0), 0u); + if (native_text::native_is_utf8()) { + // 5 bytes lands mid-character; back up to the boundary after 2 code points (4 bytes). + EXPECT_EQ(native_text::truncate_offset(p, 5), 4u); + EXPECT_EQ(native_text::truncate_offset(p, 4), 4u); + } else { + EXPECT_EQ(native_text::truncate_offset(p, 5), 5u); + } +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : From d3dcb5d943b1ef33a576377c8bcde62a61be98a5 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 04:08:21 +0200 Subject: [PATCH 003/110] feat(utf8): route pager next_page + string_add truncation via native_text (#3681) Continues track A1. next_page() now counts one column per character and steps over a multibyte character's trailing bytes; string_add()'s truncation points snap to a character boundary so a cut never splits a character. No-op under KOI8-R (char_bytes == 1, truncate_offset == the byte limit) -- byte-identical behaviour. - native_text: add char_bytes() (byte length of the character starting at a pointer). - src/engine/ui/modify.cpp: next_page() column counting; string_add() max_str/80 truncation offsets. - tests/native_text.cpp: char_bytes cases (ASCII, Cyrillic lead, 4-byte, truncated lead). --- src/engine/ui/modify.cpp | 18 ++++++++++++------ src/utils/native_text.cpp | 17 +++++++++++++++++ src/utils/native_text.h | 5 +++++ tests/native_text.cpp | 11 +++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/engine/ui/modify.cpp b/src/engine/ui/modify.cpp index 11fe7132e..ae3a00537 100644 --- a/src/engine/ui/modify.cpp +++ b/src/engine/ui/modify.cpp @@ -17,6 +17,7 @@ #include #include "modify.h" +#include "utils/native_text.h" #include "engine/olc/vedun/vedun.h" #include "interpreter.h" #include "engine/core/target_resolver.h" @@ -652,11 +653,11 @@ void string_add(DescriptorData *d, char *str) { if (!d->writer->get_string()) { if (strlen(str) + 3 > d->max_str) { SendMsgToChar("Слишком длинная строка - усечена.\r\n", d->character.get()); - strcpy(&str[d->max_str - 3], "\r\n"); + strcpy(&str[native_text::truncate_offset(str, d->max_str - 3)], "\r\n"); d->writer->set_string(str); } else if (EConState::kWriteMod == d->state && strlen(str) + 3 > 80) { SendMsgToChar("Слишком длинная строка - усечена.\r\n", d->character.get()); - str[80 - 3] = '\0'; + str[native_text::truncate_offset(str, 80 - 3)] = '\0'; d->writer->set_string(str); } else { d->writer->set_string(str); @@ -664,7 +665,7 @@ void string_add(DescriptorData *d, char *str) { } else { if (EConState::kWriteMod == d->state && strlen(str) + 3 > 80) { SendMsgToChar("Слишком длинная строка - усечена.\r\n", d->character.get()); - str[80 - 3] = '\0'; + str[native_text::truncate_offset(str, 80 - 3)] = '\0'; } if (strlen(str) + d->writer->length() + 3 > d->max_str) // \r\n\0 // @@ -1188,9 +1189,14 @@ char *next_page(char *str, CharData *ch) { // * We need to check here and see if we are over the page width, // * and if so, compensate by going to the begining of the next line. - else if ((ch)->player_specials->saved.stringLength && ++col > (ch)->player_specials->saved.stringLength) { - col = 1; - line++; + // * A multibyte character counts as one column; skip its trailing bytes so + // * they are not counted again (native_text::char_bytes == 1 under KOI8-R). + else if ((ch)->player_specials->saved.stringLength) { + if (++col > (ch)->player_specials->saved.stringLength) { + col = 1; + line++; + } + str += native_text::char_bytes(str) - 1; } } } diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index aabc54166..c3ceea1d8 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -71,6 +71,19 @@ std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { return pos; } +std::size_t char_bytes(const char *s) { + const unsigned char lead = static_cast(*s); + if (lead < 0x80) { + return 1; + } + const std::size_t want = static_cast(utf8::sequence_length(lead)); + std::size_t n = 1; + while (n < want && (static_cast(s[n]) & 0xC0) == 0x80) { + ++n; + } + return n; +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -95,6 +108,10 @@ std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { return max_bytes < s.size() ? max_bytes : s.size(); } +std::size_t char_bytes(const char *) { + return 1; +} + #endif } // namespace native_text diff --git a/src/utils/native_text.h b/src/utils/native_text.h index f334a0bca..0a2a49edf 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -43,6 +43,11 @@ void capitalize_first(char *s); // there never splits a multibyte character. KOI8-R: min(max_bytes, s.size()). std::size_t truncate_offset(std::string_view s, std::size_t max_bytes); +// Byte length of the character that starts at `s` (KOI8-R: 1), for stepping over a whole +// character byte-by-byte. Always >= 1; on a malformed/truncated UTF-8 lead it returns only the +// bytes actually present (never counts past a terminator or a non-continuation byte). +std::size_t char_bytes(const char *s); + } // namespace native_text #endif // BYLINS_SRC_UTILS_NATIVE_TEXT_H_ diff --git a/tests/native_text.cpp b/tests/native_text.cpp index f5cdefece..159da43f4 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -56,6 +56,17 @@ TEST(NativeText, CapitalizeCyrillicUtf8Only) { EXPECT_STREQ(buf, "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); // "Privet" } +TEST(NativeText, CharBytes) { + EXPECT_EQ(native_text::char_bytes("A"), 1u); + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::char_bytes(kPrivet), 2u); // Cyrillic lead -> 2 bytes + EXPECT_EQ(native_text::char_bytes("\xF0\x9F\x98\x80"), 4u); // 4-byte code point + EXPECT_EQ(native_text::char_bytes("\xD0"), 1u); // truncated lead: 1 byte present + } else { + EXPECT_EQ(native_text::char_bytes(kPrivet), 1u); // KOI8-R: every byte is a char + } +} + TEST(NativeText, TruncateOffset) { const std::string_view p(kPrivet, 12); EXPECT_EQ(native_text::truncate_offset(p, 100), 12u); // past end -> full size From 796fbe8d7420cefe7a7906b8443f9dc36251c4ff Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 04:27:56 +0200 Subject: [PATCH 004/110] feat(utf8): route str_cmp/strn_cmp case folding via native_text (#3681) Track A2. The case-insensitive comparison family (~600 str_cmp + ~67 strn_cmp call sites) now folds per character instead of per byte -- without touching a single call site. Under KOI8-R the original LOWER() byte loops are left verbatim and a guarded early return sends the UTF-8 build to the code-point fold, so the default build is byte-identical (28 added lines, 0 removed in utils_string.cpp). - native_text: add compare_ci / ncompare_ci (KOI8-R: byte-wise via a_lcc_table, preserving the magnitude callers propagate; UTF-8: code-point fold via utf8::). - native_text no longer includes utils.h -- it declares the two case tables directly, which also keeps the module standalone-testable. - tests: ASCII ordering/prefix/budget cases, Cyrillic case-insensitivity, and a reference test pinning the KOI8-R branch against the original LOWER() byte loop. --- src/utils/native_text.cpp | 88 +++++++++++++++++++++++++++++++++++++- src/utils/native_text.h | 9 ++++ src/utils/utils_string.cpp | 28 ++++++++++++ tests/native_text.cpp | 76 ++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index c3ceea1d8..2e2f7b52b 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -12,7 +12,10 @@ through them changes nothing until the encoding flip. #ifdef INTERNAL_ENCODING_UTF8 #include "utf8.h" #else -#include "utils.h" // UPPER() / a_ucc() -- the KOI8-R case table +// The KOI8-R case tables (defined in utils.cpp). Declared directly instead of including utils.h, +// which drags in fmt/ and much of the engine for what is just two 256-byte lookups. +extern const char a_ucc_table[]; +extern const char a_lcc_table[]; #endif #include @@ -84,6 +87,51 @@ std::size_t char_bytes(const char *s) { return n; } +namespace { + +// Shared driver for the two case-insensitive comparisons. `limit` caps how many bytes of `a` may +// be consumed (npos = unlimited): once that budget is spent the strings count as equal, which is +// what the strn_cmp callers -- who pass a prefix length in bytes -- expect. +int compare_folded(std::string_view a, std::string_view b, std::size_t limit) { + std::size_t pa = 0; + std::size_t pb = 0; + while (true) { + if (limit != std::string_view::npos && pa >= limit) { + return 0; + } + char32_t ca = 0; + char32_t cb = 0; + const std::size_t la = utf8::decode(a, pa, ca); + const std::size_t lb = utf8::decode(b, pb, cb); + if (la == 0 && lb == 0) { + return 0; + } + if (la == 0) { + return -1; + } + if (lb == 0) { + return 1; + } + const char32_t fa = utf8::to_lower(ca); + const char32_t fb = utf8::to_lower(cb); + if (fa != fb) { + return fa < fb ? -1 : 1; + } + pa += la; + pb += lb; + } +} + +} // namespace + +int compare_ci(std::string_view a, std::string_view b) { + return compare_folded(a, b, std::string_view::npos); +} + +int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { + return compare_folded(a, b, n); +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -100,7 +148,7 @@ std::size_t char_count(std::string_view s) { void capitalize_first(char *s) { if (s != nullptr && *s != '\0') { - *s = static_cast(UPPER(static_cast(*s))); + *s = a_ucc_table[static_cast(*s)]; } } @@ -112,6 +160,42 @@ std::size_t char_bytes(const char *) { return 1; } +namespace { + +// Byte-wise fold-and-subtract, identical to the open-coded `LOWER(a[i]) - LOWER(b[i])` loops in +// utils_string.cpp: the magnitude of the result (not just its sign) is preserved, since some +// callers propagate it. A string that ended compares as LOWER('\0') against the other's byte. +int compare_bytes(std::string_view a, std::string_view b, std::size_t limit) { + std::size_t i = 0; + while (true) { + if (limit != std::string_view::npos && i >= limit) { + return 0; + } + const bool a_end = i >= a.size(); + const bool b_end = i >= b.size(); + if (a_end && b_end) { + return 0; + } + const unsigned char ca = a_end ? '\0' : static_cast(a[i]); + const unsigned char cb = b_end ? '\0' : static_cast(b[i]); + const int chk = a_lcc_table[ca] - a_lcc_table[cb]; + if (chk != 0) { + return chk; + } + ++i; + } +} + +} // namespace + +int compare_ci(std::string_view a, std::string_view b) { + return compare_bytes(a, b, std::string_view::npos); +} + +int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { + return compare_bytes(a, b, n); +} + #endif } // namespace native_text diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 0a2a49edf..872c3d02b 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -48,6 +48,15 @@ std::size_t truncate_offset(std::string_view s, std::size_t max_bytes); // bytes actually present (never counts past a terminator or a non-continuation byte). std::size_t char_bytes(const char *s); +// Case-insensitive comparison in the native encoding: lexicographic over lowered characters, +// the shorter string orders first, returns the signed difference at the first mismatch (0 when +// equal). KOI8-R: per byte, via LOWER() -- matches str_cmp/str/str semantics. UTF-8: per code +// point, folded via utf8::to_lower (so the sign is meaningful; the magnitude is a code-point +// difference). In ncompare_ci, `n` is a byte budget -- the callers pass strlen()/length() -- and +// the comparison stops (as a match) once that many bytes of equal text have been consumed. +int compare_ci(std::string_view a, std::string_view b); +int ncompare_ci(std::string_view a, std::string_view b, std::size_t n); + } // namespace native_text #endif // BYLINS_SRC_UTILS_NATIVE_TEXT_H_ diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index cad0ed344..b61343431 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -1,6 +1,7 @@ //#include "utils_string.h" #include "utils.h" +#include "utils/native_text.h" #include "utils/utils_encoding.h" #include "gameplay/core/constants.h" @@ -579,12 +580,18 @@ char *delete_doubledollar(char *string) { } // Moved from utils.cpp +// The str_cmp/strn_cmp family folds case per *character*: under KOI8-R that is the original +// byte-wise LOWER() loop kept verbatim below, under UTF-8 it is native_text's code-point fold +// (issue #3681). The KOI8-R path is untouched so behaviour is bit-identical until the flip. int str_cmp(const char *arg1, const char *arg2) { int chk, i; if (arg1 == nullptr || arg2 == nullptr) { log("SYSERR: str_cmp() passed a nullptr pointer, %p or %p.", arg1, arg2); return (0); } + if (native_text::native_is_utf8()) { + return native_text::compare_ci(arg1, arg2); + } for (i = 0; arg1[i] || arg2[i]; i++) if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) return (chk); @@ -598,6 +605,9 @@ int str_cmp(const std::string &arg1, const char *arg2) { log("SYSERR: str_cmp() passed a NULL pointer, %p.", arg2); return (0); } + if (native_text::native_is_utf8()) { + return native_text::compare_ci(arg1, arg2); + } for (i = 0; i != arg1.length() && *arg2; i++, arg2++) if ((chk = LOWER(arg1[i]) - LOWER(*arg2)) != 0) return (chk); @@ -616,6 +626,9 @@ int str_cmp(const char *arg1, const std::string &arg2) { log("SYSERR: str_cmp() passed a NULL pointer, %p.", arg1); return (0); } + if (native_text::native_is_utf8()) { + return native_text::compare_ci(arg1, arg2); + } for (i = 0; *arg1 && i != arg2.length(); i++, arg1++) if ((chk = LOWER(*arg1) - LOWER(arg2[i])) != 0) return (chk); @@ -630,6 +643,9 @@ int str_cmp(const char *arg1, const std::string &arg2) { int str_cmp(const std::string &arg1, const std::string &arg2) { int chk; std::string::size_type i; + if (native_text::native_is_utf8()) { + return native_text::compare_ci(arg1, arg2); + } for (i = 0; i != arg1.length() && i != arg2.length(); i++) if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) return (chk); @@ -647,6 +663,9 @@ int strn_cmp(const char *arg1, const char *arg2, size_t n) { log("SYSERR: strn_cmp() passed a NULL pointer, %p or %p.", arg1, arg2); return (0); } + if (native_text::native_is_utf8()) { + return native_text::ncompare_ci(arg1, arg2, n); + } for (i = 0; (arg1[i] || arg2[i]) && (n > 0); i++, n--) if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) return (chk); @@ -660,6 +679,9 @@ int strn_cmp(const std::string &arg1, const char *arg2, size_t n) { log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg2); return (0); } + if (native_text::native_is_utf8()) { + return native_text::ncompare_ci(arg1, arg2, n); + } for (i = 0; i != arg1.length() && *arg2 && (n > 0); i++, arg2++, n--) if ((chk = LOWER(arg1[i]) - LOWER(*arg2)) != 0) return (chk); @@ -678,6 +700,9 @@ int strn_cmp(const char *arg1, const std::string &arg2, size_t n) { log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg1); return (0); } + if (native_text::native_is_utf8()) { + return native_text::ncompare_ci(arg1, arg2, n); + } for (i = 0; *arg1 && i != arg2.length() && (n > 0); i++, arg1++, n--) if ((chk = LOWER(*arg1) - LOWER(arg2[i])) != 0) return (chk); @@ -692,6 +717,9 @@ int strn_cmp(const char *arg1, const std::string &arg2, size_t n) { int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n) { int chk; std::string::size_type i; + if (native_text::native_is_utf8()) { + return native_text::ncompare_ci(arg1, arg2, n); + } for (i = 0; i != arg1.length() && i != arg2.length() && (n > 0); i++, n--) if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) return (chk); diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 159da43f4..bdf313691 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -18,6 +18,18 @@ const char *const kPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; } // namespace +// The legacy KOI8-R lowercase table (utils.cpp). Declared here rather than including utils.h, +// which pulls in fmt/ and the rest of the engine headers. +extern const char a_lcc_table[]; + +namespace { + +int legacy_lower(unsigned char c) { + return a_lcc_table[c]; +} + +} // namespace + TEST(NativeText, CharCountAscii) { EXPECT_EQ(native_text::char_count("Hello"), 5u); EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 4), native_text::native_is_utf8() ? 2u : 4u); @@ -80,4 +92,68 @@ TEST(NativeText, TruncateOffset) { } } +TEST(NativeText, CompareCiAscii) { + EXPECT_EQ(native_text::compare_ci("abc", "abc"), 0); + EXPECT_EQ(native_text::compare_ci("ABC", "abc"), 0); // case-insensitive + EXPECT_EQ(native_text::compare_ci("AbC", "aBc"), 0); + EXPECT_LT(native_text::compare_ci("abc", "abd"), 0); // ordering by first mismatch + EXPECT_GT(native_text::compare_ci("abd", "abc"), 0); + EXPECT_LT(native_text::compare_ci("ab", "abc"), 0); // prefix orders first + EXPECT_GT(native_text::compare_ci("abc", "ab"), 0); + EXPECT_EQ(native_text::compare_ci("", ""), 0); + EXPECT_LT(native_text::compare_ci("", "a"), 0); +} + +TEST(NativeText, NCompareCiAscii) { + EXPECT_EQ(native_text::ncompare_ci("abcdef", "abcXXX", 3), 0); // only first 3 bytes matter + EXPECT_NE(native_text::ncompare_ci("abcdef", "abXXXX", 3), 0); + EXPECT_EQ(native_text::ncompare_ci("ABC", "abc", 3), 0); + EXPECT_EQ(native_text::ncompare_ci("anything", "other", 0), 0); // zero budget: equal + EXPECT_LT(native_text::ncompare_ci("ab", "abc", 10), 0); // budget beyond the strings +} + +TEST(NativeText, CompareCiCyrillicIsCaseInsensitive) { + // "PRIVET" vs "privet" in Cyrillic: must compare equal in BOTH encodings -- under KOI8-R via + // the byte table, under UTF-8 via the code-point fold. This is the property that a naive + // "just compare bytes" UTF-8 migration would silently lose. + const char *const upper = "\xD0\x9F\xD0\xA0\xD0\x98\xD0\x92\xD0\x95\xD0\xA2"; + const char *const lower = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::compare_ci(upper, lower), 0); + EXPECT_EQ(native_text::compare_ci(upper, upper), 0); + EXPECT_NE(native_text::compare_ci(upper, "\xD0\xBF\xD1\x80\xD0\xB8"), 0); // prefix differs + } else { + // Under KOI8-R these UTF-8 bytes are not Cyrillic; just assert self-equality and that + // the comparison stays reflexive/antisymmetric on arbitrary high bytes. + EXPECT_EQ(native_text::compare_ci(upper, upper), 0); + EXPECT_EQ(native_text::compare_ci(lower, lower), 0); + } +} + +TEST(NativeText, CompareCiMatchesLegacyByteLoopUnderKoi8r) { + if (native_text::native_is_utf8()) { + GTEST_SKIP() << "this pins the KOI8-R branch against the original LOWER() byte loop"; + } + // Reference implementation: the exact loop str_cmp() used before the migration. + auto legacy = [](const char *a, const char *b) { + for (int i = 0;; ++i) { + if (!a[i] && !b[i]) { + return 0; + } + const int chk = legacy_lower(static_cast(a[i])) + - legacy_lower(static_cast(b[i])); + if (chk != 0) { + return chk; + } + } + }; + const char *const samples[] = {"", "a", "A", "abc", "ABC", "abd", "ab", "zzz", "\xC1\xC2", "\xE1\xE2"}; + for (const char *x : samples) { + for (const char *y : samples) { + EXPECT_EQ(native_text::compare_ci(x, y), legacy(x, y)) + << "mismatch for \"" << x << "\" vs \"" << y << "\""; + } + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 421524bcdff56096eebfcde8711ee1180d9499e8 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 05:06:41 +0200 Subject: [PATCH 005/110] feat(utf8): make isname walk characters instead of bytes (#3681) Track A2. isname() (~157 call sites) drives a backtracking state machine over bytes: classification, the case-insensitive match and every advance assumed 1 byte == 1 character. The structure -- including each `curstr = laststr` backtrack -- is kept verbatim; only those three operations now go through native_text. Under KOI8-R this is an identity (is_alnum_char == a_isalnum, chars_equal_ci == LOWER == LOWER, char_bytes == 1); a differential harness comparing the old and new implementations over ASCII and Cyrillic inputs reports 0 differences. Under UTF-8 it fixes the byte logic in both directions, also verified differentially: * false positive: "shchit" matched the unrelated name list "mech dlinnyi", because only the shared D0 lead byte was ever compared; * false negative: "MECH" no longer matched "mech", because the lead bytes fold equal but the trail bytes do not. - native_text: add is_alnum_char() and chars_equal_ci(). - tests: cases for both primitives, incl. the Cyrillic case-fold regression. --- src/utils/native_text.cpp | 32 ++++++++++++++++++++++++++++++++ src/utils/native_text.h | 9 +++++++++ src/utils/utils_string.cpp | 23 ++++++++++++++--------- tests/native_text.cpp | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 2e2f7b52b..4f93f002d 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -16,6 +16,7 @@ through them changes nothing until the encoding flip. // which drags in fmt/ and much of the engine for what is just two 256-byte lookups. extern const char a_ucc_table[]; extern const char a_lcc_table[]; +extern const bool a_isalnum_table[]; #endif #include @@ -132,6 +133,29 @@ int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { return compare_folded(a, b, n); } +bool is_alnum_char(const char *s) { + const unsigned char lead = static_cast(*s); + if (lead < 0x80) { + return (lead >= '0' && lead <= '9') || (lead >= 'A' && lead <= 'Z') || (lead >= 'a' && lead <= 'z'); + } + char32_t cp = 0; + if (utf8::decode(std::string_view(s, char_bytes(s)), 0, cp) == 0) { + return false; + } + // Russian Cyrillic block, including Yo. + return (cp >= 0x0410 && cp <= 0x044F) || cp == 0x0401 || cp == 0x0451; +} + +bool chars_equal_ci(const char *a, const char *b) { + char32_t ca = 0; + char32_t cb = 0; + if (utf8::decode(std::string_view(a, char_bytes(a)), 0, ca) == 0 + || utf8::decode(std::string_view(b, char_bytes(b)), 0, cb) == 0) { + return false; + } + return utf8::to_lower(ca) == utf8::to_lower(cb); +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -196,6 +220,14 @@ int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { return compare_bytes(a, b, n); } +bool is_alnum_char(const char *s) { + return a_isalnum_table[static_cast(*s)]; +} + +bool chars_equal_ci(const char *a, const char *b) { + return a_lcc_table[static_cast(*a)] == a_lcc_table[static_cast(*b)]; +} + #endif } // namespace native_text diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 872c3d02b..65f3a7b26 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -57,6 +57,15 @@ std::size_t char_bytes(const char *s); int compare_ci(std::string_view a, std::string_view b); int ncompare_ci(std::string_view a, std::string_view b, std::size_t n); +// Is the character starting at `s` alphanumeric? KOI8-R: the a_isalnum byte table. UTF-8: ASCII +// letters/digits plus the Russian Cyrillic block -- so a multibyte letter is classified as one +// alphanumeric character rather than a lead byte followed by "punctuation" trail bytes. +bool is_alnum_char(const char *s); + +// Do the characters starting at `a` and `b` match ignoring case? KOI8-R: LOWER(*a) == LOWER(*b). +// UTF-8: compares whole folded code points, so "P" matches "p" in Cyrillic too. +bool chars_equal_ci(const char *a, const char *b); + } // namespace native_text #endif // BYLINS_SRC_UTILS_NATIVE_TEXT_H_ diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index b61343431..2274b1aec 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -943,13 +943,18 @@ bool IsValidEmail(const char *address) { return true; } +// Walks both strings one *character* at a time (issue #3681): the classification, the +// case-insensitive match and every advance go through native_text, so a multibyte letter is one +// unit instead of a lead byte plus trail bytes that the byte tables would read as punctuation. +// Under KOI8-R every helper is the original byte operation and char_bytes() == 1, so the state +// machine below -- including each `curstr = laststr` backtrack -- behaves exactly as before. bool isname(const char *str, const char *namelist) { bool once_ok = false; const char *curname, *curstr, *laststr; if (!namelist || !*namelist || !str) { return false; } - for (curstr = str; !a_isalnum(*curstr); curstr++) { + for (curstr = str; !native_text::is_alnum_char(curstr); curstr += native_text::char_bytes(curstr)) { if (!*curstr) { return once_ok; } @@ -958,18 +963,18 @@ bool isname(const char *str, const char *namelist) { curname = namelist; for (;;) { once_ok = false; - for (;; curstr++, curname++) { + for (;; curstr += native_text::char_bytes(curstr), curname += native_text::char_bytes(curname)) { if (!*curstr) { return once_ok; } if (*curstr == '!') { - if (a_isalnum(*curname)) { + if (native_text::is_alnum_char(curname)) { curstr = laststr; break; } } - if (!a_isalnum(*curstr)) { - for (; !a_isalnum(*curstr); curstr++) { + if (!native_text::is_alnum_char(curstr)) { + for (; !native_text::is_alnum_char(curstr); curstr += native_text::char_bytes(curstr)) { if (!*curstr) { return once_ok; } @@ -980,19 +985,19 @@ bool isname(const char *str, const char *namelist) { if (!*curname) { return false; } - if (!a_isalnum(*curname)) { + if (!native_text::is_alnum_char(curname)) { curstr = laststr; break; } - if (LOWER(*curstr) != LOWER(*curname)) { + if (!native_text::chars_equal_ci(curstr, curname)) { curstr = laststr; break; } else { once_ok = true; } } - for (; a_isalnum(*curname); curname++); - for (; !a_isalnum(*curname); curname++) { + for (; native_text::is_alnum_char(curname); curname += native_text::char_bytes(curname)); + for (; !native_text::is_alnum_char(curname); curname += native_text::char_bytes(curname)) { if (!*curname) { return false; } diff --git a/tests/native_text.cpp b/tests/native_text.cpp index bdf313691..2bf42ba56 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -156,4 +156,36 @@ TEST(NativeText, CompareCiMatchesLegacyByteLoopUnderKoi8r) { } } +TEST(NativeText, IsAlnumChar) { + EXPECT_TRUE(native_text::is_alnum_char("a")); + EXPECT_TRUE(native_text::is_alnum_char("Z")); + EXPECT_TRUE(native_text::is_alnum_char("7")); + EXPECT_FALSE(native_text::is_alnum_char(" ")); + EXPECT_FALSE(native_text::is_alnum_char("!")); + EXPECT_FALSE(native_text::is_alnum_char(".")); + EXPECT_FALSE(native_text::is_alnum_char("")); // terminator is not alphanumeric + if (native_text::native_is_utf8()) { + // A Cyrillic letter is ONE alphanumeric character; its trail byte must not be read as + // punctuation (which is what the raw byte table would do and what breaks tokenisation). + EXPECT_TRUE(native_text::is_alnum_char(kPrivet)); + EXPECT_TRUE(native_text::is_alnum_char("\xD0\x81")); // Yo + EXPECT_TRUE(native_text::is_alnum_char("\xD1\x91")); // yo + } +} + +TEST(NativeText, CharsEqualCi) { + EXPECT_TRUE(native_text::chars_equal_ci("a", "a")); + EXPECT_TRUE(native_text::chars_equal_ci("a", "A")); + EXPECT_TRUE(native_text::chars_equal_ci("Z", "z")); + EXPECT_FALSE(native_text::chars_equal_ci("a", "b")); + EXPECT_FALSE(native_text::chars_equal_ci("a", "")); + if (native_text::native_is_utf8()) { + // The regression this whole step exists for: with the raw KOI8-R byte table the lead + // bytes of "P"/"p" fold equal but the trail bytes differ, so the match was lost. + EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x9F", "\xD0\xBF")); // P vs p + EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x81", "\xD1\x91")); // Yo vs yo + EXPECT_FALSE(native_text::chars_equal_ci("\xD0\x9F", "\xD1\x80")); // P vs r + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 8aed38f070ba263c3b547ae64fb8ffb48823576f Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 05:13:10 +0200 Subject: [PATCH 006/110] feat(utf8): character-aware argument splitting and name declension (#3681) Track A2, continued: the command-argument splitters and the Russian name declension no longer assume 1 byte == 1 character. Both are identities under KOI8-R (verified differentially, 0 differences) and fix real breakage under UTF-8. Splitters -- one_argument/any_one_arg/half_chop (mud_string.cpp) and one_word (utils_string.cpp) lowercased byte by byte, which under UTF-8 leaves Cyrillic untouched (the byte table maps neither the lead nor the trail bytes), so Russian command arguments would stop being folded and command lookup would miss. They now fold one whole character per step. The a_isspace()/quote tests stay byte-based on purpose: they only run at a character boundary and every delimiter is ASCII. GetCase (genchar.cpp) picked the declension from name[len - 1] / name[len - 2] and cut the stem with substr(0, len - 1) -- all byte offsets. Under UTF-8 the last-letter test never matched, so names simply stopped declining ("Anya" stayed "Anya" in every case); a differential run over 10 names x 2 genders x 6 cases shows 0/120 differences under KOI8-R and 65/120 corrections under UTF-8. - native_text: add copy_lower_char(), last_char_offset() and list_contains_char() (the strchr-over-a-letter-list replacement), plus a bounded char_bytes_at() so the view-based helpers are safe on a non-null-terminated string_view. - tests: cases for each new primitive, incl. in-place folding and partial-sequence rejection in list_contains_char. --- src/gameplay/core/genchar.cpp | 29 ++++++++----- src/utils/mud_string.cpp | 18 +++++--- src/utils/native_text.cpp | 80 +++++++++++++++++++++++++++++++++++ src/utils/native_text.h | 17 ++++++++ src/utils/utils_string.cpp | 12 ++++-- tests/native_text.cpp | 52 +++++++++++++++++++++++ 6 files changed, 188 insertions(+), 20 deletions(-) diff --git a/src/gameplay/core/genchar.cpp b/src/gameplay/core/genchar.cpp index fb1a6c327..7edccc100 100644 --- a/src/gameplay/core/genchar.cpp +++ b/src/gameplay/core/genchar.cpp @@ -20,6 +20,7 @@ #include "engine/core/comm.h" #include "utils/logger.h" #include "utils/utils.h" +#include "utils/native_text.h" #include "gameplay/magic/spells.h" #include "engine/entities/char_data.h" #include "engine/entities/char_player.h" @@ -249,10 +250,18 @@ void SetStartAbils(CharData *ch) { // 5 - предложный (о ком? о чем?) // result - результат void GetCase(std::string name, const EGender sex, int caseNum, char *data) { - size_t len = name.size(); std::string result = data; - if (strchr("цкнгшщзхфвпрлджчсмтб", name[len - 1]) != nullptr + // The declension is chosen by the last letter of the name (and sometimes the one before it). + // Those are *characters*, not bytes (issue #3681): under KOI8-R `stem`/`last`/`prev` are the + // same single bytes the old name[len - 1] / name[len - 2] / substr(0, len - 1) produced, + // under UTF-8 they are whole letters. + const size_t last_off = native_text::last_char_offset(name); + const std::string stem = name.substr(0, last_off); + const std::string last = name.substr(last_off); + const std::string prev = stem.substr(native_text::last_char_offset(stem)); + + if (native_text::list_contains_char("цкнгшщзхфвпрлджчсмтб", last) && sex == EGender::kMale) { result = name; if (caseNum == 1) @@ -265,8 +274,8 @@ void GetCase(std::string name, const EGender sex, int caseNum, char *data) { result += "ом"; // Иваном, Ретичем else if (caseNum == 5) result += "е"; // Иване - } else if (name[len - 1] == 'я') { - result = name.substr(0, len - 1); + } else if (last == "я") { + result = stem; if (caseNum == 1) result += "и"; // Ани, Вани else if (caseNum == 2) @@ -279,9 +288,9 @@ void GetCase(std::string name, const EGender sex, int caseNum, char *data) { result += "е"; // Ане, Ване else result += "я"; // Аня, Ваня - } else if (name[len - 1] == 'й' + } else if (last == "й" && sex == EGender::kMale) { - result = name.substr(0, len - 1); + result = stem; if (caseNum == 1) result += "я"; // Дрегвия else if (caseNum == 2) @@ -294,10 +303,10 @@ void GetCase(std::string name, const EGender sex, int caseNum, char *data) { result += "и"; // Дрегвии else result += "й"; // Дрегвий - } else if (name[len - 1] == 'а') { - result = name.substr(0, len - 1); + } else if (last == "а") { + result = stem; if (caseNum == 1) { - if (strchr("шщжч", name[len - 2]) != nullptr) + if (native_text::list_contains_char("шщжч", prev)) result += "и"; // Маши, Паши else result += "ы"; // Анны @@ -306,7 +315,7 @@ void GetCase(std::string name, const EGender sex, int caseNum, char *data) { else if (caseNum == 3) result += "у"; // Пашу, Анну else if (caseNum == 4) { - if (strchr("шщч", name[len - 2]) != nullptr) + if (native_text::list_contains_char("шщч", prev)) result += "ей"; // Машей, Пашей else result += "ой"; // Анной, Ханжой diff --git a/src/utils/mud_string.cpp b/src/utils/mud_string.cpp index 5a9fcd3d9..6bd64f25b 100644 --- a/src/utils/mud_string.cpp +++ b/src/utils/mud_string.cpp @@ -1,6 +1,7 @@ #include "mud_string.h" #include "utils.h" +#include "utils/native_text.h" int search_block(const char *target_string, const char **list, int exact); @@ -45,9 +46,13 @@ T one_argument_template(T argument, char *first_arg) { do { skip_spaces(&argument); first_arg = begin; + // Lowercase one whole character at a time (issue #3681). The a_isspace() test stays + // byte-based on purpose: it only ever runs at a character boundary, and every + // whitespace character is ASCII, so no multibyte lead byte can be mistaken for one. while (*argument && !a_isspace(*argument)) { - *(first_arg++) = a_lcc(*argument); - argument++; + const size_t n = native_text::copy_lower_char(argument, first_arg); + first_arg += n; + argument += n; } *first_arg = '\0'; } while (fill_word(begin)); @@ -64,11 +69,12 @@ T any_one_arg_template(T argument, char *first_arg) { skip_spaces(&argument); int num = 0; + // As above: one character per step, `num` still counts bytes so it remains a buffer guard. while (*argument && !a_isspace(*argument) && num < kMaxStringLength - 1) { - *first_arg = a_lcc(*argument); - ++first_arg; - ++argument; - ++num; + const size_t n = native_text::copy_lower_char(argument, first_arg); + first_arg += n; + argument += n; + num += static_cast(n); } *first_arg = '\0'; skip_spaces(&argument); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 4f93f002d..fd88acd11 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -156,6 +156,26 @@ bool chars_equal_ci(const char *a, const char *b) { return utf8::to_lower(ca) == utf8::to_lower(cb); } +std::size_t copy_lower_char(const char *src, char *dst) { + const std::size_t len = char_bytes(src); + char32_t cp = 0; + if (utf8::decode(std::string_view(src, len), 0, cp) != 0) { + std::string folded; + // Only rewrite when the lowercase form keeps the byte length -- true for ASCII and for + // the whole Russian alphabet, so callers never see a character change size. + if (utf8::encode(utf8::to_lower(cp), folded) == len) { + for (std::size_t i = 0; i < len; ++i) { + dst[i] = folded[i]; + } + return len; + } + } + for (std::size_t i = 0; i < len; ++i) { + dst[i] = src[i]; + } + return len; +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -228,8 +248,68 @@ bool chars_equal_ci(const char *a, const char *b) { return a_lcc_table[static_cast(*a)] == a_lcc_table[static_cast(*b)]; } +std::size_t copy_lower_char(const char *src, char *dst) { + *dst = a_lcc_table[static_cast(*src)]; + return 1; +} + #endif +// --------------------------------------------------------------------------------------------- +// Encoding-independent helpers: expressed purely in terms of the primitives above, so they need +// no per-encoding branch. Unlike char_bytes() these take a bounded view, not a C string, so they +// are safe on a string_view that is not null-terminated. +// --------------------------------------------------------------------------------------------- + +namespace { + +// Byte length of the character at `pos`, clamped to the end of `s`. +std::size_t char_bytes_at(std::string_view s, std::size_t pos) { +#ifdef INTERNAL_ENCODING_UTF8 + const unsigned char lead = static_cast(s[pos]); + if (lead < 0x80) { + return 1; + } + const std::size_t want = static_cast(utf8::sequence_length(lead)); + std::size_t n = 1; + while (n < want && pos + n < s.size() && (static_cast(s[pos + n]) & 0xC0) == 0x80) { + ++n; + } + return n; +#else + (void) s; + (void) pos; + return 1; +#endif +} + +} // namespace + +std::size_t last_char_offset(std::string_view s) { + std::size_t last = 0; + std::size_t pos = 0; + while (pos < s.size()) { + last = pos; + pos += char_bytes_at(s, pos); + } + return last; +} + +bool list_contains_char(std::string_view list, std::string_view ch) { + if (ch.empty()) { + return false; + } + std::size_t pos = 0; + while (pos < list.size()) { + const std::size_t len = char_bytes_at(list, pos); + if (len == ch.size() && list.compare(pos, len, ch) == 0) { + return true; + } + pos += len; + } + return false; +} + } // namespace native_text // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 65f3a7b26..d6da4304f 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -66,6 +66,23 @@ bool is_alnum_char(const char *s); // UTF-8: compares whole folded code points, so "P" matches "p" in Cyrillic too. bool chars_equal_ci(const char *a, const char *b); +// Copy the character starting at `src` to `dst`, lowercased, and return how many bytes were +// consumed (always the same number written, so a caller's buffer accounting is unaffected). +// KOI8-R: one byte through a_lcc_table. UTF-8: folds the code point; in the rare case where the +// lowercase form would not be the same byte length, the character is copied unchanged rather +// than resized. `dst` may alias `src` (the length-preserving property makes that safe). +std::size_t copy_lower_char(const char *src, char *dst); + +// Byte offset at which the final character of `s` begins (0 for an empty string), so that +// s.substr(0, last_char_offset(s)) drops exactly one character and s.substr(last_char_offset(s)) +// is that character. KOI8-R: s.size() - 1. +std::size_t last_char_offset(std::string_view s); + +// Does the single character `ch` occur in `list`? The replacement for strchr() over a literal +// list of letters: `list` is walked one whole character at a time, so a multibyte character can +// never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. +bool list_contains_char(std::string_view list, std::string_view ch); + } // namespace native_text #endif // BYLINS_SRC_UTILS_NATIVE_TEXT_H_ diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 2274b1aec..10fd66288 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -1009,17 +1009,21 @@ const char *one_word(const char *argument, char *first_arg) { char *begin = first_arg; skip_spaces(&argument); first_arg = begin; + // Lowercase whole characters (issue #3681); the '"' and a_isspace() tests stay byte-based + // since they only run at a character boundary and both delimiters are ASCII. if (*argument == '\"') { argument++; while (*argument && *argument != '\"') { - *(first_arg++) = a_lcc(*argument); - argument++; + const size_t n = native_text::copy_lower_char(argument, first_arg); + first_arg += n; + argument += n; } argument++; } else { while (*argument && !a_isspace(*argument)) { - *(first_arg++) = a_lcc(*argument); - argument++; + const size_t n = native_text::copy_lower_char(argument, first_arg); + first_arg += n; + argument += n; } } *first_arg = '\0'; diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 2bf42ba56..03d2250e0 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -188,4 +188,56 @@ TEST(NativeText, CharsEqualCi) { } } +TEST(NativeText, CopyLowerChar) { + char buf[8] = {0}; + EXPECT_EQ(native_text::copy_lower_char("A", buf), 1u); + EXPECT_STREQ(buf, "a"); + EXPECT_EQ(native_text::copy_lower_char("z", buf), 1u); + EXPECT_STREQ(buf, "z"); + EXPECT_EQ(native_text::copy_lower_char("7", buf), 1u); + EXPECT_STREQ(buf, "7"); + if (native_text::native_is_utf8()) { + std::memset(buf, 0, sizeof(buf)); + EXPECT_EQ(native_text::copy_lower_char("\xD0\x9F", buf), 2u); // P -> p + EXPECT_STREQ(buf, "\xD0\xBF"); + std::memset(buf, 0, sizeof(buf)); + EXPECT_EQ(native_text::copy_lower_char("\xD0\x81", buf), 2u); // Yo -> yo + EXPECT_STREQ(buf, "\xD1\x91"); + // In-place folding must be safe (the lowercase form keeps the byte length). + char inplace[] = "\xD0\x9F"; + EXPECT_EQ(native_text::copy_lower_char(inplace, inplace), 2u); + EXPECT_STREQ(inplace, "\xD0\xBF"); + } +} + +TEST(NativeText, LastCharOffset) { + EXPECT_EQ(native_text::last_char_offset(""), 0u); + EXPECT_EQ(native_text::last_char_offset("a"), 0u); + EXPECT_EQ(native_text::last_char_offset("abc"), 2u); + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::last_char_offset(kPrivet), 10u); // 6 chars, last starts at byte 10 + const std::string_view s(kPrivet, 12); + EXPECT_EQ(s.substr(native_text::last_char_offset(s)), "\xD1\x82"); // final char "t" + EXPECT_EQ(native_text::last_char_offset("\xF0\x9F\x98\x80"), 0u); // single 4-byte char + } else { + EXPECT_EQ(native_text::last_char_offset(kPrivet), 11u); // 12 bytes -> last byte + } +} + +TEST(NativeText, ListContainsChar) { + EXPECT_TRUE(native_text::list_contains_char("abc", "b")); + EXPECT_FALSE(native_text::list_contains_char("abc", "d")); + EXPECT_FALSE(native_text::list_contains_char("abc", "")); + EXPECT_FALSE(native_text::list_contains_char("", "a")); + EXPECT_FALSE(native_text::list_contains_char("abc", "A")); // case-sensitive, like strchr + if (native_text::native_is_utf8()) { + // A multibyte character must match as a whole and never on a partial byte sequence. + const char *const list = "\xD1\x88\xD1\x89\xD0\xB6\xD1\x87"; // sh shch zh ch + EXPECT_TRUE(native_text::list_contains_char(list, "\xD1\x89")); + EXPECT_TRUE(native_text::list_contains_char(list, "\xD0\xB6")); + EXPECT_FALSE(native_text::list_contains_char(list, "\xD1\x82")); + EXPECT_FALSE(native_text::list_contains_char(list, "\xD1")); // lead byte alone + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From d0d59dcc012f4955537df9cee41a05362f5da67f Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 06:20:07 +0200 Subject: [PATCH 007/110] test(utf8): regression tests for the byte-vs-character text routines (#3681) Adds tests/text_semantics.cpp: behavioural coverage for the routines the migration touched -- isname, str_cmp/strn_cmp, one_argument/half_chop and GetCase. Several of these had no unit tests at all, so this also pays down existing debt rather than only guarding the new code. The Russian literals are written as literals, not byte escapes: the file compiles in whatever encoding the engine is built with, and the routines under test work in that same native encoding, so the expectations hold both before and after the flip. That makes this file the guard that flipping the encoding does not change behaviour. Covered regressions (each one silently broken by byte semantics under UTF-8): * isname matched unrelated Cyrillic words that shared a lead byte, and lost case-insensitive matching for Russian keywords; * argument splitting left Russian arguments unfolded, so command lookup missed; * GetCase stopped declining names entirely. Also renames the fixture in tests/native_text.cpp (kPrivet -> kNtPrivet): the test sources are unity-built, so it collided with the one in tests/utf8.cpp. --- tests/meson.build | 1 + tests/native_text.cpp | 29 ++++---- tests/text_semantics.cpp | 148 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 tests/text_semantics.cpp diff --git a/tests/meson.build b/tests/meson.build index 3ece7787f..4879e6b9d 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -45,6 +45,7 @@ test_sources = files( 'accounts.storage.cpp', 'utf8.cpp', 'native_text.cpp', + 'text_semantics.cpp', 'fight.penalties.cpp', 'bonus.command.parser.cpp', 'quested.cpp', diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 03d2250e0..8684c4a17 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -13,8 +13,9 @@ namespace { -// "Privet": 6 Cyrillic code points, 12 UTF-8 bytes. -const char *const kPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; +// "Privet": 6 Cyrillic code points, 12 UTF-8 bytes. (Name-prefixed: the test files are +// unity-built, so a plain kPrivet would clash with the one in tests/utf8.cpp.) +const char *const kNtPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; } // namespace @@ -32,16 +33,16 @@ int legacy_lower(unsigned char c) { TEST(NativeText, CharCountAscii) { EXPECT_EQ(native_text::char_count("Hello"), 5u); - EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 4), native_text::native_is_utf8() ? 2u : 4u); + EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 4), native_text::native_is_utf8() ? 2u : 4u); } TEST(NativeText, CharCountReflectsEncoding) { if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::char_count(kPrivet), 6u); - EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 12), 6u); + EXPECT_EQ(native_text::char_count(kNtPrivet), 6u); + EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 12), 6u); } else { - EXPECT_EQ(native_text::char_count(kPrivet), 12u); - EXPECT_EQ(native_text::char_count(kPrivet, kPrivet + 12), 12u); + EXPECT_EQ(native_text::char_count(kNtPrivet), 12u); + EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 12), 12u); } } @@ -71,16 +72,16 @@ TEST(NativeText, CapitalizeCyrillicUtf8Only) { TEST(NativeText, CharBytes) { EXPECT_EQ(native_text::char_bytes("A"), 1u); if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::char_bytes(kPrivet), 2u); // Cyrillic lead -> 2 bytes + EXPECT_EQ(native_text::char_bytes(kNtPrivet), 2u); // Cyrillic lead -> 2 bytes EXPECT_EQ(native_text::char_bytes("\xF0\x9F\x98\x80"), 4u); // 4-byte code point EXPECT_EQ(native_text::char_bytes("\xD0"), 1u); // truncated lead: 1 byte present } else { - EXPECT_EQ(native_text::char_bytes(kPrivet), 1u); // KOI8-R: every byte is a char + EXPECT_EQ(native_text::char_bytes(kNtPrivet), 1u); // KOI8-R: every byte is a char } } TEST(NativeText, TruncateOffset) { - const std::string_view p(kPrivet, 12); + const std::string_view p(kNtPrivet, 12); EXPECT_EQ(native_text::truncate_offset(p, 100), 12u); // past end -> full size EXPECT_EQ(native_text::truncate_offset(p, 0), 0u); if (native_text::native_is_utf8()) { @@ -167,7 +168,7 @@ TEST(NativeText, IsAlnumChar) { if (native_text::native_is_utf8()) { // A Cyrillic letter is ONE alphanumeric character; its trail byte must not be read as // punctuation (which is what the raw byte table would do and what breaks tokenisation). - EXPECT_TRUE(native_text::is_alnum_char(kPrivet)); + EXPECT_TRUE(native_text::is_alnum_char(kNtPrivet)); EXPECT_TRUE(native_text::is_alnum_char("\xD0\x81")); // Yo EXPECT_TRUE(native_text::is_alnum_char("\xD1\x91")); // yo } @@ -215,12 +216,12 @@ TEST(NativeText, LastCharOffset) { EXPECT_EQ(native_text::last_char_offset("a"), 0u); EXPECT_EQ(native_text::last_char_offset("abc"), 2u); if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::last_char_offset(kPrivet), 10u); // 6 chars, last starts at byte 10 - const std::string_view s(kPrivet, 12); + EXPECT_EQ(native_text::last_char_offset(kNtPrivet), 10u); // 6 chars, last starts at byte 10 + const std::string_view s(kNtPrivet, 12); EXPECT_EQ(s.substr(native_text::last_char_offset(s)), "\xD1\x82"); // final char "t" EXPECT_EQ(native_text::last_char_offset("\xF0\x9F\x98\x80"), 0u); // single 4-byte char } else { - EXPECT_EQ(native_text::last_char_offset(kPrivet), 11u); // 12 bytes -> last byte + EXPECT_EQ(native_text::last_char_offset(kNtPrivet), 11u); // 12 bytes -> last byte } } diff --git a/tests/text_semantics.cpp b/tests/text_semantics.cpp new file mode 100644 index 000000000..089776b1f --- /dev/null +++ b/tests/text_semantics.cpp @@ -0,0 +1,148 @@ +// Regression tests for the byte-vs-character migration (issue #3681). +// +// These pin the *observable behaviour* of the text routines that used to assume 1 byte == 1 +// character: name matching, case-insensitive comparison, argument splitting and Russian name +// declension. Several of these paths had no coverage at all before the migration touched them. +// +// The Russian literals below are deliberately written as literals rather than byte escapes: the +// file is compiled in whatever encoding the engine is built with (KOI8-R today, UTF-8 after the +// flip), and the routines under test operate in that same native encoding. The expectations are +// therefore valid in both, and this file doubles as the guard that the flip did not change +// user-visible behaviour. + +#include "utils/utils_string.h" +#include "utils/mud_string.h" +#include "gameplay/core/genchar.h" +#include "utils/grammar/gender.h" +#include "engine/structs/structs.h" + +#include + +#include + +namespace { + +std::string declension(const char *name, EGender sex, int case_num) { + char buf[128] = {0}; + GetCase(name, sex, case_num, buf); + return std::string(buf); +} + +std::string first_argument(const char *line) { + char buf[kMaxInputLength] = {0}; + one_argument(line, buf); + return std::string(buf); +} + +} // namespace + +// ---------------------------------------------------------------------------- isname + +TEST(TextSemantics, IsnameMatchesAsciiKeywords) { + EXPECT_TRUE(isname("sword", "a long sword")); + EXPECT_TRUE(isname("long", "a long sword")); + EXPECT_TRUE(isname("SWORD", "a long sword")); // case-insensitive + EXPECT_TRUE(isname("swo", "a long sword")); // prefix + EXPECT_FALSE(isname("xyzzy", "a long sword")); +} + +TEST(TextSemantics, IsnameMatchesRussianKeywords) { + EXPECT_TRUE(isname("меч", "меч длинный")); + EXPECT_TRUE(isname("длинный", "меч длинный")); + EXPECT_TRUE(isname("ме", "меч длинный")); // prefix +} + +TEST(TextSemantics, IsnameIsCaseInsensitiveForRussian) { + // Regression: with byte-wise folding under UTF-8 the lead bytes of an upper/lower Cyrillic + // letter fold equal but the trail bytes do not, so this match was silently lost. + EXPECT_TRUE(isname("МЕЧ", "меч")); + EXPECT_TRUE(isname("Меч", "меч длинный")); + EXPECT_TRUE(isname("меч", "МЕЧ ДЛИННЫЙ")); +} + +TEST(TextSemantics, IsnameRejectsUnrelatedRussianWords) { + // Regression: byte-wise matching under UTF-8 compared only the shared leading byte of two + // different Cyrillic letters, so unrelated words matched each other. + EXPECT_FALSE(isname("щит", "меч длинный")); + EXPECT_FALSE(isname("меч", "щит деревянный")); + EXPECT_FALSE(isname("кольцо", "меч")); +} + +// ---------------------------------------------------------------------------- str_cmp + +TEST(TextSemantics, StrCmpIgnoresCase) { + EXPECT_EQ(str_cmp("abc", "ABC"), 0); + EXPECT_EQ(str_cmp("меч", "МЕЧ"), 0); + EXPECT_EQ(str_cmp(std::string("меч"), "МЕЧ"), 0); + EXPECT_NE(str_cmp("меч", "щит"), 0); +} + +TEST(TextSemantics, StrCmpOrdersConsistently) { + EXPECT_LT(str_cmp("abc", "abd"), 0); + EXPECT_GT(str_cmp("abd", "abc"), 0); + EXPECT_LT(str_cmp("ab", "abc"), 0); // prefix sorts first + EXPECT_GT(str_cmp("abc", "ab"), 0); +} + +TEST(TextSemantics, StrnCmpComparesPrefixOnly) { + EXPECT_EQ(strn_cmp("abcdef", "abcXXX", 3), 0); + EXPECT_NE(strn_cmp("abcdef", "abXXXX", 3), 0); + EXPECT_EQ(strn_cmp("МЕЧ", "меч", 6), 0); +} + +// ---------------------------------------------------------------------------- argument splitting + +TEST(TextSemantics, OneArgumentLowercasesAscii) { + EXPECT_EQ(first_argument("LOOK north"), "look"); + EXPECT_EQ(first_argument(" Kill orc"), "kill"); +} + +TEST(TextSemantics, OneArgumentLowercasesRussian) { + // Regression: the byte table leaves UTF-8 Cyrillic untouched, so a Russian command argument + // would reach the command lookup unfolded and fail to match. + EXPECT_EQ(first_argument("СМОТРЕТЬ север"), "смотреть"); + EXPECT_EQ(first_argument("Убить орка"), "убить"); + EXPECT_EQ(first_argument("меч"), "меч"); +} + +TEST(TextSemantics, HalfChopSplitsAndLowercasesFirstWord) { + char arg1[kMaxInputLength] = {0}; + char arg2[kMaxInputLength] = {0}; + half_chop("СКАЗАТЬ привет всем", arg1, arg2); + EXPECT_STREQ(arg1, "сказать"); + EXPECT_STREQ(arg2, "привет всем"); +} + +// ---------------------------------------------------------------------------- GetCase + +TEST(TextSemantics, DeclensionOfFeminineNameEndingInYa) { + // Regression: under UTF-8 the byte-wise last-letter test never matched, so names stopped + // declining entirely and every case returned the nominative. + EXPECT_EQ(declension("Аня", EGender::kFemale, 1), "Ани"); + EXPECT_EQ(declension("Аня", EGender::kFemale, 2), "Ане"); + EXPECT_EQ(declension("Аня", EGender::kFemale, 3), "Аню"); + EXPECT_EQ(declension("Аня", EGender::kFemale, 4), "Аней"); + EXPECT_EQ(declension("Аня", EGender::kFemale, 5), "Ане"); +} + +TEST(TextSemantics, DeclensionOfMasculineNameEndingInConsonant) { + EXPECT_EQ(declension("Иван", EGender::kMale, 1), "Ивана"); + EXPECT_EQ(declension("Иван", EGender::kMale, 2), "Ивану"); + EXPECT_EQ(declension("Иван", EGender::kMale, 4), "Иваном"); + EXPECT_EQ(declension("Иван", EGender::kMale, 5), "Иване"); +} + +TEST(TextSemantics, DeclensionOfNameEndingInA) { + // The genitive/instrumental endings depend on the letter *before* the final one. + EXPECT_EQ(declension("Маша", EGender::kFemale, 1), "Маши"); // after ш -> и + EXPECT_EQ(declension("Анна", EGender::kFemale, 1), "Анны"); // otherwise -> ы + EXPECT_EQ(declension("Маша", EGender::kFemale, 4), "Машей"); // after ш -> ей + EXPECT_EQ(declension("Анна", EGender::kFemale, 4), "Анной"); // otherwise -> ой +} + +TEST(TextSemantics, DeclensionOfMasculineNameEndingInIShort) { + EXPECT_EQ(declension("Дрегвий", EGender::kMale, 1), "Дрегвия"); + EXPECT_EQ(declension("Дрегвий", EGender::kMale, 4), "Дрегвием"); +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 4fa3ac37462e42d22c4a3edcf512306f5d1815de Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 07:51:34 +0200 Subject: [PATCH 008/110] feat(utf8): character-aware ctype scanning, closing track A2 (#3681) The byte ctype tables break under UTF-8 in one specific shape: a multibyte letter reads as "alnum lead byte + non-alnum trail bytes", so every loop that scans a *run* of letters stops in the middle of one. Audited all 82 a_is* call sites; only the alpha/alnum/upper family can misclassify (isspace/isdigit/isxdigit are ASCII-only and answer correctly for a trail byte), which narrowed the work to five real scanners: * fname() -- first keyword of a name list * im.cpp -- crafting alias extraction * dg_scripts.cpp -- script token boundaries * cut_one_word() -- word splitting * do_gen_comm.cpp -- the anti-caps filter, which could not see uppercase Cyrillic at all (a UTF-8 lead byte is outside the table's range), and whose percentage denominator counted bytes, not characters Deliberately left byte-based: IsValidShopId (ids are ASCII identifiers by design) and the single-character checks in login.cpp / interpreter.cpp, which run at a character boundary. pred_separator is dead code -- no users outside utils.h -- so it was not migrated; it should simply be deleted. Debt paid while here: fname() and the im.cpp alias loop copied into fixed buffers (30 and 16 bytes) with no bounds check at all -- multibyte text reaches the end twice as fast, so both now stop short of the buffer. dg_scripts passed a raw (negative) char to isspace(), which is undefined behaviour. - native_text: add is_alpha_char() and is_upper_char(). - tests: predicate coverage plus behavioural tests for fname() (incl. the overflow guard) and cut_one_word(), neither of which had any before. Verified: full suite 648 passed / 0 failed; live boot on the production world (world.20260802.tgz) with declension prompts still correct. --- src/engine/scripting/dg_scripts.cpp | 10 ++++-- src/engine/ui/cmd/do_gen_comm.cpp | 17 ++++++++--- src/gameplay/crafting/im.cpp | 13 +++++++- src/utils/native_text.cpp | 34 +++++++++++++++++++++ src/utils/native_text.h | 8 +++++ src/utils/utils_parse.cpp | 22 +++++++++++--- src/utils/utils_string.cpp | 8 +++-- tests/native_text.cpp | 27 +++++++++++++++++ tests/text_semantics.cpp | 47 +++++++++++++++++++++++++++++ 9 files changed, 171 insertions(+), 15 deletions(-) diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index fdb30e629..834c8fdeb 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -31,6 +31,7 @@ #include "gameplay/mechanics/illumination.h" #include "gameplay/mechanics/inventory.h" #include "utils/utils_parse.h" +#include "utils/native_text.h" #include "dg_event.h" #include "engine/ui/color.h" #include "gameplay/clans/house.h" @@ -4338,8 +4339,13 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go p = matching_paren(p) + 1; else if (*p == '"') p = matching_quote(p) + 1; - else if (a_isalnum(*p)) - for (p++; *p && (a_isalnum(*p) || isspace(*p)); p++); + // Step over whole characters (issue #3681): a byte-wise scan ends a token in the middle + // of a multibyte letter. isspace() takes an unsigned value -- a raw char is negative for + // any non-ASCII byte, which is undefined behaviour. + else if (native_text::is_alnum_char(p)) + for (p += native_text::char_bytes(p); + *p && (native_text::is_alnum_char(p) || isspace(static_cast(*p))); + p += native_text::char_bytes(p)); else p++; } diff --git a/src/engine/ui/cmd/do_gen_comm.cpp b/src/engine/ui/cmd/do_gen_comm.cpp index 83c113818..7af33bc90 100644 --- a/src/engine/ui/cmd/do_gen_comm.cpp +++ b/src/engine/ui/cmd/do_gen_comm.cpp @@ -8,6 +8,7 @@ #include "do_gen_comm.h" #include "administration/privilege.h" +#include "utils/native_text.h" #include "utils/grammar/gender.h" #include "gameplay/mechanics/sight.h" @@ -156,17 +157,25 @@ void do_gen_comm(CharData *ch, char *argument, int/* cmd*/, int subcmd) { int bad_simb_cnt = 0, bad_seq_cnt = 0; // фильтруем верхний регистр - for (int k = 0; argument[k] != '\0'; k++) { - if (a_isupper(argument[k])) { + // Counted and folded per character (issue #3681): a byte-wise scan cannot see an + // uppercase Cyrillic letter at all, so the filter silently stopped working for Russian. + // The denominator is the character count for the same reason -- with byte lengths the + // percentage would be halved for Russian text. + const size_t total_chars = native_text::char_count(argument); + for (int k = 0; argument[k] != '\0';) { + const int bytes = static_cast(native_text::char_bytes(argument + k)); + if (native_text::is_upper_char(argument + k)) { bad_simb_cnt++; bad_seq_cnt++; } else bad_seq_cnt = 0; if ((bad_seq_cnt > 1) && - (((bad_simb_cnt * 100 / strlen(argument)) > bad_smb_procent) || + (((bad_simb_cnt * 100 / total_chars) > bad_smb_procent) || (bad_seq_cnt > MAX_UPPERS_SEQ_CHAR))) - argument[k] = a_lcc(argument[k]); + native_text::copy_lower_char(argument + k, argument + k); + + k += bytes; } // фильтруем одинаковые сообщения в эфире if (!str_cmp(ch->get_last_tell().c_str(), argument)) { diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index 5d555bdcd..81ede15a6 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -13,6 +13,7 @@ #include "im.h" #include "utils/parser_wrapper.h" #include "utils/utils_parse.h" +#include "utils/native_text.h" #include #include #include @@ -246,7 +247,17 @@ const char *replace_alias(const char *ptr, im_memb *sample, int rnum, const char if (*ptr == VAR_CHAR) { int k; ++ptr; - for (k = 0; (*ptr) && a_isalnum(*ptr); aname[k++] = *ptr++); + // One whole character per step (issue #3681), with a bounds check: the + // previous loop had none, and multibyte text fills aname[] twice as fast. + for (k = 0; *ptr && native_text::is_alnum_char(ptr);) { + const size_t bytes = native_text::char_bytes(ptr); + if (static_cast(k) + bytes >= sizeof(aname)) { + break; + } + for (size_t i = 0; i < bytes; ++i) { + aname[k++] = *ptr++; + } + } aname[k] = 0; al = get_im_alias(sample, aname); strcpy(dst, al ? al : aname); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index fd88acd11..8c72edd93 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -17,6 +17,8 @@ through them changes nothing until the encoding flip. extern const char a_ucc_table[]; extern const char a_lcc_table[]; extern const bool a_isalnum_table[]; +extern const bool a_isalpha_table[]; +extern const bool a_isupper_table[]; #endif #include @@ -146,6 +148,30 @@ bool is_alnum_char(const char *s) { return (cp >= 0x0410 && cp <= 0x044F) || cp == 0x0401 || cp == 0x0451; } +bool is_alpha_char(const char *s) { + const unsigned char lead = static_cast(*s); + if (lead < 0x80) { + return (lead >= 'A' && lead <= 'Z') || (lead >= 'a' && lead <= 'z'); + } + char32_t cp = 0; + if (utf8::decode(std::string_view(s, char_bytes(s)), 0, cp) == 0) { + return false; + } + return (cp >= 0x0410 && cp <= 0x044F) || cp == 0x0401 || cp == 0x0451; +} + +bool is_upper_char(const char *s) { + const unsigned char lead = static_cast(*s); + if (lead < 0x80) { + return lead >= 'A' && lead <= 'Z'; + } + char32_t cp = 0; + if (utf8::decode(std::string_view(s, char_bytes(s)), 0, cp) == 0) { + return false; + } + return (cp >= 0x0410 && cp <= 0x042F) || cp == 0x0401; +} + bool chars_equal_ci(const char *a, const char *b) { char32_t ca = 0; char32_t cb = 0; @@ -244,6 +270,14 @@ bool is_alnum_char(const char *s) { return a_isalnum_table[static_cast(*s)]; } +bool is_alpha_char(const char *s) { + return a_isalpha_table[static_cast(*s)]; +} + +bool is_upper_char(const char *s) { + return a_isupper_table[static_cast(*s)]; +} + bool chars_equal_ci(const char *a, const char *b) { return a_lcc_table[static_cast(*a)] == a_lcc_table[static_cast(*b)]; } diff --git a/src/utils/native_text.h b/src/utils/native_text.h index d6da4304f..d9ba7a3ce 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -62,6 +62,14 @@ int ncompare_ci(std::string_view a, std::string_view b, std::size_t n); // alphanumeric character rather than a lead byte followed by "punctuation" trail bytes. bool is_alnum_char(const char *s); +// Is the character starting at `s` a letter? Same contract as is_alnum_char, minus the digits. +bool is_alpha_char(const char *s); + +// Is the character starting at `s` an uppercase letter? KOI8-R: the a_isupper byte table. +// UTF-8: ASCII A-Z plus the uppercase Cyrillic range (and Yo) as whole code points -- the byte +// table cannot see these at all, since a UTF-8 Cyrillic lead byte is not in its uppercase range. +bool is_upper_char(const char *s); + // Do the characters starting at `a` and `b` match ignoring case? KOI8-R: LOWER(*a) == LOWER(*b). // UTF-8: compares whole folded code points, so "P" matches "p" in Cyrillic too. bool chars_equal_ci(const char *a, const char *b); diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 0105e2bd1..622ddd0fd 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -4,7 +4,8 @@ #include "utils_parse.h" #include "third_party_libs/pugixml/pugixml.h" -#include "utils/parser_wrapper.h" // issue.xml-parse-cleaning: AttrInt/AttrStr over DataNode +#include "utils/parser_wrapper.h" +#include "utils/native_text.h" // issue.xml-parse-cleaning: AttrInt/AttrStr over DataNode #include "engine/db/obj_prototypes.h" #include "engine/db/db.h" @@ -393,10 +394,21 @@ int get_number(std::string &name) { // issue.handler-cleaning: first keyword of a name list (moved from handler). char *fname(const char *namelist) { static char holder[30]; - char *point; - - for (point = holder; a_isalpha(*namelist); namelist++, point++) - *point = *namelist; + char *point = holder; + + // Copy the leading word one whole character at a time (issue #3681): a byte-wise copy stops + // in the middle of a multibyte letter. The bounds check is new -- the previous loop could + // already run past holder[] on a long keyword, and multibyte text reaches the end twice as + // fast, so leave room for the terminator. + while (native_text::is_alpha_char(namelist)) { + const size_t bytes = native_text::char_bytes(namelist); + if (point + bytes >= holder + sizeof(holder)) { + break; + } + for (size_t i = 0; i < bytes; ++i) { + *point++ = *namelist++; + } + } *point = '\0'; diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 10fd66288..7f9725f25 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -878,13 +878,15 @@ void cut_one_word(std::string &str, std::string &word) { } bool process = false; unsigned begin = 0, end = 0; - for (unsigned i = 0; i < str.size(); ++i) { - if (!process && a_isalnum(str.at(i))) { + // Word boundaries are looked for one whole character at a time (issue #3681); a byte-wise + // scan finds a "boundary" inside a multibyte letter and cuts the word in half. + for (unsigned i = 0; i < str.size(); i += native_text::char_bytes(str.c_str() + i)) { + if (!process && native_text::is_alnum_char(str.c_str() + i)) { process = true; begin = i; continue; } - if (process && !a_isalnum(str.at(i))) { + if (process && !native_text::is_alnum_char(str.c_str() + i)) { end = i; break; } diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 8684c4a17..17bc01c33 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -174,6 +174,33 @@ TEST(NativeText, IsAlnumChar) { } } +TEST(NativeText, IsAlphaChar) { + EXPECT_TRUE(native_text::is_alpha_char("a")); + EXPECT_TRUE(native_text::is_alpha_char("Z")); + EXPECT_FALSE(native_text::is_alpha_char("7")); // digit: alnum but not alpha + EXPECT_FALSE(native_text::is_alpha_char(" ")); + EXPECT_FALSE(native_text::is_alpha_char("")); + if (native_text::native_is_utf8()) { + EXPECT_TRUE(native_text::is_alpha_char(kNtPrivet)); + EXPECT_TRUE(native_text::is_alpha_char("\xD0\x81")); // Yo + } +} + +TEST(NativeText, IsUpperChar) { + EXPECT_TRUE(native_text::is_upper_char("A")); + EXPECT_FALSE(native_text::is_upper_char("a")); + EXPECT_FALSE(native_text::is_upper_char("7")); + EXPECT_FALSE(native_text::is_upper_char("")); + if (native_text::native_is_utf8()) { + // The byte table cannot see these: a UTF-8 Cyrillic lead byte is outside its uppercase + // range, which is why the anti-caps filter stopped working for Russian. + EXPECT_TRUE(native_text::is_upper_char("\xD0\x9F")); // P + EXPECT_FALSE(native_text::is_upper_char("\xD0\xBF")); // p + EXPECT_TRUE(native_text::is_upper_char("\xD0\x81")); // Yo + EXPECT_FALSE(native_text::is_upper_char("\xD1\x91")); // yo + } +} + TEST(NativeText, CharsEqualCi) { EXPECT_TRUE(native_text::chars_equal_ci("a", "a")); EXPECT_TRUE(native_text::chars_equal_ci("a", "A")); diff --git a/tests/text_semantics.cpp b/tests/text_semantics.cpp index 089776b1f..35aa6dd78 100644 --- a/tests/text_semantics.cpp +++ b/tests/text_semantics.cpp @@ -11,6 +11,7 @@ // user-visible behaviour. #include "utils/utils_string.h" +#include "utils/utils_parse.h" #include "utils/mud_string.h" #include "gameplay/core/genchar.h" #include "utils/grammar/gender.h" @@ -145,4 +146,50 @@ TEST(TextSemantics, DeclensionOfMasculineNameEndingInIShort) { EXPECT_EQ(declension("Дрегвий", EGender::kMale, 4), "Дрегвием"); } +// ---------------------------------------------------------------------------- fname + +TEST(TextSemantics, FnameExtractsFirstKeyword) { + EXPECT_STREQ(fname("sword long blade"), "sword"); + EXPECT_STREQ(fname("меч длинный"), "меч"); + EXPECT_STREQ(fname("кольцо"), "кольцо"); + EXPECT_STREQ(fname(""), ""); + EXPECT_STREQ(fname(" leading space"), ""); // stops at the very first non-letter +} + +TEST(TextSemantics, FnameStaysInsideItsBuffer) { + // fname() returns a fixed 30-byte buffer and used to copy without any bounds check; a long + // keyword (twice as many bytes per letter once the text is multibyte) ran past its end. + const char *const very_long = "оченьдлинноеключевоесловокотороенепомещается прочее"; + const char *const got = fname(very_long); + EXPECT_LT(std::strlen(got), 30u); + // Whatever was copied must be a prefix of the input, never mangled bytes. + EXPECT_EQ(std::string(very_long).compare(0, std::strlen(got), got), 0); +} + +// ---------------------------------------------------------------------------- cut_one_word + +TEST(TextSemantics, CutOneWordSplitsOnWordBoundaries) { + std::string rest = "меч длинный острый"; + std::string word; + cut_one_word(rest, word); + EXPECT_EQ(word, "меч"); + cut_one_word(rest, word); + EXPECT_EQ(word, "длинный"); + cut_one_word(rest, word); + EXPECT_EQ(word, "острый"); +} + +TEST(TextSemantics, CutOneWordHandlesAsciiAndEmpty) { + std::string rest = "take all"; + std::string word; + cut_one_word(rest, word); + EXPECT_EQ(word, "take"); + cut_one_word(rest, word); + EXPECT_EQ(word, "all"); + + std::string empty; + cut_one_word(empty, word); + EXPECT_TRUE(word.empty()); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 35f4170396993911e30d801f4a80c3dbf16012dd Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 08:00:48 +0200 Subject: [PATCH 009/110] feat(utf8): character-width column padding, first A3 subsystems (#3681) Track A3: a printf "%-Ns" field pads by bytes, so a column of Russian text comes out half as wide once the text is multibyte. Adds pad_right()/pad_left() (width in characters -- byte-identical under KOI8-R) and converts the most visible player-facing columns: WHO short list, WHERE, skills and affects. Also makes the libfort wrapper pick its base class by encoding: char_table measures a cell in bytes, utf8_table in code points. Neither is right for both -- under KOI8-R the byte table is correct and utf8_table would misread KOI8-R as UTF-8 -- so the choice now follows INTERNAL_ENCODING_UTF8 instead of being hardcoded. Scope finding: fmt::format's "{:<20}" needs NO migration. Measured against the vendored fmt -- it counts code points for valid UTF-8 and falls back to one-unit-per-byte for KOI8-R (which is invalid UTF-8), so it is already correct in both encodings. That takes every fmt-based column, including where_format, out of this track; only printf-style fields remain. Deliberately unchanged: colour codes embedded in a padded string still count toward the width, exactly as with "%-Ns" today. That skew is pre-existing and orthogonal. --- src/engine/ui/cmd/do_affects.cpp | 13 +++++++++---- src/engine/ui/cmd/do_skills.cpp | 6 ++++-- src/engine/ui/cmd/do_where.cpp | 8 ++++++-- src/engine/ui/cmd/do_who.cpp | 13 +++++++++---- src/engine/ui/table_wrapper.h | 16 ++++++++++++++-- src/utils/native_text.cpp | 21 +++++++++++++++++++++ src/utils/native_text.h | 8 ++++++++ tests/native_text.cpp | 17 +++++++++++++++++ 8 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/engine/ui/cmd/do_affects.cpp b/src/engine/ui/cmd/do_affects.cpp index 95b9e6437..340a6d191 100644 --- a/src/engine/ui/cmd/do_affects.cpp +++ b/src/engine/ui/cmd/do_affects.cpp @@ -2,6 +2,7 @@ // Created by Sventovit on 07.09.2024. // +#include #include "engine/entities/char_data.h" #include "gameplay/affects/affect_messages.h" #include "administration/privilege.h" @@ -118,9 +119,11 @@ void do_affects(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { for (const auto &r : rows) { // A permanent source wins the label; otherwise show the longest remaining time. FormatAffectDuration(r.permanent ? -1 : r.best_mod, buf2, sizeof(buf2)); - snprintf(buf, kMaxStringLength, "%s%s%-21s %-12s%s", + // Ширина колонок -- в символах: fmt для корректного UTF-8 меряет её в кодовых + // точках, printf мерил бы в байтах (issue #3681). + strcpy(buf, fmt::format("{}{}{:<21} {:<12}{}", (!r.name.empty() && r.name[0] == '!') ? "Состояние : " : "Заклинание : ", - kColorBoldCyn, r.name.c_str(), buf2, kColorNrm); + kColorBoldCyn, r.name.c_str(), buf2, kColorNrm).c_str()); if (r.count > 1) { snprintf(buf + strlen(buf), kMaxStringLength - strlen(buf), " [x%d]", r.count); } @@ -140,9 +143,11 @@ void do_affects(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { snprintf(sp_name, sizeof(sp_name), "%s", affects::AffectMsg(aff->affect_type, affects::EAffectMsgType::kShortDesc).c_str()); FormatAffectDuration(AffectDisplayMod(aff), buf2, sizeof(buf2)); - snprintf(buf, kMaxStringLength, "%s%s%-21s %-12s%s ", + // Ширина колонок -- в символах: fmt для корректного UTF-8 меряет её в кодовых + // точках, printf мерил бы в байтах (issue #3681). + strcpy(buf, fmt::format("{}{}{:<21} {:<12}{} ", *sp_name == '!' ? "Состояние : " : "Заклинание : ", - kColorBoldCyn, sp_name, buf2, kColorNrm); + kColorBoldCyn, sp_name, buf2, kColorNrm).c_str()); *buf2 = '\0'; if (immortal) { if (aff->modifier) { diff --git a/src/engine/ui/cmd/do_skills.cpp b/src/engine/ui/cmd/do_skills.cpp index a3ff7c1d7..4197a913d 100644 --- a/src/engine/ui/cmd/do_skills.cpp +++ b/src/engine/ui/cmd/do_skills.cpp @@ -1,4 +1,5 @@ #include "do_skills.h" +#include "utils/native_text.h" #include "engine/ui/color.h" #include "engine/entities/char_data.h" @@ -77,8 +78,9 @@ void DisplaySkills(CharData *ch, CharData *vict, const char *filter/* = nullptr* default: sprintf(buf, " "); } - sprintf(buf + strlen(buf), "%-23s %s (%d)%s \r\n", - skill.GetName(), + // Ширина колонки - в символах, а не в байтах (issue #3681). + sprintf(buf + strlen(buf), "%s %s (%d)%s \r\n", + native_text::pad_right(skill.GetName(), 23).c_str(), how_good(GetSkill(ch, skill_id), CalcSkillHardCap(ch, skill_id)), GetTrainedSkill(ch, skill_id) == 0 ? GetEquippedSkill(ch, skill_id) : std::min(CalcSkillMinCap(ch, skill_id) + GetEquippedSkill(ch, skill_id), MUD::Skill(skill_id).cap), diff --git a/src/engine/ui/cmd/do_where.cpp b/src/engine/ui/cmd/do_where.cpp index 73ad5cb5e..c408fcb78 100644 --- a/src/engine/ui/cmd/do_where.cpp +++ b/src/engine/ui/cmd/do_where.cpp @@ -3,6 +3,7 @@ // #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/world_objects.h" #include "gameplay/economics/exchange.h" @@ -145,7 +146,9 @@ void PerformMortalWhere(CharData *ch, char *arg) { continue; } - sprintf(buf, "%-20s - %s\r\n", GET_NAME(i), world[i->in_room]->name); + // Ширина колонки - в символах, а не в байтах (issue #3681). + sprintf(buf, "%s - %s\r\n", + native_text::pad_right(GET_NAME(i), 20).c_str(), world[i->in_room]->name); SendMsgToChar(buf, ch); } } else // print only FIRST char, not all. @@ -165,7 +168,8 @@ void PerformMortalWhere(CharData *ch, char *arg) { continue; } - sprintf(buf, "%-25s - %s\r\n", GET_NAME(i), world[i->in_room]->name); + sprintf(buf, "%s - %s\r\n", + native_text::pad_right(GET_NAME(i), 25).c_str(), world[i->in_room]->name); SendMsgToChar(buf, ch); return; } diff --git a/src/engine/ui/cmd/do_who.cpp b/src/engine/ui/cmd/do_who.cpp index 6dfbbb12f..d40824637 100644 --- a/src/engine/ui/cmd/do_who.cpp +++ b/src/engine/ui/cmd/do_who.cpp @@ -3,6 +3,7 @@ // #include "engine/ui/cmd/do_who.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "utils/grammar/gender.h" #include "gameplay/mechanics/sight.h" @@ -164,15 +165,19 @@ void DoWho(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (short_list) { char tmp[kMaxInputLength]; snprintf(tmp, sizeof(tmp), "%s%s%s", GetPkNameColor(tch), GET_NAME(tch), kColorNrm); + // Ширина колонки - в символах, а не в байтах (issue #3681): "%-30s" отсчитывает + // байты, из-за чего колонка с русским именем под UTF-8 выходит вдвое уже. if (privilege::IsImpl(ch) || ch->IsFlagged(EPrf::kCoderinfo)) { - sprintf(buf, "%s[%2d %s] %-30s%s", + sprintf(buf, "%s[%2d %s] %s%s", privilege::IsGod(tch.get()) ? kColorWht : "", GetRealLevel(tch), MUD::Class(tch->GetClass()).GetCName(), - tmp, privilege::IsGod(tch.get()) ? kColorNrm : ""); + native_text::pad_right(tmp, 30).c_str(), + privilege::IsGod(tch.get()) ? kColorNrm : ""); } else { - sprintf(buf, "%s%-30s%s", + sprintf(buf, "%s%s%s", privilege::IsImmortal(tch.get()) ? kColorWht : "", - tmp, privilege::IsImmortal(tch.get()) ? kColorNrm : ""); + native_text::pad_right(tmp, 30).c_str(), + privilege::IsImmortal(tch.get()) ? kColorNrm : ""); } } else { if (privilege::IsImpl(ch) diff --git a/src/engine/ui/table_wrapper.h b/src/engine/ui/table_wrapper.h index 94d684c47..e3a36238d 100644 --- a/src/engine/ui/table_wrapper.h +++ b/src/engine/ui/table_wrapper.h @@ -41,9 +41,21 @@ using Color = fort::color; using TextAlign = fort::text_align; /** - * Таблица в стандартной, не unicode кодировке. + * Базовый тип таблицы выбирается под нативную кодировку движка (issue #3681). + * libfort считает ширину ячейки по-разному: char_table меряет байтами, utf8_table - кодовыми + * точками. Под KOI8-R (1 байт = 1 символ) верен первый, под UTF-8 - второй; при неверном выборе + * колонки с русским текстом съезжают вдвое. */ -class Table : public fort::char_table { +#ifdef INTERNAL_ENCODING_UTF8 +using TableBase = fort::utf8_table; +#else +using TableBase = fort::char_table; +#endif + +/** + * Таблица в нативной кодировке движка. + */ +class Table : public TableBase { public: void SetColumnAlign(std::size_t column_index, TextAlign align) { this->column(column_index).set_cell_text_align(align); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 8c72edd93..8ee147504 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -329,6 +329,27 @@ std::size_t last_char_offset(std::string_view s) { return last; } +namespace { + +std::size_t missing_width(std::string_view s, std::size_t width) { + const std::size_t have = char_count(s); + return have >= width ? 0 : width - have; +} + +} // namespace + +std::string pad_right(std::string_view s, std::size_t width, char fill) { + std::string out(s); + out.append(missing_width(s, width), fill); + return out; +} + +std::string pad_left(std::string_view s, std::size_t width, char fill) { + std::string out(missing_width(s, width), fill); + out.append(s); + return out; +} + bool list_contains_char(std::string_view list, std::string_view ch) { if (ch.empty()) { return false; diff --git a/src/utils/native_text.h b/src/utils/native_text.h index d9ba7a3ce..47deedb98 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -86,6 +86,14 @@ std::size_t copy_lower_char(const char *src, char *dst); // is that character. KOI8-R: s.size() - 1. std::size_t last_char_offset(std::string_view s); +// Pad `s` to at least `width` display characters, appending (pad_right) or prepending (pad_left) +// `fill`. The replacement for a printf "%-Ns" / "%Ns" field, which counts bytes: under KOI8-R the +// result is identical, under UTF-8 a Russian column no longer comes out half as wide. +// Note: like printf, colour codes embedded in `s` still count toward the width -- that is a +// separate, pre-existing skew and is deliberately not changed here. +std::string pad_right(std::string_view s, std::size_t width, char fill = ' '); +std::string pad_left(std::string_view s, std::size_t width, char fill = ' '); + // Does the single character `ch` occur in `list`? The replacement for strchr() over a literal // list of letters: `list` is walked one whole character at a time, so a multibyte character can // never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 17bc01c33..b20f2de83 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace { @@ -252,6 +253,22 @@ TEST(NativeText, LastCharOffset) { } } +TEST(NativeText, PadRightAndLeft) { + EXPECT_EQ(native_text::pad_right("ab", 5), "ab "); + EXPECT_EQ(native_text::pad_left("ab", 5), " ab"); + EXPECT_EQ(native_text::pad_right("ab", 2), "ab"); // already wide enough + EXPECT_EQ(native_text::pad_right("abcdef", 3), "abcdef"); // never truncates + EXPECT_EQ(native_text::pad_right("", 3), " "); + EXPECT_EQ(native_text::pad_right("ab", 4, '.'), "ab.."); + if (native_text::native_is_utf8()) { + // 6 Cyrillic characters (12 bytes) padded to a 10-character column: no padding, and + // crucially not 12 bytes' worth of "already too wide" either. + EXPECT_EQ(native_text::pad_right(kNtPrivet, 10), kNtPrivet); + // 6 characters padded to 8 -> exactly two spaces, not eight. + EXPECT_EQ(native_text::pad_right(kNtPrivet, 8), std::string(kNtPrivet) + " "); + } +} + TEST(NativeText, ListContainsChar) { EXPECT_TRUE(native_text::list_contains_char("abc", "b")); EXPECT_FALSE(native_text::list_contains_char("abc", "d")); From 360c6ee4141a3de3d43d36705e71fbf90d7675a6 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 08:13:07 +0200 Subject: [PATCH 010/110] feat(utf8): character-width columns across the remaining game screens (#3681) Continues A3 with the player-facing subsystems: score (the grouping line, which also capped its text with substr(0, 76) -- a byte cut), spellbook, features, exits, PK list, crafting recipes and item creation, glory stats, parcels and named items. Adds native_text::truncate_to_chars() for the "cap a display string" case, so a cap never lands inside a character. Skipped after checking: do_levels pads only digit strings (thousands_sep), so its "%13s" fields are ASCII-only and need nothing. Verified: full suite 649 passed / 0 failed, clean build, and a live boot on the production world with both a UTF-8 and a legacy KOI8-R client rendering correctly. --- src/engine/ui/cmd/do_exits.cpp | 5 +++-- src/engine/ui/cmd/do_features.cpp | 9 +++++---- src/engine/ui/cmd/do_score.cpp | 7 ++++--- src/engine/ui/cmd/do_spells.cpp | 11 +++++++---- src/gameplay/communication/parcel.cpp | 5 +++-- src/gameplay/crafting/im.cpp | 10 +++++----- src/gameplay/crafting/item_creation.cpp | 8 +++++--- src/gameplay/fight/pk.cpp | 17 +++++++++-------- src/gameplay/mechanics/glory_const.cpp | 4 +++- src/gameplay/mechanics/named_stuff.cpp | 11 ++++++----- src/utils/native_text.cpp | 10 ++++++++++ src/utils/native_text.h | 5 +++++ 12 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/engine/ui/cmd/do_exits.cpp b/src/engine/ui/cmd/do_exits.cpp index e5549e9ed..219b0706b 100644 --- a/src/engine/ui/cmd/do_exits.cpp +++ b/src/engine/ui/cmd/do_exits.cpp @@ -6,6 +6,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/mechanics/sight.h" #include "gameplay/mechanics/illumination.h" @@ -29,10 +30,10 @@ void DoExits(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { for (door = 0; door < EDirection::kMaxDirNum; door++) if (EXIT(ch, door) && EXIT(ch, door)->to_room() != kNowhere && !EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kClosed)) { if (privilege::IsGod(ch)) - sprintf(buf2, "%-6s - [%5d] %s\r\n", dirs_rus[door], + sprintf(buf2, "%s - [%5d] %s\r\n", native_text::pad_right(dirs_rus[door], 6).c_str(), GET_ROOM_VNUM(EXIT(ch, door)->to_room()), world[EXIT(ch, door)->to_room()]->name); else { - sprintf(buf2, "%-6s - ", dirs_rus[door]); + sprintf(buf2, "%s - ", native_text::pad_right(dirs_rus[door], 6).c_str()); if (is_dark(EXIT(ch, door)->to_room()) && !sight::CanSeeInDark(ch)) strcat(buf2, "слишком темно\r\n"); else { diff --git a/src/engine/ui/cmd/do_features.cpp b/src/engine/ui/cmd/do_features.cpp index 406081057..a48d26e9d 100644 --- a/src/engine/ui/cmd/do_features.cpp +++ b/src/engine/ui/cmd/do_features.cpp @@ -1,4 +1,5 @@ #include "engine/ui/color.h" +#include "utils/native_text.h" #include "gameplay/core/remort.h" #include "engine/entities/char_data.h" #include "gameplay/abilities/timed_abilities.h" @@ -81,17 +82,17 @@ void DisplayFeats(CharData *ch, CharData *vict, bool all_feats) { continue; } if (!ch->IsFlagged(EPrf::kBlindMode)) { - sprintf(buf, " %s%s %-30s%s\r\n", + sprintf(buf, " %s%s %s%s\r\n", ch->HaveFeat(feat.GetId()) ? kColorGrn : CanGetFeat(ch, feat.GetId()) ? kColorNrm : kColorRed, ch->HaveFeat(feat.GetId()) ? "[И]" : CanGetFeat(ch, feat.GetId()) ? "[Д]" : "[Н]", - MUD::Feat(feat.GetId()).GetCName(), kColorNrm); + native_text::pad_right(MUD::Feat(feat.GetId()).GetCName(), 30).c_str(), kColorNrm); } else { - sprintf(buf, " %s %-30s\r\n", + sprintf(buf, " %s %s\r\n", ch->HaveFeat(feat.GetId()) ? "[И]" : CanGetFeat(ch, feat.GetId()) ? "[Д]" : "[Н]", - MUD::Feat(feat.GetId()).GetCName()); + native_text::pad_right(MUD::Feat(feat.GetId()).GetCName(), 30).c_str()); } if (feat.IsInborn() || diff --git a/src/engine/ui/cmd/do_score.cpp b/src/engine/ui/cmd/do_score.cpp index 53e91692c..12e5aedc0 100644 --- a/src/engine/ui/cmd/do_score.cpp +++ b/src/engine/ui/cmd/do_score.cpp @@ -7,6 +7,7 @@ */ #include "engine/ui/color.h" +#include "utils/native_text.h" #include "gameplay/affects/affect_messages.h" #include "utils/utils_string.h" #include "gameplay/core/experience.h" @@ -232,12 +233,12 @@ void PrintScoreList(CharData *ch) { } else if (NAME_BAD(ch)) { SendMsgToChar(ch, "ВНИМАНИЕ! ваше имя запрещено богами. Очень скоро вы прекратите получать опыт.\r\n"); } - SendMsgToChar(ch, "Вы можете вступить в группу с максимальной разницей в %2d %-75s\r\n", + SendMsgToChar(ch, "Вы можете вступить в группу с максимальной разницей в %2d %s\r\n", grouping[ch->GetClass()][static_cast(remort::GetRealRemort(ch))], - (std::string( + native_text::pad_right(native_text::truncate_to_chars(std::string( grammar::GetDeclensionInNumber(grouping[ch->GetClass()][static_cast(remort::GetRealRemort( ch))], grammar::EWhat::kLvl) - + std::string(" без потерь для опыта.")).substr(0, 76).c_str())); + + std::string(" без потерь для опыта.")), 76), 75).c_str()); SendMsgToChar(ch, "Вы можете принять в группу максимум %d соратников.\r\n", group::max_group_size(ch)); std::ostringstream out; diff --git a/src/engine/ui/cmd/do_spells.cpp b/src/engine/ui/cmd/do_spells.cpp index 690bfbb6a..f8eb53df5 100644 --- a/src/engine/ui/cmd/do_spells.cpp +++ b/src/engine/ui/cmd/do_spells.cpp @@ -1,4 +1,5 @@ #include "do_spells.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/mechanics/magic_item.h" @@ -88,15 +89,17 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { continue; if (CheckRecipeItems(ch, spell_id, ESpellType::kRunes, false)) { slots[slot_num] += sprintf(names[slot_num] + slots[slot_num], - "%s|<...%4d.> %s%-38s&n|", + "%s|<...%4d.> %s%s&n|", slots[slot_num] % 114 < 10 ? "\r\n" : " ", - CalcSpellManacost(ch, spell_id), GetSpellColor(spell_id), MUD::Spell(spell_id).GetCName()); + CalcSpellManacost(ch, spell_id), GetSpellColor(spell_id), + native_text::pad_right(MUD::Spell(spell_id).GetCName(), 38).c_str()); } else { if (all) { slots[slot_num] += sprintf(names[slot_num] + slots[slot_num], - "%s|+--------+ %s%-38s&n|", slots[slot_num] % 114 < 10 ? "\r\n" - : " ", GetSpellColor(spell_id), MUD::Spell(spell_id).GetCName()); + "%s|+--------+ %s%s&n|", slots[slot_num] % 114 < 10 ? "\r\n" + : " ", GetSpellColor(spell_id), + native_text::pad_right(MUD::Spell(spell_id).GetCName(), 38).c_str()); } } } else { diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 5e288d57a..b117805da 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -3,6 +3,7 @@ // Part of Bylins http://www.mud.ru #include "parcel.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "gameplay/economics/currencies.h" @@ -828,10 +829,10 @@ bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { std::string sender = GetNameByUnique(it2->first); found = true; - SendMsgToChar(ch, "%2d. [%6d] %-25s - наход%sся на почте (отправитель: %s, получатель: %s).\r\n", + SendMsgToChar(ch, "%2d. [%6d] %s - наход%sся на почте (отправитель: %s, получатель: %s).\r\n", num++, GET_OBJ_VNUM(it3->obj_.get()), - it3->obj_->get_short_description().c_str(), + native_text::pad_right(it3->obj_->get_short_description(), 25).c_str(), grammar::ObjPluralVerbEnding((it3->obj_)->get_sex()), sender.c_str(), target.c_str()); diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index 81ede15a6..ead6fddec 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -953,13 +953,13 @@ void list_recipes(CharData *ch, bool all_recipes) { rs = im_get_char_rskill(ch, sortpos); const bool unavailable = req->level > GetRealLevel(ch) || req->remort > remort::GetRealRemort(ch); if (!ch->IsFlagged(EPrf::kBlindMode)) { - sprintf(buf, " %s%-30s%s %2d (%2d)%s\r\n", + sprintf(buf, " %s%s%s %2d (%2d)%s\r\n", unavailable ? kColorRed : rs ? kColorGrn : kColorNrm, - imrecipes[sortpos].name, kColorCyn, + native_text::pad_right(imrecipes[sortpos].name, 30).c_str(), kColorCyn, req->level, req->remort, kColorNrm); } else { - sprintf(buf, " %s %-30s %2d (%2d)\r\n", - unavailable ? "[Н]" : rs ? "[И]" : "[Д]", imrecipes[sortpos].name, + sprintf(buf, " %s %s %2d (%2d)\r\n", + unavailable ? "[Н]" : rs ? "[И]" : "[Д]", native_text::pad_right(imrecipes[sortpos].name, 30).c_str(), req->level, req->remort); } strcat(buf1, buf); @@ -982,7 +982,7 @@ void list_recipes(CharData *ch, bool all_recipes) { } if (rs->perc <= 0) continue; - sprintf(buf, "%-30s %s%s\r\n", imrecipes[rs->rid].name, how_good(rs->perc, kMaxRecipeLevel), kColorBoldBlk); + sprintf(buf, "%s %s%s\r\n", native_text::pad_right(imrecipes[rs->rid].name, 30).c_str(), how_good(rs->perc, kMaxRecipeLevel), kColorBoldBlk); strcat(buf2, buf); ++i; } diff --git a/src/gameplay/crafting/item_creation.cpp b/src/gameplay/crafting/item_creation.cpp index 05a066ece..7ada01f71 100644 --- a/src/gameplay/crafting/item_creation.cpp +++ b/src/gameplay/crafting/item_creation.cpp @@ -7,6 +7,7 @@ * $Revision$ * ************************************************************************ */ #include "item_creation.h" +#include "utils/native_text.h" #include "utils/utils_parse.h" #include "utils/parser_wrapper.h" #include "administration/privilege.h" @@ -333,8 +334,9 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ } j++; } - sprintf(tmpbuf, "%3zd %-1s %-6s %-40s(%5d) :", - i + 1, (trec->locked ? "*" : " "), skill_name.c_str(), obj_name.c_str(), trec->obj_proto); + sprintf(tmpbuf, "%3zd %-1s %s %s(%5d) :", + i + 1, (trec->locked ? "*" : " "), native_text::pad_right(skill_name, 6).c_str(), + native_text::pad_right(obj_name, 40).c_str(), trec->obj_proto); tmpstr += string(tmpbuf); for (int j = 0; j < MAX_PARTS; j++) { if (trec->parts[j].proto != 0) { @@ -344,7 +346,7 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ } else { obj_name = "Нет"; } - sprintf(tmpbuf, " %-35s(%5d)", obj_name.c_str(), trec->parts[j].proto); + sprintf(tmpbuf, " %s(%5d)", native_text::pad_right(obj_name, 35).c_str(), trec->parts[j].proto); if (j > 0) { if (j % 2 == 0) { // разбиваем строчки если ингров больше 2; diff --git a/src/gameplay/fight/pk.cpp b/src/gameplay/fight/pk.cpp index d927493e2..66ea9fc89 100644 --- a/src/gameplay/fight/pk.cpp +++ b/src/gameplay/fight/pk.cpp @@ -12,6 +12,7 @@ ************************************************************************ */ #include "pk.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" #include "gameplay/mechanics/mount.h" @@ -663,9 +664,9 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (tch->get_uid() == uid) { found = true; if (pk.battle_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %-40s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", temp.c_str()); + sprintf(buf + strlen(buf), " %s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", native_text::pad_right(temp, 40).c_str()); } else { - sprintf(buf + strlen(buf), " %-40s %3ld %3ld\r\n", temp.c_str(), pk.kill_num, pk.revenge_num); + sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", native_text::pad_right(temp, 40).c_str(), pk.kill_num, pk.revenge_num); } break; } @@ -673,9 +674,9 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else { found = true; if (pk.battle_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %-40s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", temp.c_str()); + sprintf(buf + strlen(buf), " %s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", native_text::pad_right(temp, 40).c_str()); } else { - sprintf(buf + strlen(buf), " %-40s %3ld %3ld\r\n", temp.c_str(), pk.kill_num, pk.revenge_num); + sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", native_text::pad_right(temp, 40).c_str(), pk.kill_num, pk.revenge_num); } } } @@ -708,12 +709,12 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { // Сначала проверка клан флага if (CLAN(ch) && pk.clan_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %-40s <ВОЙНА>\r\n", GET_NAME(tch)); + sprintf(buf + strlen(buf), " %s <ВОЙНА>\r\n", native_text::pad_right(GET_NAME(tch), 40).c_str()); } else if (pk.clan_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %-40s <ВРЕМЕННЫЙ ФЛАГ>\r\n", GET_NAME(tch)); + sprintf(buf + strlen(buf), " %s <ВРЕМЕННЫЙ ФЛАГ>\r\n", native_text::pad_right(GET_NAME(tch), 40).c_str()); } else if (pk.kill_num + pk.revenge_num > 0) { - sprintf(buf + strlen(buf), " %-40s %3ld %3ld\r\n", - GET_NAME(tch), pk.kill_num, pk.revenge_num); + sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", + native_text::pad_right(GET_NAME(tch), 40).c_str(), pk.kill_num, pk.revenge_num); } else { continue; } diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index 6edfc7fb3..121cefa25 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -4,6 +4,7 @@ // Part of Bylins http://www.mud.ru #include "glory_const.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "utils/grammar/declensions.h" @@ -226,7 +227,8 @@ void print_glory(CharData *ch, GloryListType::iterator &it) { *buf = '\0'; for (auto i = it->second->stats.begin(), iend = it->second->stats.end(); i != iend; ++i) { if ((i->first >= 0) && (i->first < (int) sizeof(olc_stat_name))) { - sprintf(buf + strlen(buf), "%-16s: +%d", olc_stat_name[i->first], i->second * stat_multi(i->first)); + sprintf(buf + strlen(buf), "%s: +%d", native_text::pad_right(olc_stat_name[i->first], 16).c_str(), + i->second * stat_multi(i->first)); if (stat_multi(i->first) > 1) sprintf(buf + strlen(buf), "(%d)", i->second); strcat(buf, "\r\n"); diff --git a/src/gameplay/mechanics/named_stuff.cpp b/src/gameplay/mechanics/named_stuff.cpp index 078faf8e5..be9b96175 100644 --- a/src/gameplay/mechanics/named_stuff.cpp +++ b/src/gameplay/mechanics/named_stuff.cpp @@ -3,6 +3,7 @@ // Part of Bylins http://www.mud.ru #include "named_stuff.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" @@ -369,10 +370,10 @@ void do_named(CharData *ch, char *argument, int cmd, int subcmd) { out += buf1; } found++; - sprintf(buf2, "%6ld) &R*&n%-31s Владелец:%-16s e-mail:&S%s&s\r\n", + sprintf(buf2, "%6ld) &R*&n%s Владелец:%s e-mail:&S%s&s\r\n", it->first + 1, - "Несуществующий предмет", - GetNameByUnique(it->second->uid, false).c_str(), + native_text::pad_right("Несуществующий предмет", 31).c_str(), + native_text::pad_right(GetNameByUnique(it->second->uid, false), 16).c_str(), str_dup(it->second->mail.c_str()) ); out += buf2; @@ -388,9 +389,9 @@ void do_named(CharData *ch, char *argument, int cmd, int subcmd) { obj_proto[r_num]->get_vnum(), colored_name(obj_proto[r_num]->get_short_description().c_str(), -32)); if (privilege::IsGrGod(ch) || ch->IsFlagged(EPrf::kCoderinfo)) { - snprintf(buf2, kMaxStringLength, "%s Игра:%d Пост:%d Владелец:%-16s e-mail:&S%s&s\r\n", buf1, + snprintf(buf2, kMaxStringLength, "%s Игра:%d Пост:%d Владелец:%s e-mail:&S%s&s\r\n", buf1, obj_proto.total_online(r_num), obj_proto.stored(r_num), - GetNameByUnique(it->second->uid, false).c_str(), it->second->mail.c_str()); + native_text::pad_right(GetNameByUnique(it->second->uid, false), 16).c_str(), it->second->mail.c_str()); } else { snprintf(buf2, kMaxStringLength, "%s\r\n", buf1); } diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 8ee147504..679e84ecb 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -338,6 +338,16 @@ std::size_t missing_width(std::string_view s, std::size_t width) { } // namespace +std::string truncate_to_chars(std::string_view s, std::size_t count) { + std::size_t pos = 0; + std::size_t seen = 0; + while (pos < s.size() && seen < count) { + pos += char_bytes_at(s, pos); + ++seen; + } + return std::string(s.substr(0, pos)); +} + std::string pad_right(std::string_view s, std::size_t width, char fill) { std::string out(s); out.append(missing_width(s, width), fill); diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 47deedb98..26d7710bd 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -94,6 +94,11 @@ std::size_t last_char_offset(std::string_view s); std::string pad_right(std::string_view s, std::size_t width, char fill = ' '); std::string pad_left(std::string_view s, std::size_t width, char fill = ' '); +// First `count` characters of `s` (all of it when shorter). The replacement for a substr(0, N) +// used to cap a display length: under KOI8-R it is exactly that, under UTF-8 it counts characters +// and so never cuts one in half. +std::string truncate_to_chars(std::string_view s, std::size_t count); + // Does the single character `ch` occur in `list`? The replacement for strchr() over a literal // list of letters: `list` is walked one whole character at a time, so a multibyte character can // never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. From 455521c67fd040fb2559a7f515f1b811a92da159 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 3 Aug 2026 08:16:53 +0200 Subject: [PATCH 011/110] feat(utf8): character-width columns in god commands, closing track A3 (#3681) Finishes A3 with the immortal-facing listings: show (rune spells, linkdrop, snoop), last, users, tabulate, liblist, commands and alias, plus the trigger listing in dg_scripts. The substr(0, N) display caps in show and liblist now go through truncate_to_chars so they cannot split a character. do_users shares one format string across three call sites; it becomes plain "%s" fields with each column padded explicitly. Checked and deliberately skipped: * fmt::sprintf (do_stat) -- measured like fmt::format, already correct in both encodings, so the "%-21s" there needs nothing; * "%-s" in exchange -- a left-align flag with no width, so it never pads; * show_fields/set_fields command names and do_levels' thousands_sep columns -- ASCII-only content; * do_toggle's "%-3s" -- the values are all the same width, so nothing shifts. Verified: full suite 649 passed / 0 failed, clean build. --- src/engine/scripting/dg_scripts.cpp | 4 ++-- src/engine/ui/cmd/do_alias.cpp | 3 ++- src/engine/ui/cmd/do_commands.cpp | 7 ++++--- src/engine/ui/cmd_god/do_last.cpp | 10 ++++++--- src/engine/ui/cmd_god/do_liblist.cpp | 3 ++- src/engine/ui/cmd_god/do_show.cpp | 20 +++++++++--------- src/engine/ui/cmd_god/do_tabulate.cpp | 8 +++++--- src/engine/ui/cmd_god/do_users.cpp | 29 ++++++++++++++++----------- 8 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 834c8fdeb..1027381d9 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -6283,8 +6283,8 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { char trgtypes[256]; for (; nr < top_of_trigt && (trig_index[nr]->vnum <= last); nr++) { std::string out = ""; - snprintf(buf, sizeof(buf), "%2d) [%5d] %-50s ", ++found, - trig_index[nr]->vnum, trig_index[nr]->proto->get_name().c_str()); + snprintf(buf, sizeof(buf), "%2d) [%5d] %s ", ++found, + trig_index[nr]->vnum, native_text::pad_right(trig_index[nr]->proto->get_name(), 50).c_str()); out += buf; if (trig_index[nr]->proto->get_attach_type() == MOB_TRIGGER) { sprintbit(trig_index[nr]->proto->get_trigger_type(), trig_types, trgtypes, sizeof(trgtypes)); diff --git a/src/engine/ui/cmd/do_alias.cpp b/src/engine/ui/cmd/do_alias.cpp index de102d0e2..f6050cd85 100644 --- a/src/engine/ui/cmd/do_alias.cpp +++ b/src/engine/ui/cmd/do_alias.cpp @@ -7,6 +7,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "engine/ui/alias.h" void do_alias(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -24,7 +25,7 @@ void do_alias(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar(" Нет алиасов.\r\n", ch); else { while (a != nullptr) { - sprintf(buf, "%-15s %s\r\n", a->alias, a->replacement); + sprintf(buf, "%s %s\r\n", native_text::pad_right(a->alias, 15).c_str(), a->replacement); SendMsgToChar(buf, ch); a = a->next; } diff --git a/src/engine/ui/cmd/do_commands.cpp b/src/engine/ui/cmd/do_commands.cpp index 85c460ba0..b84bdb180 100644 --- a/src/engine/ui/cmd/do_commands.cpp +++ b/src/engine/ui/cmd/do_commands.cpp @@ -7,6 +7,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/communication/social.h" #include "engine/db/global_objects.h" @@ -37,7 +38,7 @@ void do_commands(CharData *ch, char *argument, int/* cmd*/, int subcmd) { continue; } for (const auto &kw : soc.GetKeywords()) { - sprintf(buf + strlen(buf), "%-19s", kw.c_str()); + sprintf(buf + strlen(buf), "%s", native_text::pad_right(kw, 19).c_str()); if (!(no % 4)) strcat(buf, "\r\n"); no++; @@ -49,13 +50,13 @@ void do_commands(CharData *ch, char *argument, int/* cmd*/, int subcmd) { i = cmd_sort_info[cmd_num].sort_pos; if (wizhelp) { if (privilege::HasPrivilege(vict, std::string(cmd_info[i].command), 0, 0, 0)) { - sprintf(buf + strlen(buf), "%-15s", cmd_info[i].command); + sprintf(buf + strlen(buf), "%s", native_text::pad_right(cmd_info[i].command, 15).c_str()); if (!(no % 5)) strcat(buf, "\r\n"); no++; } } else if (cmd_info[i].minimum_level >= 0 && (static_cast(socials) == cmd_sort_info[i].is_social)) { - sprintf(buf + strlen(buf), "%-15s", cmd_info[i].command); + sprintf(buf + strlen(buf), "%s", native_text::pad_right(cmd_info[i].command, 15).c_str()); if (!(no % 5)) strcat(buf, "\r\n"); no++; diff --git a/src/engine/ui/cmd_god/do_last.cpp b/src/engine/ui/cmd_god/do_last.cpp index b1286ab21..bf72a352a 100644 --- a/src/engine/ui/cmd_god/do_last.cpp +++ b/src/engine/ui/cmd_god/do_last.cpp @@ -7,6 +7,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/entities/char_player.h" #include "engine/db/global_objects.h" @@ -28,10 +29,13 @@ void DoPageLastLogins(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) SendMsgToChar("Вы не столь уж и божественны для этого.\r\n", ch); } else { time_t tmp_time = chdata->get_last_logon(); - sprintf(buf, "[%5ld] [%2d %s] %-12s : %-18s : %-20s\r\n", + sprintf(buf, "[%5ld] [%2d %s] %s : %s : %s\r\n", chdata->get_uid(), GetRealLevel(chdata), - MUD::Class(chdata->GetClass()).GetAbbr().c_str(), GET_NAME(chdata), - chdata->player_specials->saved.LastIP[0] ? chdata->player_specials->saved.LastIP : "Unknown", ctime(&tmp_time)); + MUD::Class(chdata->GetClass()).GetAbbr().c_str(), + native_text::pad_right(GET_NAME(chdata), 12).c_str(), + native_text::pad_right(chdata->player_specials->saved.LastIP[0] + ? chdata->player_specials->saved.LastIP : "Unknown", 18).c_str(), + native_text::pad_right(ctime(&tmp_time), 20).c_str()); SendMsgToChar(buf, ch); } } diff --git a/src/engine/ui/cmd_god/do_liblist.cpp b/src/engine/ui/cmd_god/do_liblist.cpp index 66abc672d..8ff33f548 100644 --- a/src/engine/ui/cmd_god/do_liblist.cpp +++ b/src/engine/ui/cmd_god/do_liblist.cpp @@ -7,6 +7,7 @@ */ #include "do_liblist.h" +#include "utils/native_text.h" #include "engine/entities/char_data.h" #include "engine/db/obj_prototypes.h" @@ -264,7 +265,7 @@ void Print(CharData *ch, int first, int last, const std::string &options) { for (int i = 0; i <= top_of_mobt; ++i) { if (mob_index[i].vnum >= first && mob_index[i].vnum <= last) { fmt::format_to(std::back_inserter(out), "{:5}. {:<45} [{:<6}] [{:<2}]{}", - ++cnt, mob_proto[i].get_name_str().substr(0, 45), + ++cnt, native_text::truncate_to_chars(mob_proto[i].get_name_str(), 45), mob_index[i].vnum, mob_proto[i].GetLevel(), PrintFlag(mob_proto + i, options)); if (!mob_proto[i].proto_script->empty()) { diff --git a/src/engine/ui/cmd_god/do_show.cpp b/src/engine/ui/cmd_god/do_show.cpp index 1bc4d0adf..9831f1a5e 100644 --- a/src/engine/ui/cmd_god/do_show.cpp +++ b/src/engine/ui/cmd_god/do_show.cpp @@ -3,6 +3,7 @@ // #include "administration/accounts.h" +#include "utils/native_text.h" #include "administration/ban.h" #include "administration/privilege.h" #include "engine/ui/cmd/do_features.h" @@ -254,10 +255,10 @@ void print_mob_bosses(CharData *ch, bool lvl_sort) { const auto vnum = GET_MOB_VNUM(mob); out += fmt::format("{:<3} {:<31}s [{:<2}][{:<6}] {:<31}s\r\n", ++cnt, - mob->get_name_str().substr(0, 31), + native_text::truncate_to_chars(mob->get_name_str(), 31), zone_table[mob_index[mob_rnum].zone].mob_level, vnum, - zone_name_str.substr(0, 31)); + native_text::truncate_to_chars(zone_name_str, 31)); } page_string(ch->desc, out); } @@ -460,8 +461,8 @@ void ListSpellCreate(CharData *ch) { if (r > 0) runes_str += '|'; runes_str += std::to_string(info.runes[r]); } - SendMsgToChar(ch, "%3d) Rune spell [%3d] &W%-30s&n runes: %s level %d\r\n", - ++i, to_underlying(spell_id), MUD::Spell(spell_id).GetCName(), + SendMsgToChar(ch, "%3d) Rune spell [%3d] &W%s&n runes: %s level %d\r\n", + ++i, to_underlying(spell_id), native_text::pad_right(MUD::Spell(spell_id).GetCName(), 30).c_str(), runes_str.c_str(), info.min_caster_level); } } @@ -718,8 +719,8 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { && ((sight::CanSee(ch, d->character) && GetRealLevel(ch) >= GetRealLevel(d->character)) || ch->IsFlagged(EPrf::kCoderinfo))) { sprintf(buf + strlen(buf), - "%-10s - подслушивается %s (map %s).\r\n", - GET_NAME(d->snooping->character), + "%s - подслушивается %s (map %s).\r\n", + native_text::pad_right(GET_NAME(d->snooping->character), 10).c_str(), GET_PAD(d->character, 4), d->snoop_with_map ? "on" : "off"); } @@ -728,7 +729,8 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; // snoop case 9: // show linkdrop SendMsgToChar(" Список игроков в состоянии 'link drop'\r\n", ch); - sprintf(buf, "%-50s%-16s %s\r\n", " Имя", "Комната", "Бездействие (тики)"); + sprintf(buf, "%s%s %s\r\n", native_text::pad_right(" Имя", 50).c_str(), + native_text::pad_right("Комната", 16).c_str(), "Бездействие (тики)"); SendMsgToChar(buf, ch); i = 0; for (const auto &character : character_list) { @@ -737,8 +739,8 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { continue; } ++i; - sprintf(buf, "%-50s[%6d][%6d] %d\r\n", - character->GetNameWithTitleOrRace().c_str(), GET_ROOM_VNUM(character->in_room), + sprintf(buf, "%s[%6d][%6d] %d\r\n", + native_text::pad_right(character->GetNameWithTitleOrRace(), 50).c_str(), GET_ROOM_VNUM(character->in_room), GET_ROOM_VNUM(character->get_was_in_room()), character->char_specials.timer); SendMsgToChar(buf, ch); } diff --git a/src/engine/ui/cmd_god/do_tabulate.cpp b/src/engine/ui/cmd_god/do_tabulate.cpp index c11c6d52e..f333fd324 100644 --- a/src/engine/ui/cmd_god/do_tabulate.cpp +++ b/src/engine/ui/cmd_god/do_tabulate.cpp @@ -3,6 +3,7 @@ // #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "gameplay/magic/magic_utils.h" #include "engine/ui/modify.h" #include "engine/ui/objects_filter.h" @@ -96,9 +97,9 @@ int TabulateObjsByFilter(char *argument, CharData *ch) { for (const auto &i : obj_proto) { // ch не передаём: у прототипов нет наносимых меток (custom label). if (filter.check(i.get(), nullptr)) { - snprintf(line, sizeof(line), "%3d. [%7d] %-50s %s\r\n", + snprintf(line, sizeof(line), "%3d. [%7d] %s %s\r\n", ++found, i->get_vnum(), - utils::RemoveColors(i->get_short_description()).c_str(), + native_text::pad_right(utils::RemoveColors(i->get_short_description()), 50).c_str(), filter.show_obj_aff(i.get()).c_str()); out += line; } @@ -117,7 +118,8 @@ int TabulateMobsByName(char *searchname, CharData *ch) { for (nr = 0; nr <= top_of_mobt; nr++) { if (isname(searchname, mob_proto[nr].GetCharAliases())) { - sprintf(buf, "%3d. [%5d] %-30s (%s)\r\n", ++found, mob_index[nr].vnum, mob_proto[nr].get_npc_name().c_str(), + sprintf(buf, "%3d. [%5d] %s (%s)\r\n", ++found, mob_index[nr].vnum, + native_text::pad_right(mob_proto[nr].get_npc_name(), 30).c_str(), npc_race_types[mob_proto[nr].player_data.Race - ENpcRace::kBasic]); SendMsgToChar(buf, ch); } diff --git a/src/engine/ui/cmd_god/do_users.cpp b/src/engine/ui/cmd_god/do_users.cpp index a89f377f2..4d5b95fc2 100644 --- a/src/engine/ui/cmd_god/do_users.cpp +++ b/src/engine/ui/cmd_god/do_users.cpp @@ -3,6 +3,7 @@ // #include "engine/ui/color.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "gameplay/classes/pc_classes.h" #include "engine/entities/char_data.h" @@ -105,7 +106,9 @@ void do_users(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } // end while (parser) - const char *format = "%3d %-7s %-20s %-17s %-3s %-8s "; + // Ширина колонок - в символах, а не в байтах (issue #3681): поля ниже паддятся +// через native_text, поэтому формат содержит голые "%s". + const char *format = "%3d %s %s %s %s %s "; if (showemail) { strcpy(line, "Ном Професс Имя Состояние Idl Логин Сайт E-mail\r\n"); } else { @@ -263,23 +266,25 @@ void do_users(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { sprintf(line, format, d->desc_num, - classname, - d->original->GetCharAliases().c_str(), - state, - idletime, - timeptr); + native_text::pad_right(classname, 7).c_str(), + native_text::pad_right(d->original->GetCharAliases().c_str(), 20).c_str(), + native_text::pad_right(state, 17).c_str(), + native_text::pad_right(idletime, 3).c_str(), + native_text::pad_right(timeptr, 8).c_str()); } else { sprintf(line, format, d->desc_num, - classname, - d->character->GetCharAliases().c_str(), - state, - idletime, - timeptr); + native_text::pad_right(classname, 7).c_str(), + native_text::pad_right(d->character->GetCharAliases().c_str(), 20).c_str(), + native_text::pad_right(state, 17).c_str(), + native_text::pad_right(idletime, 3).c_str(), + native_text::pad_right(timeptr, 8).c_str()); } } else { - sprintf(line, format, d->desc_num, " - ", "UNDEFINED", state, idletime, timeptr); + sprintf(line, format, d->desc_num, native_text::pad_right(" - ", 7).c_str(), + native_text::pad_right("UNDEFINED", 20).c_str(), native_text::pad_right(state, 17).c_str(), + native_text::pad_right(idletime, 3).c_str(), native_text::pad_right(timeptr, 8).c_str()); } if (d && *d->host) { From 9be6a1953a6019fc54cca14f5b11c83407d0d7ce Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 04:09:16 +0200 Subject: [PATCH 012/110] docs: fix the declared C++ standard (C++17 -> C++20) CONTRIBUTING.md claimed C++17 while the build has used C++20 for a while (meson.build: cpp_std=c++20, and -std=c++20 in compile_commands.json). CLAUDE.md already said C++20; this brings the two in line. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0bba9fd8c..7029a85f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ При написании кода вы должны следовать правилу "одна команда - одна строка". Желательно это же правило распространять и на объявления переменных. -Былины используют стандарт C++17. +Былины используют стандарт C++20 (задан в meson.build: cpp_std=c++20). Тела конструкций if, if ... else for, while, do ... while всегда должны быть заключены в символы '{', '}'. From 702bd7d19e289fce9f45c06534f6b72dba734170 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 04:30:07 +0200 Subject: [PATCH 013/110] refactor(utf8): use fmt for column widths instead of the pad_right helper (#3681) Replaces all 63 native_text::pad_right/pad_left/truncate_to_chars call sites with fmt, which puts the width back where it belongs -- inside the format string -- instead of wrapping every argument: sprintf(buf, "%s - %s\r\n", native_text::pad_right(GET_NAME(i), 20).c_str(), room); SendMsgToChar(buf, ch); becomes SendMsgToChar(fmt::format("{:<20} - {}\r\n", GET_NAME(i), room), ch); This is possible because fmt measures width AND precision in code points for valid UTF-8 and falls back to one-unit-per-byte for KOI8-R (which is not valid UTF-8) -- measured, not assumed -- so it is already correct in both encodings and needs no change at the flip. printf cannot do this: its width/precision are byte-based by definition, which is what made the helper necessary in the first place. pad_right/pad_left/truncate_to_chars are removed from native_text along with their tests; the character primitives the scanners need (char_bytes, is_alnum_char, ...) stay. Several sites also lose their intermediate char buffer entirely. Drive-by, on a line this change already touches: do_show's mob listing had "{:<31}s" in its format string -- a leftover 's' from an earlier %-31s conversion that printed a stray letter after the name. Removed. Verified: clean build, full suite 648 passed / 0 failed. --- src/engine/scripting/dg_scripts.cpp | 4 ++-- src/engine/ui/cmd/do_alias.cpp | 5 ++-- src/engine/ui/cmd/do_commands.cpp | 8 +++---- src/engine/ui/cmd/do_exits.cpp | 8 +++---- src/engine/ui/cmd/do_features.cpp | 10 ++++---- src/engine/ui/cmd/do_score.cpp | 7 +++--- src/engine/ui/cmd/do_skills.cpp | 8 +++---- src/engine/ui/cmd/do_spells.cpp | 21 +++++++++-------- src/engine/ui/cmd/do_where.cpp | 9 ++----- src/engine/ui/cmd/do_who.cpp | 16 ++++++------- src/engine/ui/cmd_god/do_last.cpp | 15 ++++++------ src/engine/ui/cmd_god/do_liblist.cpp | 5 ++-- src/engine/ui/cmd_god/do_show.cpp | 31 ++++++++++++------------- src/engine/ui/cmd_god/do_tabulate.cpp | 13 +++++------ src/engine/ui/cmd_god/do_users.cpp | 31 ++++++++----------------- src/gameplay/communication/parcel.cpp | 9 ++++--- src/gameplay/crafting/im.cpp | 15 ++++++------ src/gameplay/crafting/item_creation.cpp | 11 ++++----- src/gameplay/fight/pk.cpp | 18 +++++++------- src/gameplay/mechanics/glory_const.cpp | 5 ++-- src/gameplay/mechanics/named_stuff.cpp | 16 ++++++------- src/utils/native_text.cpp | 31 ------------------------- src/utils/native_text.h | 13 ----------- tests/native_text.cpp | 16 ------------- 24 files changed, 121 insertions(+), 204 deletions(-) diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 1027381d9..69e261ae3 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -6283,8 +6283,8 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { char trgtypes[256]; for (; nr < top_of_trigt && (trig_index[nr]->vnum <= last); nr++) { std::string out = ""; - snprintf(buf, sizeof(buf), "%2d) [%5d] %s ", ++found, - trig_index[nr]->vnum, native_text::pad_right(trig_index[nr]->proto->get_name(), 50).c_str()); + strcpy(buf, fmt::format("{:2}) [{:5}] {:<50} ", ++found, + trig_index[nr]->vnum, trig_index[nr]->proto->get_name()).c_str()); out += buf; if (trig_index[nr]->proto->get_attach_type() == MOB_TRIGGER) { sprintbit(trig_index[nr]->proto->get_trigger_type(), trig_types, trgtypes, sizeof(trgtypes)); diff --git a/src/engine/ui/cmd/do_alias.cpp b/src/engine/ui/cmd/do_alias.cpp index f6050cd85..c7421ee0e 100644 --- a/src/engine/ui/cmd/do_alias.cpp +++ b/src/engine/ui/cmd/do_alias.cpp @@ -7,7 +7,7 @@ */ #include "engine/entities/char_data.h" -#include "utils/native_text.h" +#include #include "engine/ui/alias.h" void do_alias(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -25,8 +25,7 @@ void do_alias(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar(" Нет алиасов.\r\n", ch); else { while (a != nullptr) { - sprintf(buf, "%s %s\r\n", native_text::pad_right(a->alias, 15).c_str(), a->replacement); - SendMsgToChar(buf, ch); + SendMsgToChar(fmt::format("{:<15} {}\r\n", a->alias, a->replacement), ch); a = a->next; } } diff --git a/src/engine/ui/cmd/do_commands.cpp b/src/engine/ui/cmd/do_commands.cpp index b84bdb180..3520f446b 100644 --- a/src/engine/ui/cmd/do_commands.cpp +++ b/src/engine/ui/cmd/do_commands.cpp @@ -7,7 +7,7 @@ */ #include "engine/entities/char_data.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/communication/social.h" #include "engine/db/global_objects.h" @@ -38,7 +38,7 @@ void do_commands(CharData *ch, char *argument, int/* cmd*/, int subcmd) { continue; } for (const auto &kw : soc.GetKeywords()) { - sprintf(buf + strlen(buf), "%s", native_text::pad_right(kw, 19).c_str()); + strcat(buf, fmt::format("{:<19}", kw).c_str()); if (!(no % 4)) strcat(buf, "\r\n"); no++; @@ -50,13 +50,13 @@ void do_commands(CharData *ch, char *argument, int/* cmd*/, int subcmd) { i = cmd_sort_info[cmd_num].sort_pos; if (wizhelp) { if (privilege::HasPrivilege(vict, std::string(cmd_info[i].command), 0, 0, 0)) { - sprintf(buf + strlen(buf), "%s", native_text::pad_right(cmd_info[i].command, 15).c_str()); + strcat(buf, fmt::format("{:<15}", cmd_info[i].command).c_str()); if (!(no % 5)) strcat(buf, "\r\n"); no++; } } else if (cmd_info[i].minimum_level >= 0 && (static_cast(socials) == cmd_sort_info[i].is_social)) { - sprintf(buf + strlen(buf), "%s", native_text::pad_right(cmd_info[i].command, 15).c_str()); + strcat(buf, fmt::format("{:<15}", cmd_info[i].command).c_str()); if (!(no % 5)) strcat(buf, "\r\n"); no++; diff --git a/src/engine/ui/cmd/do_exits.cpp b/src/engine/ui/cmd/do_exits.cpp index 219b0706b..16ebec4a3 100644 --- a/src/engine/ui/cmd/do_exits.cpp +++ b/src/engine/ui/cmd/do_exits.cpp @@ -6,7 +6,7 @@ */ #include "engine/entities/char_data.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/mechanics/sight.h" #include "gameplay/mechanics/illumination.h" @@ -30,10 +30,10 @@ void DoExits(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { for (door = 0; door < EDirection::kMaxDirNum; door++) if (EXIT(ch, door) && EXIT(ch, door)->to_room() != kNowhere && !EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kClosed)) { if (privilege::IsGod(ch)) - sprintf(buf2, "%s - [%5d] %s\r\n", native_text::pad_right(dirs_rus[door], 6).c_str(), - GET_ROOM_VNUM(EXIT(ch, door)->to_room()), world[EXIT(ch, door)->to_room()]->name); + strcpy(buf2, fmt::format("{:<6} - [{:5}] {}\r\n", dirs_rus[door], + GET_ROOM_VNUM(EXIT(ch, door)->to_room()), world[EXIT(ch, door)->to_room()]->name).c_str()); else { - sprintf(buf2, "%s - ", native_text::pad_right(dirs_rus[door], 6).c_str()); + strcpy(buf2, fmt::format("{:<6} - ", dirs_rus[door]).c_str()); if (is_dark(EXIT(ch, door)->to_room()) && !sight::CanSeeInDark(ch)) strcat(buf2, "слишком темно\r\n"); else { diff --git a/src/engine/ui/cmd/do_features.cpp b/src/engine/ui/cmd/do_features.cpp index a48d26e9d..01f03bce1 100644 --- a/src/engine/ui/cmd/do_features.cpp +++ b/src/engine/ui/cmd/do_features.cpp @@ -1,5 +1,5 @@ #include "engine/ui/color.h" -#include "utils/native_text.h" +#include #include "gameplay/core/remort.h" #include "engine/entities/char_data.h" #include "gameplay/abilities/timed_abilities.h" @@ -82,17 +82,17 @@ void DisplayFeats(CharData *ch, CharData *vict, bool all_feats) { continue; } if (!ch->IsFlagged(EPrf::kBlindMode)) { - sprintf(buf, " %s%s %s%s\r\n", + strcpy(buf, fmt::format(" {}{} {:<30}{}\r\n", ch->HaveFeat(feat.GetId()) ? kColorGrn : CanGetFeat(ch, feat.GetId()) ? kColorNrm : kColorRed, ch->HaveFeat(feat.GetId()) ? "[И]" : CanGetFeat(ch, feat.GetId()) ? "[Д]" : "[Н]", - native_text::pad_right(MUD::Feat(feat.GetId()).GetCName(), 30).c_str(), kColorNrm); + MUD::Feat(feat.GetId()).GetCName(), kColorNrm).c_str()); } else { - sprintf(buf, " %s %s\r\n", + strcpy(buf, fmt::format(" {} {:<30}\r\n", ch->HaveFeat(feat.GetId()) ? "[И]" : CanGetFeat(ch, feat.GetId()) ? "[Д]" : "[Н]", - native_text::pad_right(MUD::Feat(feat.GetId()).GetCName(), 30).c_str()); + MUD::Feat(feat.GetId()).GetCName()).c_str()); } if (feat.IsInborn() || diff --git a/src/engine/ui/cmd/do_score.cpp b/src/engine/ui/cmd/do_score.cpp index 12e5aedc0..bd877500b 100644 --- a/src/engine/ui/cmd/do_score.cpp +++ b/src/engine/ui/cmd/do_score.cpp @@ -7,7 +7,6 @@ */ #include "engine/ui/color.h" -#include "utils/native_text.h" #include "gameplay/affects/affect_messages.h" #include "utils/utils_string.h" #include "gameplay/core/experience.h" @@ -233,12 +232,12 @@ void PrintScoreList(CharData *ch) { } else if (NAME_BAD(ch)) { SendMsgToChar(ch, "ВНИМАНИЕ! ваше имя запрещено богами. Очень скоро вы прекратите получать опыт.\r\n"); } - SendMsgToChar(ch, "Вы можете вступить в группу с максимальной разницей в %2d %s\r\n", + SendMsgToChar(fmt::format("Вы можете вступить в группу с максимальной разницей в {:2} {:<75.76}\r\n", grouping[ch->GetClass()][static_cast(remort::GetRealRemort(ch))], - native_text::pad_right(native_text::truncate_to_chars(std::string( + grammar::GetDeclensionInNumber(grouping[ch->GetClass()][static_cast(remort::GetRealRemort( ch))], grammar::EWhat::kLvl) - + std::string(" без потерь для опыта.")), 76), 75).c_str()); + + std::string(" без потерь для опыта.")), ch); SendMsgToChar(ch, "Вы можете принять в группу максимум %d соратников.\r\n", group::max_group_size(ch)); std::ostringstream out; diff --git a/src/engine/ui/cmd/do_skills.cpp b/src/engine/ui/cmd/do_skills.cpp index 4197a913d..fbdccf49e 100644 --- a/src/engine/ui/cmd/do_skills.cpp +++ b/src/engine/ui/cmd/do_skills.cpp @@ -1,5 +1,5 @@ #include "do_skills.h" -#include "utils/native_text.h" +#include #include "engine/ui/color.h" #include "engine/entities/char_data.h" @@ -79,12 +79,12 @@ void DisplaySkills(CharData *ch, CharData *vict, const char *filter/* = nullptr* } // Ширина колонки - в символах, а не в байтах (issue #3681). - sprintf(buf + strlen(buf), "%s %s (%d)%s \r\n", - native_text::pad_right(skill.GetName(), 23).c_str(), + strcat(buf, fmt::format("{:<23} {} ({}){} \r\n", + skill.GetName(), how_good(GetSkill(ch, skill_id), CalcSkillHardCap(ch, skill_id)), GetTrainedSkill(ch, skill_id) == 0 ? GetEquippedSkill(ch, skill_id) : std::min(CalcSkillMinCap(ch, skill_id) + GetEquippedSkill(ch, skill_id), MUD::Skill(skill_id).cap), - kColorNrm); + kColorNrm).c_str()); skills_names.emplace_back(buf); i++; } diff --git a/src/engine/ui/cmd/do_spells.cpp b/src/engine/ui/cmd/do_spells.cpp index f8eb53df5..1ebb0dd5f 100644 --- a/src/engine/ui/cmd/do_spells.cpp +++ b/src/engine/ui/cmd/do_spells.cpp @@ -1,5 +1,5 @@ #include "do_spells.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/mechanics/magic_item.h" @@ -88,18 +88,19 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { if (CalcSpellManacost(ch, spell_id) > Mana(GetRealWis(ch))) continue; if (CheckRecipeItems(ch, spell_id, ESpellType::kRunes, false)) { - slots[slot_num] += sprintf(names[slot_num] + slots[slot_num], - "%s|<...%4d.> %s%s&n|", - slots[slot_num] % 114 < - 10 ? "\r\n" : " ", + const auto line = fmt::format("{}|<...{:4}.> {}{:<38}&n|", + slots[slot_num] % 114 < 10 ? "\r\n" : " ", CalcSpellManacost(ch, spell_id), GetSpellColor(spell_id), - native_text::pad_right(MUD::Spell(spell_id).GetCName(), 38).c_str()); + MUD::Spell(spell_id).GetCName()); + strcpy(names[slot_num] + slots[slot_num], line.c_str()); + slots[slot_num] += static_cast(line.size()); } else { if (all) { - slots[slot_num] += sprintf(names[slot_num] + slots[slot_num], - "%s|+--------+ %s%s&n|", slots[slot_num] % 114 < 10 ? "\r\n" - : " ", GetSpellColor(spell_id), - native_text::pad_right(MUD::Spell(spell_id).GetCName(), 38).c_str()); + const auto line = fmt::format("{}|+--------+ {}{:<38}&n|", + slots[slot_num] % 114 < 10 ? "\r\n" : " ", GetSpellColor(spell_id), + MUD::Spell(spell_id).GetCName()); + strcpy(names[slot_num] + slots[slot_num], line.c_str()); + slots[slot_num] += static_cast(line.size()); } } } else { diff --git a/src/engine/ui/cmd/do_where.cpp b/src/engine/ui/cmd/do_where.cpp index c408fcb78..2d95e0d0d 100644 --- a/src/engine/ui/cmd/do_where.cpp +++ b/src/engine/ui/cmd/do_where.cpp @@ -3,7 +3,6 @@ // #include "engine/entities/char_data.h" -#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/world_objects.h" #include "gameplay/economics/exchange.h" @@ -147,9 +146,7 @@ void PerformMortalWhere(CharData *ch, char *arg) { } // Ширина колонки - в символах, а не в байтах (issue #3681). - sprintf(buf, "%s - %s\r\n", - native_text::pad_right(GET_NAME(i), 20).c_str(), world[i->in_room]->name); - SendMsgToChar(buf, ch); + SendMsgToChar(fmt::format("{:<20} - {}\r\n", GET_NAME(i), world[i->in_room]->name), ch); } } else // print only FIRST char, not all. { @@ -168,9 +165,7 @@ void PerformMortalWhere(CharData *ch, char *arg) { continue; } - sprintf(buf, "%s - %s\r\n", - native_text::pad_right(GET_NAME(i), 25).c_str(), world[i->in_room]->name); - SendMsgToChar(buf, ch); + SendMsgToChar(fmt::format("{:<25} - {}\r\n", GET_NAME(i), world[i->in_room]->name), ch); return; } SendMsgToChar("Никого похожего с этим именем нет.\r\n", ch); diff --git a/src/engine/ui/cmd/do_who.cpp b/src/engine/ui/cmd/do_who.cpp index d40824637..482f91e8c 100644 --- a/src/engine/ui/cmd/do_who.cpp +++ b/src/engine/ui/cmd/do_who.cpp @@ -3,7 +3,7 @@ // #include "engine/ui/cmd/do_who.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "utils/grammar/gender.h" #include "gameplay/mechanics/sight.h" @@ -165,19 +165,19 @@ void DoWho(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (short_list) { char tmp[kMaxInputLength]; snprintf(tmp, sizeof(tmp), "%s%s%s", GetPkNameColor(tch), GET_NAME(tch), kColorNrm); - // Ширина колонки - в символах, а не в байтах (issue #3681): "%-30s" отсчитывает + // Ширина колонки - в символах, а не в байтах (issue #3681): fmt "{:<30}" считает // байты, из-за чего колонка с русским именем под UTF-8 выходит вдвое уже. if (privilege::IsImpl(ch) || ch->IsFlagged(EPrf::kCoderinfo)) { - sprintf(buf, "%s[%2d %s] %s%s", + strcpy(buf, fmt::format("{}[{:2} {}] {:<30}{}", privilege::IsGod(tch.get()) ? kColorWht : "", GetRealLevel(tch), MUD::Class(tch->GetClass()).GetCName(), - native_text::pad_right(tmp, 30).c_str(), - privilege::IsGod(tch.get()) ? kColorNrm : ""); + tmp, + privilege::IsGod(tch.get()) ? kColorNrm : "").c_str()); } else { - sprintf(buf, "%s%s%s", + strcpy(buf, fmt::format("{}{:<30}{}", privilege::IsImmortal(tch.get()) ? kColorWht : "", - native_text::pad_right(tmp, 30).c_str(), - privilege::IsImmortal(tch.get()) ? kColorNrm : ""); + tmp, + privilege::IsImmortal(tch.get()) ? kColorNrm : "").c_str()); } } else { if (privilege::IsImpl(ch) diff --git a/src/engine/ui/cmd_god/do_last.cpp b/src/engine/ui/cmd_god/do_last.cpp index bf72a352a..5c040dcdb 100644 --- a/src/engine/ui/cmd_god/do_last.cpp +++ b/src/engine/ui/cmd_god/do_last.cpp @@ -7,7 +7,7 @@ */ #include "engine/entities/char_data.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "engine/entities/char_player.h" #include "engine/db/global_objects.h" @@ -29,14 +29,13 @@ void DoPageLastLogins(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) SendMsgToChar("Вы не столь уж и божественны для этого.\r\n", ch); } else { time_t tmp_time = chdata->get_last_logon(); - sprintf(buf, "[%5ld] [%2d %s] %s : %s : %s\r\n", + SendMsgToChar(fmt::format("[{:5}] [{:2} {}] {:<12} : {:<18} : {:<20}\r\n", chdata->get_uid(), GetRealLevel(chdata), - MUD::Class(chdata->GetClass()).GetAbbr().c_str(), - native_text::pad_right(GET_NAME(chdata), 12).c_str(), - native_text::pad_right(chdata->player_specials->saved.LastIP[0] - ? chdata->player_specials->saved.LastIP : "Unknown", 18).c_str(), - native_text::pad_right(ctime(&tmp_time), 20).c_str()); - SendMsgToChar(buf, ch); + MUD::Class(chdata->GetClass()).GetAbbr(), + GET_NAME(chdata), + chdata->player_specials->saved.LastIP[0] + ? chdata->player_specials->saved.LastIP : "Unknown", + ctime(&tmp_time)), ch); } } diff --git a/src/engine/ui/cmd_god/do_liblist.cpp b/src/engine/ui/cmd_god/do_liblist.cpp index 8ff33f548..f7a5da3b3 100644 --- a/src/engine/ui/cmd_god/do_liblist.cpp +++ b/src/engine/ui/cmd_god/do_liblist.cpp @@ -7,7 +7,6 @@ */ #include "do_liblist.h" -#include "utils/native_text.h" #include "engine/entities/char_data.h" #include "engine/db/obj_prototypes.h" @@ -264,8 +263,8 @@ void Print(CharData *ch, int first, int last, const std::string &options) { int cnt = 0; for (int i = 0; i <= top_of_mobt; ++i) { if (mob_index[i].vnum >= first && mob_index[i].vnum <= last) { - fmt::format_to(std::back_inserter(out), "{:5}. {:<45} [{:<6}] [{:<2}]{}", - ++cnt, native_text::truncate_to_chars(mob_proto[i].get_name_str(), 45), + fmt::format_to(std::back_inserter(out), "{:5}. {:<45.45} [{:<6}] [{:<2}]{}", + ++cnt, mob_proto[i].get_name_str(), mob_index[i].vnum, mob_proto[i].GetLevel(), PrintFlag(mob_proto + i, options)); if (!mob_proto[i].proto_script->empty()) { diff --git a/src/engine/ui/cmd_god/do_show.cpp b/src/engine/ui/cmd_god/do_show.cpp index 9831f1a5e..fd2c0229f 100644 --- a/src/engine/ui/cmd_god/do_show.cpp +++ b/src/engine/ui/cmd_god/do_show.cpp @@ -3,7 +3,6 @@ // #include "administration/accounts.h" -#include "utils/native_text.h" #include "administration/ban.h" #include "administration/privilege.h" #include "engine/ui/cmd/do_features.h" @@ -253,12 +252,12 @@ void print_mob_bosses(CharData *ch, bool lvl_sort) { const auto mob = mob_proto + mob_rnum; const auto vnum = GET_MOB_VNUM(mob); - out += fmt::format("{:<3} {:<31}s [{:<2}][{:<6}] {:<31}s\r\n", + out += fmt::format("{:<3} {:<31.31} [{:<2}][{:<6}] {:<31.31}\r\n", ++cnt, - native_text::truncate_to_chars(mob->get_name_str(), 31), + mob->get_name_str(), zone_table[mob_index[mob_rnum].zone].mob_level, vnum, - native_text::truncate_to_chars(zone_name_str, 31)); + zone_name_str); } page_string(ch->desc, out); } @@ -461,9 +460,9 @@ void ListSpellCreate(CharData *ch) { if (r > 0) runes_str += '|'; runes_str += std::to_string(info.runes[r]); } - SendMsgToChar(ch, "%3d) Rune spell [%3d] &W%s&n runes: %s level %d\r\n", - ++i, to_underlying(spell_id), native_text::pad_right(MUD::Spell(spell_id).GetCName(), 30).c_str(), - runes_str.c_str(), info.min_caster_level); + SendMsgToChar(fmt::format("{:3}) Rune spell [{:3}] &W{:<30}&n runes: {} level {}\r\n", + ++i, to_underlying(spell_id), MUD::Spell(spell_id).GetCName(), + runes_str, info.min_caster_level), ch); } } @@ -718,19 +717,19 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { && d->character->in_room != kNowhere && ((sight::CanSee(ch, d->character) && GetRealLevel(ch) >= GetRealLevel(d->character)) || ch->IsFlagged(EPrf::kCoderinfo))) { - sprintf(buf + strlen(buf), - "%s - подслушивается %s (map %s).\r\n", - native_text::pad_right(GET_NAME(d->snooping->character), 10).c_str(), + strcat(buf, fmt::format( + "{:<10} - подслушивается {} (map {}).\r\n", + GET_NAME(d->snooping->character), GET_PAD(d->character, 4), - d->snoop_with_map ? "on" : "off"); + d->snoop_with_map ? "on" : "off").c_str()); } } SendMsgToChar(*buf ? buf : "Никто не подслушивается.\r\n", ch); break; // snoop case 9: // show linkdrop SendMsgToChar(" Список игроков в состоянии 'link drop'\r\n", ch); - sprintf(buf, "%s%s %s\r\n", native_text::pad_right(" Имя", 50).c_str(), - native_text::pad_right("Комната", 16).c_str(), "Бездействие (тики)"); + strcpy(buf, fmt::format("{:<50}{:<16} {}\r\n", " Имя", + "Комната", "Бездействие (тики)").c_str()); SendMsgToChar(buf, ch); i = 0; for (const auto &character : character_list) { @@ -739,9 +738,9 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { continue; } ++i; - sprintf(buf, "%s[%6d][%6d] %d\r\n", - native_text::pad_right(character->GetNameWithTitleOrRace(), 50).c_str(), GET_ROOM_VNUM(character->in_room), - GET_ROOM_VNUM(character->get_was_in_room()), character->char_specials.timer); + strcpy(buf, fmt::format("{:<50}[{:6}][{:6}] {}\r\n", + character->GetNameWithTitleOrRace(), GET_ROOM_VNUM(character->in_room), + GET_ROOM_VNUM(character->get_was_in_room()), character->char_specials.timer).c_str()); SendMsgToChar(buf, ch); } sprintf(buf, "Всего - %d\r\n", i); diff --git a/src/engine/ui/cmd_god/do_tabulate.cpp b/src/engine/ui/cmd_god/do_tabulate.cpp index f333fd324..89bea52fc 100644 --- a/src/engine/ui/cmd_god/do_tabulate.cpp +++ b/src/engine/ui/cmd_god/do_tabulate.cpp @@ -3,7 +3,6 @@ // #include "engine/entities/char_data.h" -#include "utils/native_text.h" #include "gameplay/magic/magic_utils.h" #include "engine/ui/modify.h" #include "engine/ui/objects_filter.h" @@ -97,10 +96,10 @@ int TabulateObjsByFilter(char *argument, CharData *ch) { for (const auto &i : obj_proto) { // ch не передаём: у прототипов нет наносимых меток (custom label). if (filter.check(i.get(), nullptr)) { - snprintf(line, sizeof(line), "%3d. [%7d] %s %s\r\n", + strcpy(line, fmt::format("{:3}. [{:7}] {:<50} {}\r\n", ++found, i->get_vnum(), - native_text::pad_right(utils::RemoveColors(i->get_short_description()), 50).c_str(), - filter.show_obj_aff(i.get()).c_str()); + utils::RemoveColors(i->get_short_description()), + filter.show_obj_aff(i.get())).c_str()); out += line; } } @@ -118,9 +117,9 @@ int TabulateMobsByName(char *searchname, CharData *ch) { for (nr = 0; nr <= top_of_mobt; nr++) { if (isname(searchname, mob_proto[nr].GetCharAliases())) { - sprintf(buf, "%3d. [%5d] %s (%s)\r\n", ++found, mob_index[nr].vnum, - native_text::pad_right(mob_proto[nr].get_npc_name(), 30).c_str(), - npc_race_types[mob_proto[nr].player_data.Race - ENpcRace::kBasic]); + strcpy(buf, fmt::format("{:3}. [{:5}] {:<30} ({})\r\n", ++found, mob_index[nr].vnum, + mob_proto[nr].get_npc_name(), + npc_race_types[mob_proto[nr].player_data.Race - ENpcRace::kBasic]).c_str()); SendMsgToChar(buf, ch); } } diff --git a/src/engine/ui/cmd_god/do_users.cpp b/src/engine/ui/cmd_god/do_users.cpp index 4d5b95fc2..7398ea12d 100644 --- a/src/engine/ui/cmd_god/do_users.cpp +++ b/src/engine/ui/cmd_god/do_users.cpp @@ -3,7 +3,7 @@ // #include "engine/ui/color.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/classes/pc_classes.h" #include "engine/entities/char_data.h" @@ -108,7 +108,7 @@ void do_users(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { // Ширина колонок - в символах, а не в байтах (issue #3681): поля ниже паддятся // через native_text, поэтому формат содержит голые "%s". - const char *format = "%3d %s %s %s %s %s "; + const char *format = "{:3} {:<7} {:<20} {:<17} {:<3} {:<8} "; if (showemail) { strcpy(line, "Ном Професс Имя Состояние Idl Логин Сайт E-mail\r\n"); } else { @@ -263,28 +263,17 @@ void do_users(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (d->character && d->character->GetCharAliases().c_str()) { if (d->original) { - sprintf(line, - format, - d->desc_num, - native_text::pad_right(classname, 7).c_str(), - native_text::pad_right(d->original->GetCharAliases().c_str(), 20).c_str(), - native_text::pad_right(state, 17).c_str(), - native_text::pad_right(idletime, 3).c_str(), - native_text::pad_right(timeptr, 8).c_str()); + strcpy(line, fmt::format(fmt::runtime(format), + d->desc_num, classname, d->original->GetCharAliases().c_str(), + state, idletime, timeptr).c_str()); } else { - sprintf(line, - format, - d->desc_num, - native_text::pad_right(classname, 7).c_str(), - native_text::pad_right(d->character->GetCharAliases().c_str(), 20).c_str(), - native_text::pad_right(state, 17).c_str(), - native_text::pad_right(idletime, 3).c_str(), - native_text::pad_right(timeptr, 8).c_str()); + strcpy(line, fmt::format(fmt::runtime(format), + d->desc_num, classname, d->character->GetCharAliases().c_str(), + state, idletime, timeptr).c_str()); } } else { - sprintf(line, format, d->desc_num, native_text::pad_right(" - ", 7).c_str(), - native_text::pad_right("UNDEFINED", 20).c_str(), native_text::pad_right(state, 17).c_str(), - native_text::pad_right(idletime, 3).c_str(), native_text::pad_right(timeptr, 8).c_str()); + strcpy(line, fmt::format(fmt::runtime(format), d->desc_num, " - ", + "UNDEFINED", state, idletime, timeptr).c_str()); } if (d && *d->host) { diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index b117805da..29f379689 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -3,7 +3,6 @@ // Part of Bylins http://www.mud.ru #include "parcel.h" -#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "gameplay/economics/currencies.h" @@ -829,13 +828,13 @@ bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { std::string sender = GetNameByUnique(it2->first); found = true; - SendMsgToChar(ch, "%2d. [%6d] %s - наход%sся на почте (отправитель: %s, получатель: %s).\r\n", + SendMsgToChar(fmt::format("{:2}. [{:6}] {:<25} - наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", num++, GET_OBJ_VNUM(it3->obj_.get()), - native_text::pad_right(it3->obj_->get_short_description(), 25).c_str(), + it3->obj_->get_short_description(), grammar::ObjPluralVerbEnding((it3->obj_)->get_sex()), - sender.c_str(), - target.c_str()); + sender, + target), ch); } } } diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index ead6fddec..e35dc3e95 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -11,6 +11,7 @@ // Реализация ингредиентной магии #include "im.h" +#include #include "utils/parser_wrapper.h" #include "utils/utils_parse.h" #include "utils/native_text.h" @@ -953,14 +954,14 @@ void list_recipes(CharData *ch, bool all_recipes) { rs = im_get_char_rskill(ch, sortpos); const bool unavailable = req->level > GetRealLevel(ch) || req->remort > remort::GetRealRemort(ch); if (!ch->IsFlagged(EPrf::kBlindMode)) { - sprintf(buf, " %s%s%s %2d (%2d)%s\r\n", + strcpy(buf, fmt::format(" {}{:<30}{} {:2} ({:2}){}\r\n", unavailable ? kColorRed : rs ? kColorGrn : kColorNrm, - native_text::pad_right(imrecipes[sortpos].name, 30).c_str(), kColorCyn, - req->level, req->remort, kColorNrm); + imrecipes[sortpos].name, kColorCyn, + req->level, req->remort, kColorNrm).c_str()); } else { - sprintf(buf, " %s %s %2d (%2d)\r\n", - unavailable ? "[Н]" : rs ? "[И]" : "[Д]", native_text::pad_right(imrecipes[sortpos].name, 30).c_str(), - req->level, req->remort); + strcpy(buf, fmt::format(" {} {:<30} {:2} ({:2})\r\n", + unavailable ? "[Н]" : rs ? "[И]" : "[Д]", imrecipes[sortpos].name, + req->level, req->remort).c_str()); } strcat(buf1, buf); ++i; @@ -982,7 +983,7 @@ void list_recipes(CharData *ch, bool all_recipes) { } if (rs->perc <= 0) continue; - sprintf(buf, "%s %s%s\r\n", native_text::pad_right(imrecipes[rs->rid].name, 30).c_str(), how_good(rs->perc, kMaxRecipeLevel), kColorBoldBlk); + strcpy(buf, fmt::format("{:<30} {}{}\r\n", imrecipes[rs->rid].name, how_good(rs->perc, kMaxRecipeLevel), kColorBoldBlk).c_str()); strcat(buf2, buf); ++i; } diff --git a/src/gameplay/crafting/item_creation.cpp b/src/gameplay/crafting/item_creation.cpp index 7ada01f71..f6ae07813 100644 --- a/src/gameplay/crafting/item_creation.cpp +++ b/src/gameplay/crafting/item_creation.cpp @@ -7,7 +7,7 @@ * $Revision$ * ************************************************************************ */ #include "item_creation.h" -#include "utils/native_text.h" +#include #include "utils/utils_parse.h" #include "utils/parser_wrapper.h" #include "administration/privilege.h" @@ -334,10 +334,9 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ } j++; } - sprintf(tmpbuf, "%3zd %-1s %s %s(%5d) :", - i + 1, (trec->locked ? "*" : " "), native_text::pad_right(skill_name, 6).c_str(), - native_text::pad_right(obj_name, 40).c_str(), trec->obj_proto); - tmpstr += string(tmpbuf); + tmpstr += fmt::format("{:3} {:<1} {:<6} {:<40}({:5}) :", + i + 1, (trec->locked ? "*" : " "), skill_name, + obj_name, trec->obj_proto); for (int j = 0; j < MAX_PARTS; j++) { if (trec->parts[j].proto != 0) { obj = GetObjectPrototype(trec->parts[j].proto); @@ -346,7 +345,7 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ } else { obj_name = "Нет"; } - sprintf(tmpbuf, " %s(%5d)", native_text::pad_right(obj_name, 35).c_str(), trec->parts[j].proto); + strcpy(tmpbuf, fmt::format(" {:<35}({:5})", obj_name, trec->parts[j].proto).c_str()); if (j > 0) { if (j % 2 == 0) { // разбиваем строчки если ингров больше 2; diff --git a/src/gameplay/fight/pk.cpp b/src/gameplay/fight/pk.cpp index 66ea9fc89..800a424b5 100644 --- a/src/gameplay/fight/pk.cpp +++ b/src/gameplay/fight/pk.cpp @@ -12,7 +12,7 @@ ************************************************************************ */ #include "pk.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" #include "gameplay/mechanics/mount.h" @@ -664,9 +664,9 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (tch->get_uid() == uid) { found = true; if (pk.battle_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", native_text::pad_right(temp, 40).c_str()); + strcat(buf, fmt::format(" {:<40} <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", temp).c_str()); } else { - sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", native_text::pad_right(temp, 40).c_str(), pk.kill_num, pk.revenge_num); + strcat(buf, fmt::format(" {:<40} {:3} {:3}\r\n", temp, pk.kill_num, pk.revenge_num).c_str()); } break; } @@ -674,9 +674,9 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else { found = true; if (pk.battle_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %s <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", native_text::pad_right(temp, 40).c_str()); + strcat(buf, fmt::format(" {:<40} <БОЕВЫЕ ДЕЙСТВИЯ>\r\n", temp).c_str()); } else { - sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", native_text::pad_right(temp, 40).c_str(), pk.kill_num, pk.revenge_num); + strcat(buf, fmt::format(" {:<40} {:3} {:3}\r\n", temp, pk.kill_num, pk.revenge_num).c_str()); } } } @@ -709,12 +709,12 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { // Сначала проверка клан флага if (CLAN(ch) && pk.clan_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %s <ВОЙНА>\r\n", native_text::pad_right(GET_NAME(tch), 40).c_str()); + strcat(buf, fmt::format(" {:<40} <ВОЙНА>\r\n", GET_NAME(tch)).c_str()); } else if (pk.clan_exp > time(nullptr)) { - sprintf(buf + strlen(buf), " %s <ВРЕМЕННЫЙ ФЛАГ>\r\n", native_text::pad_right(GET_NAME(tch), 40).c_str()); + strcat(buf, fmt::format(" {:<40} <ВРЕМЕННЫЙ ФЛАГ>\r\n", GET_NAME(tch)).c_str()); } else if (pk.kill_num + pk.revenge_num > 0) { - sprintf(buf + strlen(buf), " %s %3ld %3ld\r\n", - native_text::pad_right(GET_NAME(tch), 40).c_str(), pk.kill_num, pk.revenge_num); + strcat(buf, fmt::format(" {:<40} {:3} {:3}\r\n", + GET_NAME(tch), pk.kill_num, pk.revenge_num).c_str()); } else { continue; } diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index 121cefa25..739d57020 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -4,7 +4,6 @@ // Part of Bylins http://www.mud.ru #include "glory_const.h" -#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "utils/grammar/declensions.h" @@ -227,8 +226,8 @@ void print_glory(CharData *ch, GloryListType::iterator &it) { *buf = '\0'; for (auto i = it->second->stats.begin(), iend = it->second->stats.end(); i != iend; ++i) { if ((i->first >= 0) && (i->first < (int) sizeof(olc_stat_name))) { - sprintf(buf + strlen(buf), "%s: +%d", native_text::pad_right(olc_stat_name[i->first], 16).c_str(), - i->second * stat_multi(i->first)); + strcat(buf, fmt::format("{:<16}: +{}", olc_stat_name[i->first], + i->second * stat_multi(i->first)).c_str()); if (stat_multi(i->first) > 1) sprintf(buf + strlen(buf), "(%d)", i->second); strcat(buf, "\r\n"); diff --git a/src/gameplay/mechanics/named_stuff.cpp b/src/gameplay/mechanics/named_stuff.cpp index be9b96175..cdbb3dd59 100644 --- a/src/gameplay/mechanics/named_stuff.cpp +++ b/src/gameplay/mechanics/named_stuff.cpp @@ -3,7 +3,7 @@ // Part of Bylins http://www.mud.ru #include "named_stuff.h" -#include "utils/native_text.h" +#include #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" @@ -370,12 +370,12 @@ void do_named(CharData *ch, char *argument, int cmd, int subcmd) { out += buf1; } found++; - sprintf(buf2, "%6ld) &R*&n%s Владелец:%s e-mail:&S%s&s\r\n", + strcpy(buf2, fmt::format("{:6}) &R*&n{:<31} Владелец:{:<16} e-mail:&S{}&s\r\n", it->first + 1, - native_text::pad_right("Несуществующий предмет", 31).c_str(), - native_text::pad_right(GetNameByUnique(it->second->uid, false), 16).c_str(), - str_dup(it->second->mail.c_str()) - ); + "Несуществующий предмет", + GetNameByUnique(it->second->uid, false), + it->second->mail + ).c_str()); out += buf2; } } else { @@ -389,9 +389,9 @@ void do_named(CharData *ch, char *argument, int cmd, int subcmd) { obj_proto[r_num]->get_vnum(), colored_name(obj_proto[r_num]->get_short_description().c_str(), -32)); if (privilege::IsGrGod(ch) || ch->IsFlagged(EPrf::kCoderinfo)) { - snprintf(buf2, kMaxStringLength, "%s Игра:%d Пост:%d Владелец:%s e-mail:&S%s&s\r\n", buf1, + strcpy(buf2, fmt::format("{} Игра:{} Пост:{} Владелец:{:<16} e-mail:&S{}&s\r\n", buf1, obj_proto.total_online(r_num), obj_proto.stored(r_num), - native_text::pad_right(GetNameByUnique(it->second->uid, false), 16).c_str(), it->second->mail.c_str()); + GetNameByUnique(it->second->uid, false), it->second->mail).c_str()); } else { snprintf(buf2, kMaxStringLength, "%s\r\n", buf1); } diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 679e84ecb..8c72edd93 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -329,37 +329,6 @@ std::size_t last_char_offset(std::string_view s) { return last; } -namespace { - -std::size_t missing_width(std::string_view s, std::size_t width) { - const std::size_t have = char_count(s); - return have >= width ? 0 : width - have; -} - -} // namespace - -std::string truncate_to_chars(std::string_view s, std::size_t count) { - std::size_t pos = 0; - std::size_t seen = 0; - while (pos < s.size() && seen < count) { - pos += char_bytes_at(s, pos); - ++seen; - } - return std::string(s.substr(0, pos)); -} - -std::string pad_right(std::string_view s, std::size_t width, char fill) { - std::string out(s); - out.append(missing_width(s, width), fill); - return out; -} - -std::string pad_left(std::string_view s, std::size_t width, char fill) { - std::string out(missing_width(s, width), fill); - out.append(s); - return out; -} - bool list_contains_char(std::string_view list, std::string_view ch) { if (ch.empty()) { return false; diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 26d7710bd..d9ba7a3ce 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -86,19 +86,6 @@ std::size_t copy_lower_char(const char *src, char *dst); // is that character. KOI8-R: s.size() - 1. std::size_t last_char_offset(std::string_view s); -// Pad `s` to at least `width` display characters, appending (pad_right) or prepending (pad_left) -// `fill`. The replacement for a printf "%-Ns" / "%Ns" field, which counts bytes: under KOI8-R the -// result is identical, under UTF-8 a Russian column no longer comes out half as wide. -// Note: like printf, colour codes embedded in `s` still count toward the width -- that is a -// separate, pre-existing skew and is deliberately not changed here. -std::string pad_right(std::string_view s, std::size_t width, char fill = ' '); -std::string pad_left(std::string_view s, std::size_t width, char fill = ' '); - -// First `count` characters of `s` (all of it when shorter). The replacement for a substr(0, N) -// used to cap a display length: under KOI8-R it is exactly that, under UTF-8 it counts characters -// and so never cuts one in half. -std::string truncate_to_chars(std::string_view s, std::size_t count); - // Does the single character `ch` occur in `list`? The replacement for strchr() over a literal // list of letters: `list` is walked one whole character at a time, so a multibyte character can // never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. diff --git a/tests/native_text.cpp b/tests/native_text.cpp index b20f2de83..6ef53a563 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -253,22 +253,6 @@ TEST(NativeText, LastCharOffset) { } } -TEST(NativeText, PadRightAndLeft) { - EXPECT_EQ(native_text::pad_right("ab", 5), "ab "); - EXPECT_EQ(native_text::pad_left("ab", 5), " ab"); - EXPECT_EQ(native_text::pad_right("ab", 2), "ab"); // already wide enough - EXPECT_EQ(native_text::pad_right("abcdef", 3), "abcdef"); // never truncates - EXPECT_EQ(native_text::pad_right("", 3), " "); - EXPECT_EQ(native_text::pad_right("ab", 4, '.'), "ab.."); - if (native_text::native_is_utf8()) { - // 6 Cyrillic characters (12 bytes) padded to a 10-character column: no padding, and - // crucially not 12 bytes' worth of "already too wide" either. - EXPECT_EQ(native_text::pad_right(kNtPrivet, 10), kNtPrivet); - // 6 characters padded to 8 -> exactly two spaces, not eight. - EXPECT_EQ(native_text::pad_right(kNtPrivet, 8), std::string(kNtPrivet) + " "); - } -} - TEST(NativeText, ListContainsChar) { EXPECT_TRUE(native_text::list_contains_char("abc", "b")); EXPECT_FALSE(native_text::list_contains_char("abc", "d")); From 4b57f5e82522a1ba53d140e55ddb7b93a7e5939c Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 04:50:21 +0200 Subject: [PATCH 014/110] feat(utf8): character iteration API + the case sites the first sweep missed (#3681) A full audit sweep showed track A was closed prematurely: beyond the hotspots already converted, 55 more per-byte case sites were left, and they are the same class of bug. * 36 "capitalise the first letter" sites (name[0] = UPPER(name[0]) and friends across 19 files) now go through native_text::capitalize_first, which folds a whole character instead of mangling a multibyte lead byte. * The shared string helpers are fixed at the source rather than per call site: ConvertToLow (both overloads), SubstToLow, SubstStrToLow, SubstStrToUpper, colorLOW, colorCAP and IsAbbr. Fixing ConvertToLow alone covers its many callers. Adds the API the call sites were missing, so they stop doing pointer arithmetic: * native_text::chars(s) -- range-for over characters, each element a string_view covering exactly one character: for (auto letter : native_text::chars(argument)) { ... } * native_text::to_lower/to_upper for std::string and char* -- whole-string case conversion, which collapses several hand-rolled loops to one line. * copy_upper_char, the counterpart of copy_lower_char. Note on naming: character count stays an explicit call (char_count) rather than a "length()", because both lengths are genuinely needed -- characters for display width, bytes for buffer sizing -- and conflating them is how this whole bug class started. Left as-is deliberately: SubstStrToLow raises case despite its name. That mismatch predates this work; only its byte semantics changed. Verified: clean build, full suite 648 passed / 0 failed. --- src/administration/accounts.cpp | 5 +- src/engine/boot/boot_data_files.cpp | 3 +- src/engine/db/obj_save.cpp | 3 +- src/engine/db/sqlite_world_data_source.cpp | 3 +- src/engine/db/yaml_world_data_source.cpp | 3 +- src/engine/ui/cmd/do_gen_comm.cpp | 16 +++--- src/engine/ui/cmd/do_ignore.cpp | 9 ++-- src/engine/ui/cmd/do_who_am_i.cpp | 3 +- src/engine/ui/cmd_god/do_set_all.cpp | 3 +- src/engine/ui/cmd_god/do_show.cpp | 5 +- src/engine/ui/cmd_god/do_stat.cpp | 3 +- src/gameplay/clans/house.cpp | 13 ++--- src/gameplay/communication/parcel.cpp | 5 +- src/gameplay/core/game_limits.cpp | 5 +- src/gameplay/economics/exchange.cpp | 9 ++-- src/gameplay/fight/pk.cpp | 3 +- src/gameplay/mechanics/glory_const.cpp | 3 +- src/gameplay/mechanics/sight.cpp | 3 +- src/utils/native_text.cpp | 62 ++++++++++++++++++++-- src/utils/native_text.h | 49 +++++++++++++++++ src/utils/utils_string.cpp | 42 +++++++-------- tests/native_text.cpp | 47 ++++++++++++++++ 22 files changed, 229 insertions(+), 68 deletions(-) diff --git a/src/administration/accounts.cpp b/src/administration/accounts.cpp index 5fcf86a8e..87b452337 100644 --- a/src/administration/accounts.cpp +++ b/src/administration/accounts.cpp @@ -3,6 +3,7 @@ * 2018 (c) bodrich */ #include "accounts.h" +#include "utils/native_text.h" #include "password.h" #include "engine/entities/zone.h" #include @@ -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++; } @@ -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++; diff --git a/src/engine/boot/boot_data_files.cpp b/src/engine/boot/boot_data_files.cpp index 5613d7b39..15f4ee6d5 100644 --- a/src/engine/boot/boot_data_files.cpp +++ b/src/engine/boot/boot_data_files.cpp @@ -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" @@ -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; diff --git a/src/engine/db/obj_save.cpp b/src/engine/db/obj_save.cpp index 26298fca4..42bf4b576 100644 --- a/src/engine/db/obj_save.cpp +++ b/src/engine/db/obj_save.cpp @@ -11,6 +11,7 @@ #include "engine/core/char_handler.h" #include "gameplay/affects/obj_affects.h" +#include "utils/native_text.h" #include "gameplay/mechanics/equipment.h" #include "obj_save.h" #include "gameplay/mechanics/groups.h" @@ -1662,7 +1663,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) { diff --git a/src/engine/db/sqlite_world_data_source.cpp b/src/engine/db/sqlite_world_data_source.cpp index bce8f4ca0..9cb9da9b4 100644 --- a/src/engine/db/sqlite_world_data_source.cpp +++ b/src/engine/db/sqlite_world_data_source.cpp @@ -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" @@ -1254,7 +1255,7 @@ std::vector SqliteWorldDataSource::LoadRooms(const std::vector 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()) diff --git a/src/engine/db/yaml_world_data_source.cpp b/src/engine/db/yaml_world_data_source.cpp index 7fa14f462..c6f8d97cf 100644 --- a/src/engine/db/yaml_world_data_source.cpp +++ b/src/engine/db/yaml_world_data_source.cpp @@ -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" @@ -1419,7 +1420,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", ""); diff --git a/src/engine/ui/cmd/do_gen_comm.cpp b/src/engine/ui/cmd/do_gen_comm.cpp index 7af33bc90..30c2dc3f1 100644 --- a/src/engine/ui/cmd/do_gen_comm.cpp +++ b/src/engine/ui/cmd/do_gen_comm.cpp @@ -162,9 +162,8 @@ void do_gen_comm(CharData *ch, char *argument, int/* cmd*/, int subcmd) { // The denominator is the character count for the same reason -- with byte lengths the // percentage would be halved for Russian text. const size_t total_chars = native_text::char_count(argument); - for (int k = 0; argument[k] != '\0';) { - const int bytes = static_cast(native_text::char_bytes(argument + k)); - if (native_text::is_upper_char(argument + k)) { + for (auto letter : native_text::chars(argument)) { + if (native_text::is_upper_char(letter.data())) { bad_simb_cnt++; bad_seq_cnt++; } else @@ -172,10 +171,11 @@ void do_gen_comm(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if ((bad_seq_cnt > 1) && (((bad_simb_cnt * 100 / total_chars) > bad_smb_procent) || - (bad_seq_cnt > MAX_UPPERS_SEQ_CHAR))) - native_text::copy_lower_char(argument + k, argument + k); - - k += bytes; + (bad_seq_cnt > MAX_UPPERS_SEQ_CHAR))) { + // letter указывает внутрь argument; свёртка регистра не меняет длину. + char *at = argument + (letter.data() - argument); + native_text::copy_lower_char(at, at); + } } // фильтруем одинаковые сообщения в эфире if (!str_cmp(ch->get_last_tell().c_str(), argument)) { @@ -302,7 +302,7 @@ std::string format_gossip_name(CharData *ch, CharData *vict) { return ""; } std::string name = privilege::IsImmortal(ch) ? GET_NAME(ch) : sight::PersonName(ch, vict, 0); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); return name; } diff --git a/src/engine/ui/cmd/do_ignore.cpp b/src/engine/ui/cmd/do_ignore.cpp index 8aa55b047..2ba9d612f 100644 --- a/src/engine/ui/cmd/do_ignore.cpp +++ b/src/engine/ui/cmd/do_ignore.cpp @@ -7,6 +7,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "utils/utils_string.h" #include "utils/utils.h" #include "engine/core/comm.h" @@ -43,7 +44,7 @@ void do_ignore(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { strcpy(name, "Все"); } else { strcpy(name, ign_find_name(ignore->id)); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); } sprintf(buf, " %s: ", name); SendMsgToChar(buf, ch); @@ -146,7 +147,7 @@ void do_ignore(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar("Вы и так не игнорируете всех сразу.\r\n", ch); } else { strcpy(name, ign_find_name(vict_id)); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); sprintf(buf, "Вы и так не игнорируете " "персонажа %s%s%s.\r\n", kColorWht, name, kColorNrm); @@ -173,7 +174,7 @@ void do_ignore(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar(buf, ch); } else { strcpy(name, ign_find_name(ignore->id)); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); sprintf(buf, "Для персонажа %s%s%s вы игнорируете:%s.\r\n", kColorWht, name, kColorNrm, text_ignore_modes(ignore->mode, buf1)); SendMsgToChar(buf, ch); @@ -183,7 +184,7 @@ void do_ignore(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar("Вы больше не игнорируете всех сразу.\r\n", ch); } else { strcpy(name, ign_find_name(vict_id)); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); sprintf(buf, "Вы больше не игнорируете персонажа %s%s%s.\r\n", kColorWht, name, kColorNrm); SendMsgToChar(buf, ch); diff --git a/src/engine/ui/cmd/do_who_am_i.cpp b/src/engine/ui/cmd/do_who_am_i.cpp index c9189e44b..31998bcc3 100644 --- a/src/engine/ui/cmd/do_who_am_i.cpp +++ b/src/engine/ui/cmd/do_who_am_i.cpp @@ -7,6 +7,7 @@ */ #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "gameplay/clans/house.h" #include "engine/db/player_index.h" #include "gameplay/core/remort.h" @@ -29,7 +30,7 @@ void DoWhoAmI(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { } else { const int god_level = (ch)->player_specials->saved.NameGod > 1000 ? (ch)->player_specials->saved.NameGod - 1000 : (ch)->player_specials->saved.NameGod; sprintf(buf1, "%s", GetNameById((ch)->player_specials->saved.NameIDGod).c_str()); - *buf1 = UPPER(*buf1); + native_text::capitalize_first(buf1); static const char *by_rank_god = "Богом"; static const char *by_rank_privileged = "привилегированным игроком"; diff --git a/src/engine/ui/cmd_god/do_set_all.cpp b/src/engine/ui/cmd_god/do_set_all.cpp index e832949bc..b254ce472 100644 --- a/src/engine/ui/cmd_god/do_set_all.cpp +++ b/src/engine/ui/cmd_god/do_set_all.cpp @@ -7,6 +7,7 @@ */ #include "engine/ui/cmd_god/do_set_all.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/karma.h" @@ -167,7 +168,7 @@ void setall_inspect() { } Password::set_password(vict, std::string(it->second->pwd)); std::string str = player_table[it->second->pos].name(); - str[0] = UPPER(str[0]); + native_text::capitalize_first(str); sprintf(buf2, "У персонажа %s изменен пароль (setall).", player_table[it->second->pos].name().c_str()); it->second->out += buf2; sprintf(buf1, "\r\n"); diff --git a/src/engine/ui/cmd_god/do_show.cpp b/src/engine/ui/cmd_god/do_show.cpp index fd2c0229f..c7a16a52c 100644 --- a/src/engine/ui/cmd_god/do_show.cpp +++ b/src/engine/ui/cmd_god/do_show.cpp @@ -3,6 +3,7 @@ // #include "administration/accounts.h" +#include "utils/native_text.h" #include "administration/ban.h" #include "administration/privilege.h" #include "engine/ui/cmd/do_features.h" @@ -594,11 +595,11 @@ void do_show(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { sprintf(buf + strlen(buf), "Имя никем не одобрено!\r\n"); } else if ((vict)->player_specials->saved.NameGod < 1000) { sprintf(buf1, "%s", GetNameById((vict)->player_specials->saved.NameIDGod).c_str()); - *buf1 = UPPER(*buf1); + native_text::capitalize_first(buf1); snprintf(buf + strlen(buf), kMaxStringLength, "Имя запрещено богом %s\r\n", buf1); } else { sprintf(buf1, "%s", GetNameById((vict)->player_specials->saved.NameIDGod).c_str()); - *buf1 = UPPER(*buf1); + native_text::capitalize_first(buf1); snprintf(buf + strlen(buf), kMaxStringLength, "Имя одобрено богом %s\r\n", buf1); } if (remort::GetRealRemort(vict) < 4) diff --git a/src/engine/ui/cmd_god/do_stat.cpp b/src/engine/ui/cmd_god/do_stat.cpp index fe766e7cf..6f058ea5e 100644 --- a/src/engine/ui/cmd_god/do_stat.cpp +++ b/src/engine/ui/cmd_god/do_stat.cpp @@ -1,5 +1,6 @@ #include "gameplay/mechanics/equipment.h" #include "gameplay/affects/obj_affects.h" // issue.obj-affects: Diag +#include "utils/native_text.h" #include "gameplay/affects/affect_messages.h" #include "do_stat.h" #include "utils/utils_string.h" @@ -858,7 +859,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { } } if (!str.empty()) { - str[0] = UPPER(str[0]); + native_text::capitalize_first(str); SendMsgToChar(ch, "&C%s&n", str.c_str()); } else { auto room = get_room_where_obj(j); diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 13f2ec5c8..78da7b0c7 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -5,6 +5,7 @@ ******************************************************************************/ #include "house.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "gameplay/economics/currencies.h" #include "utils/utils_encoding.h" @@ -991,7 +992,7 @@ void Clan::HouseInfo(CharData *ch) { for (const auto &it : temp_list) { if (temp != ranks[it->rank_num]) { std::string rnk = ranks[it->rank_num]; - rnk[0] = UPPER(rnk[0]); + native_text::capitalize_first(rnk); if (temp == "") { buffer << rnk << ": "; @@ -1116,7 +1117,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { return; } std::string name = buffer2; - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); if (unique == ch->get_uid()) { SendMsgToChar("Сам себя повысил, самому себе вынес благодарность?\r\n", ch); return; @@ -1308,7 +1309,7 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { if (d->character && CLAN(d->character) && CLAN(d->character)->GetRent() == this->GetRent()) { - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); SendMsgToChar(d->character.get(), "%s более не является членом вашей дружины.\r\n", name.c_str()); } } @@ -1426,7 +1427,7 @@ void Clan::hcon_outcast(CharData *ch, std::string &buffer) { char tmpstr[kMaxInputLength]; sprintf(tmpstr, "Богом %s", GET_NAME(ch)); clan->remove_member(member_uid, tmpstr); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); SendMsgToChar(ch, "%s исключен(a) из дружины '%s'.\r\n", name.c_str(), clan->name.c_str()); return; } @@ -2073,7 +2074,7 @@ void Clan::HcontrolBuild(CharData *ch, std::string &buffer) { tempClan->chest_room = rent; tempClan->guard = guard; // пишем воеводу - owner[0] = UPPER(owner[0]); + native_text::capitalize_first(owner); tempClan->owner = owner; const auto tempMember = std::make_shared(); tempMember->name = owner; @@ -3479,7 +3480,7 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { else if (CLAN(d->character) && CLAN(ch) != CLAN(d->character)) SendMsgToChar("Вы не можете передать свои права члену другой дружины.\r\n", ch); else { - buffer2[0] = UPPER(buffer2[0]); + native_text::capitalize_first(buffer2); // воевода идет рангом ниже this->m_members.set_rank(ch->get_uid(), 1); Clan::SetClanData(ch); diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 29f379689..f7300e389 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -3,6 +3,7 @@ // Part of Bylins http://www.mud.ru #include "parcel.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "gameplay/economics/currencies.h" @@ -852,8 +853,8 @@ std::string FindParcelObj(const ObjData *obj) { std::string target = GetNameByUnique(it->first); std::string sender = GetNameByUnique(it2->first); - target[0] = UPPER(target[0]); - sender[0] = UPPER(sender[0]); + native_text::capitalize_first(target); + native_text::capitalize_first(sender); str = fmt::format("наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", grammar::ObjPluralVerbEnding((it3->obj_)->get_sex()), sender.c_str(), diff --git a/src/gameplay/core/game_limits.cpp b/src/gameplay/core/game_limits.cpp index 90f3dd159..f0955a799 100644 --- a/src/gameplay/core/game_limits.cpp +++ b/src/gameplay/core/game_limits.cpp @@ -13,6 +13,7 @@ ************************************************************************ */ #include "gameplay/core/game_limits.h" +#include "utils/native_text.h" #include "gameplay/core/experience.h" #include "gameplay/affects/affect_data.h" // issue.mob-flag-affect-materialization: restore re-materialize #include "administration/privilege.h" @@ -1154,7 +1155,7 @@ void exchange_point_update() { if (GET_EXCHANGE_ITEM(exch_item)->get_timer() == 0) { std::string cap = GET_EXCHANGE_ITEM(exch_item)->get_PName(grammar::ECase::kNom); - cap[0] = UPPER(cap[0]); + native_text::capitalize_first(cap); sprintf(buf, "Exchange: - %s рассыпал%s от длительного использования.\r\n", cap.c_str(), grammar::ObjSexEnding((GET_EXCHANGE_ITEM(exch_item))->get_sex(), 2)); log("%s", buf); @@ -1239,7 +1240,7 @@ void charmee_obj_decay_tell(CharData *charmee, ObjData *obj, ECharmeeObjPos obj_ короче, рефакторинг приветствуется, если кто-нибудь придумает лучше. */ std::string cap = obj->get_PName(grammar::ECase::kNom); - cap[0] = UPPER(cap[0]); + native_text::capitalize_first(cap); snprintf(local_buf, kMaxStringLength, "%s сказал%s вам : '%s%s рассыпал%s %s...'", GET_NAME(charmee), grammar::SexEnding((charmee)->get_sex(), 1), diff --git a/src/gameplay/economics/exchange.cpp b/src/gameplay/economics/exchange.cpp index bc58e1f07..3582cb57b 100644 --- a/src/gameplay/economics/exchange.cpp +++ b/src/gameplay/economics/exchange.cpp @@ -9,6 +9,7 @@ ************************************************************************ */ #include "exchange.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/global_objects.h" #include "gameplay/economics/currencies.h" @@ -472,7 +473,7 @@ int exchange_information(CharData *ch, char *arg) { } auto seller_name = GetNameById(GET_EXCHANGE_ITEM_SELLERID(item)); snprintf(buf2, sizeof(buf2), "%s", seller_name.empty() ? "(сожран долгоносиком)" : seller_name.c_str()); - *buf2 = UPPER(*buf2); + native_text::capitalize_first(buf2); out += fmt::sprintf("Продавец %s\n", buf2); if (GET_EXCHANGE_ITEM_COMMENT(item)) { out += fmt::sprintf("Берестовая наклейка на лоте гласит: '%s'.\n", GET_EXCHANGE_ITEM_COMMENT(item)); @@ -668,7 +669,7 @@ int exchange_offers(const CharData *ch, const char *arg) { filter += GET_NAME(ch); } else { while (*arg1) { - arg1[0] = UPPER(arg1[0]); + native_text::capitalize_first(arg1); filter += arg1; filter += ' '; arg = one_argument(arg, arg1); @@ -907,7 +908,7 @@ int LoadExchange() { // Предмет разваливается от старости if (GET_EXCHANGE_ITEM(item)->get_timer() <= 0) { std::string cap = GET_EXCHANGE_ITEM(item)->get_PName(grammar::ECase::kNom); - cap[0] = UPPER(cap[0]); + native_text::capitalize_first(cap); log("Exchange: - %s рассыпал%s от длительного использования.\r\n", cap.c_str(), grammar::ObjSexEnding((GET_EXCHANGE_ITEM(item))->get_sex(), 2)); extract_exchange_item(item); @@ -1002,7 +1003,7 @@ int exchange_database_reload(bool loadbackup) { // Предмет разваливается от старости if (GET_EXCHANGE_ITEM(item)->get_timer() <= 0) { std::string cap = GET_EXCHANGE_ITEM(item)->get_PName(grammar::ECase::kNom); - cap[0] = UPPER(cap[0]); + native_text::capitalize_first(cap); log("Exchange: - %s рассыпал%s от длительного использования.\r\n", cap.c_str(), grammar::ObjSexEnding((GET_EXCHANGE_ITEM(item))->get_sex(), 2)); extract_exchange_item(item); diff --git a/src/gameplay/fight/pk.cpp b/src/gameplay/fight/pk.cpp index 800a424b5..7b092d481 100644 --- a/src/gameplay/fight/pk.cpp +++ b/src/gameplay/fight/pk.cpp @@ -12,6 +12,7 @@ ************************************************************************ */ #include "pk.h" +#include "utils/native_text.h" #include #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" @@ -653,7 +654,7 @@ void do_revenge(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { continue; } - temp[0] = UPPER(temp[0]); + native_text::capitalize_first(temp); // если нада исключаем тех, кто находится оффлайн if (bOnlineOnly) { for (const auto &tch : character_list) { diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index 739d57020..c842e993a 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -4,6 +4,7 @@ // Part of Bylins http://www.mud.ru #include "glory_const.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "utils/grammar/declensions.h" @@ -1127,7 +1128,7 @@ void PrintGloryChart(CharData *ch) { t_it != playerGloryList.end() && i < kPlayerChartSize; ++t_it, ++i) { std::string name = GetNameByUnique(t_it->get()->uid); - name[0] = UPPER(name[0]); + native_text::capitalize_first(name); if (name.length() == 0) { name = "*скрыто*"; } diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 58882327c..f010c4f97 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -7,6 +7,7 @@ #include "engine/core/char_movement.h" #include "gameplay/affects/obj_affects.h" +#include "utils/native_text.h" #include "engine/core/target_resolver.h" #include "sight.h" #include "gameplay/mechanics/hide.h" @@ -1358,7 +1359,7 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s } } else if (mode >= 2 && how <= 1) { std::string obj_name = OBJN(object, ch, grammar::ECase::kNom); - obj_name[0] = UPPER(obj_name[0]); + native_text::capitalize_first(obj_name); if (object->get_type() == EObjType::kLightSource) { if (GET_OBJ_VAL(object, 2) == -1) { sprintf(buf2, "\r\n%s дает вечный свет.", obj_name.c_str()); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 8c72edd93..bed49dee0 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -182,14 +182,16 @@ bool chars_equal_ci(const char *a, const char *b) { return utf8::to_lower(ca) == utf8::to_lower(cb); } -std::size_t copy_lower_char(const char *src, char *dst) { +namespace { + +std::size_t copy_folded_char(const char *src, char *dst, char32_t (*fold)(char32_t)) { const std::size_t len = char_bytes(src); char32_t cp = 0; if (utf8::decode(std::string_view(src, len), 0, cp) != 0) { std::string folded; - // Only rewrite when the lowercase form keeps the byte length -- true for ASCII and for - // the whole Russian alphabet, so callers never see a character change size. - if (utf8::encode(utf8::to_lower(cp), folded) == len) { + // Only rewrite when the folded form keeps the byte length -- true for ASCII and for the + // whole Russian alphabet, so callers never see a character change size. + if (utf8::encode(fold(cp), folded) == len) { for (std::size_t i = 0; i < len; ++i) { dst[i] = folded[i]; } @@ -202,6 +204,16 @@ std::size_t copy_lower_char(const char *src, char *dst) { return len; } +} // namespace + +std::size_t copy_lower_char(const char *src, char *dst) { + return copy_folded_char(src, dst, utf8::to_lower); +} + +std::size_t copy_upper_char(const char *src, char *dst) { + return copy_folded_char(src, dst, utf8::to_upper); +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -287,6 +299,11 @@ std::size_t copy_lower_char(const char *src, char *dst) { return 1; } +std::size_t copy_upper_char(const char *src, char *dst) { + *dst = a_ucc_table[static_cast(*src)]; + return 1; +} + #endif // --------------------------------------------------------------------------------------------- @@ -319,6 +336,43 @@ std::size_t char_bytes_at(std::string_view s, std::size_t pos) { } // namespace +void capitalize_first(std::string &s) { + if (s.empty()) { + return; + } + // The uppercase form keeps the byte length for ASCII and the whole Russian alphabet, so + // capitalising in place never resizes the string. + capitalize_first(&s[0]); +} + +std::size_t CharRange::Iterator::step(std::string_view s, std::size_t pos) { + return pos < s.size() ? char_bytes_at(s, pos) : 0; +} + +void to_lower(std::string &s) { + for (std::size_t i = 0; i < s.size();) { + i += copy_lower_char(&s[i], &s[i]); + } +} + +void to_upper(std::string &s) { + for (std::size_t i = 0; i < s.size();) { + i += copy_upper_char(&s[i], &s[i]); + } +} + +void to_lower(char *s) { + while (*s) { + s += copy_lower_char(s, s); + } +} + +void to_upper(char *s) { + while (*s) { + s += copy_upper_char(s, s); + } +} + std::size_t last_char_offset(std::string_view s) { std::size_t last = 0; std::size_t pos = 0; diff --git a/src/utils/native_text.h b/src/utils/native_text.h index d9ba7a3ce..7d0140f0d 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -20,6 +20,7 @@ the encoding flip. Once the flip is permanent the KOI8-R branch and this indirec #define BYLINS_SRC_UTILS_NATIVE_TEXT_H_ #include +#include #include namespace native_text { @@ -38,6 +39,7 @@ std::size_t char_count(std::string_view s); // Cyrillic incl. Yo). No-op on an empty string, on a non-cased first character, or in the (never // occurring for these alphabets) case where the uppercase form has a different byte length. void capitalize_first(char *s); +void capitalize_first(std::string &s); // Largest byte offset <= max_bytes that lands on a character boundary, so cutting the string // there never splits a multibyte character. KOI8-R: min(max_bytes, s.size()). @@ -81,11 +83,58 @@ bool chars_equal_ci(const char *a, const char *b); // than resized. `dst` may alias `src` (the length-preserving property makes that safe). std::size_t copy_lower_char(const char *src, char *dst); +// Uppercase counterpart of copy_lower_char, with the same contract. +std::size_t copy_upper_char(const char *src, char *dst); + // Byte offset at which the final character of `s` begins (0 for an empty string), so that // s.substr(0, last_char_offset(s)) drops exactly one character and s.substr(last_char_offset(s)) // is that character. KOI8-R: s.size() - 1. std::size_t last_char_offset(std::string_view s); +// Range-for over the characters of `s`: each element is a string_view covering exactly one +// character, so scanning code never does pointer arithmetic and never lands mid-character. +// +// for (auto ch : native_text::chars(name)) { ... } // ch is one character, whatever its size +// +// The view must outlive the loop (it is not copied). Malformed bytes yield one element each. +class CharRange { + public: + explicit CharRange(std::string_view s) : m_str(s) {} + + class Iterator { + public: + Iterator(std::string_view s, std::size_t pos) : m_str(s), m_pos(pos), m_len(step(s, pos)) {} + std::string_view operator*() const { return m_str.substr(m_pos, m_len); } + Iterator &operator++() { + m_pos += m_len; + m_len = step(m_str, m_pos); + return *this; + } + bool operator!=(const Iterator &other) const { return m_pos != other.m_pos; } + + private: + static std::size_t step(std::string_view s, std::size_t pos); + std::string_view m_str; + std::size_t m_pos; + std::size_t m_len; + }; + + [[nodiscard]] Iterator begin() const { return Iterator(m_str, 0); } + [[nodiscard]] Iterator end() const { return Iterator(m_str, m_str.size()); } + + private: + std::string_view m_str; +}; + +inline CharRange chars(std::string_view s) { return CharRange(s); } + +// Whole-string case conversion in place. Prefer these over hand-rolled per-character loops. +// Length-preserving for ASCII and the Russian alphabet, so no reallocation happens. +void to_lower(std::string &s); +void to_upper(std::string &s); +void to_lower(char *s); +void to_upper(char *s); + // Does the single character `ch` occur in `list`? The replacement for strchr() over a literal // list of letters: `list` is walked one whole character at a time, so a multibyte character can // never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 7f9725f25..e4adbbe04 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -117,10 +117,13 @@ bool IsAbbr(const char *arg1, const char *arg2) { return false; } - for (; *arg1 && *arg2; arg1++, arg2++) { - if (LOWER(*arg1) != LOWER(*arg2)) { + // Посимвольно (issue #3681): побайтное сравнение под UTF-8 теряет регистронезависимость. + while (*arg1 && *arg2) { + if (!native_text::chars_equal_ci(arg1, arg2)) { return false; } + arg1 += native_text::char_bytes(arg1); + arg2 += native_text::char_bytes(arg2); } if (!*arg1) { @@ -229,9 +232,7 @@ std::string ExtractFirstArgument(const std::string &s, std::string &remains) { } std::string SubstToLow(std::string s) { - for (char &it: s) { - it = LOWER(it); - } + ConvertToLow(s); return s; } @@ -258,29 +259,22 @@ std::string SubstWtoK(std::string s) { } void ConvertToLow(std::string &text) { - for (char &it: text) { - it = LOWER(it); - } + native_text::to_lower(text); } void ConvertToLow(char *text) { - while (*text) { - *text = LOWER(*text); - text++; - } + native_text::to_lower(text); } std::string SubstStrToLow(std::string s) { - for (char &it: s) { - it = UPPER(it); - } + // NB: имя говорит "ToLow", а тело поднимает регистр. Расхождение предсуществующее, + // поведение сохранено намеренно -- меняется только байтовая семантика на символьную. + native_text::to_upper(s); return s; } std::string SubstStrToUpper(std::string s) { - for (char &it: s) { - it = UPPER(it); - } + native_text::to_upper(s); return s; } @@ -477,14 +471,14 @@ const char *first_letter(const char *txt) { char *colorCAP(char *txt) { char *letter = const_cast(first_letter(txt)); if (letter && *letter) { - *letter = UPPER(*letter); + native_text::capitalize_first(letter); } return txt; } std::string &colorCAP(std::string &txt) { size_t pos = first_letter(txt.c_str()) - txt.c_str(); - txt[pos] = UPPER(txt[pos]); + native_text::capitalize_first(&txt[pos]); return txt; } @@ -496,14 +490,14 @@ std::string &colorCAP(std::string &&txt) { char *colorLOW(char *txt) { char *letter = const_cast(first_letter(txt)); if (letter && *letter) { - *letter = LOWER(*letter); + native_text::copy_lower_char(letter, letter); } return txt; } std::string &colorLOW(std::string &txt) { size_t pos = first_letter(txt.c_str()) - txt.c_str(); - txt[pos] = LOWER(txt[pos]); + native_text::copy_lower_char(&txt[pos], &txt[pos]); return txt; } @@ -513,13 +507,13 @@ std::string &colorLOW(std::string &&txt) { } char *CAP(char *txt) { - *txt = UPPER(*txt); + native_text::capitalize_first(txt); return (txt); } std::string CAP(const std::string txt) { std::string tmp_str = txt; - tmp_str[0] = UPPER(tmp_str[0]); + native_text::capitalize_first(tmp_str); return (tmp_str); } diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 6ef53a563..82f66ff64 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace { @@ -269,4 +270,50 @@ TEST(NativeText, ListContainsChar) { } } +TEST(NativeText, CharRangeIteratesWholeCharacters) { + std::vector got; + for (auto c : native_text::chars(kNtPrivet)) { + got.emplace_back(c); + } + if (native_text::native_is_utf8()) { + ASSERT_EQ(got.size(), 6u); // 6 letters, not 12 bytes + EXPECT_EQ(got.front(), "\xD0\x9F"); // whole "P", both bytes + EXPECT_EQ(got.back(), "\xD1\x82"); // whole "t" + } else { + ASSERT_EQ(got.size(), 12u); // KOI8-R: every byte is a character + } + + got.clear(); + for (auto c : native_text::chars("abc")) { + got.emplace_back(c); + } + EXPECT_EQ(got, (std::vector{"a", "b", "c"})); + + got.clear(); + for (auto c : native_text::chars("")) { + got.emplace_back(c); + } + EXPECT_TRUE(got.empty()); +} + +TEST(NativeText, WholeStringCaseTransforms) { + std::string s = "Hello World"; + native_text::to_lower(s); + EXPECT_EQ(s, "hello world"); + native_text::to_upper(s); + EXPECT_EQ(s, "HELLO WORLD"); + + char buf[] = "MiXeD"; + native_text::to_lower(buf); + EXPECT_STREQ(buf, "mixed"); + + if (native_text::native_is_utf8()) { + std::string ru = "\xD0\x9F\xD0\xA0\xD0\x98"; // "PRI" in Cyrillic + native_text::to_lower(ru); + EXPECT_EQ(ru, "\xD0\xBF\xD1\x80\xD0\xB8"); // "pri" + native_text::to_upper(ru); + EXPECT_EQ(ru, "\xD0\x9F\xD0\xA0\xD0\x98"); + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 1875b4049c8f71cff092b4a39a34b273cdf227d1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 04:59:54 +0200 Subject: [PATCH 015/110] perf(utf8): make case folding cost what the byte loop cost (#3681) Benchmarking the character-based case conversion showed it was ~17x slower than the byte loop it replaces under UTF-8. That was not inherent to per-character work; it was this implementation: * a std::string was constructed per character just to hold the re-encoded result; * the fold was reached through a function pointer, so nothing inlined; * char_bytes() called utf8::sequence_length() across a translation unit, once per character, in every scan. Fixed by folding ASCII and the two-byte Cyrillic block directly on the bytes, with the general decode/encode path kept only as a fallback for characters this codebase never carries. Whole-string conversion is now one tight loop with no calls in the hot path. Measured against a like-for-like (also out-of-line) copy of the old ConvertToLow: KOI8-R: 32 ms -> 33 ms (the production path today: no regression) UTF-8 : 140 ms -> 211 ms (1.5x, and the 140 ms baseline does not even fold Cyrillic) The benchmark also caught a real bug this work had introduced: the shared fold loop treated 0xD0/0xD1 as UTF-8 lead bytes, but under KOI8-R those are the letters "p" and "r" -- it would have corrupted text on the current build. The loops are now per encoding. Verified exhaustively: under KOI8-R the result is identical to the raw table for all 255 byte values; under UTF-8 the folds are correct including Yo/yo. Also adds utf8::encode(char32_t, char*), an allocation-free encode into a caller buffer. --- src/utils/native_text.cpp | 237 ++++++++++++++++++++++++++++++++------ src/utils/utf8.cpp | 29 +++++ src/utils/utf8.h | 4 + 3 files changed, 234 insertions(+), 36 deletions(-) diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index bed49dee0..77ef7830d 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -77,12 +77,35 @@ std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { return pos; } +namespace { + +// Local copy of the lead-byte length table. utf8::sequence_length lives in another translation +// unit, and this runs once per character in every scan -- a cross-module call there costs more +// than the work itself. +inline std::size_t lead_len(unsigned char c) { + if (c < 0x80) { + return 1; + } + if (c >= 0xC0 && c <= 0xDF) { + return 2; + } + if (c >= 0xE0 && c <= 0xEF) { + return 3; + } + if (c >= 0xF0 && c <= 0xF7) { + return 4; + } + return 1; +} + +} // namespace + std::size_t char_bytes(const char *s) { const unsigned char lead = static_cast(*s); if (lead < 0x80) { return 1; } - const std::size_t want = static_cast(utf8::sequence_length(lead)); + const std::size_t want = lead_len(lead); std::size_t n = 1; while (n < want && (static_cast(s[n]) & 0xC0) == 0x80) { ++n; @@ -184,22 +207,88 @@ bool chars_equal_ci(const char *a, const char *b) { namespace { -std::size_t copy_folded_char(const char *src, char *dst, char32_t (*fold)(char32_t)) { +// Case folding for the repertoire the engine actually carries -- ASCII and the two-byte Cyrillic +// block -- done directly on the bytes. This runs per character in hot paths (whole-string case +// conversion, argument parsing), so it must not decode, allocate, or make a cross-module call: +// +// A-Z / a-z : one byte, +-0x20 +// A-P (D0 90..D0 9F) <-> a-p (D0 B0..D0 BF) : lead stays D0, trail +-0x20 +// R-Ya(D0 A0..D0 AF) <-> r-ya(D1 80..D1 8F) : lead flips D0<->D1, trail -+0x20 +// Yo (D0 81) <-> yo (D1 91) +// +// Anything else (other scripts, malformed bytes) falls through to the general path below, which +// is correct but slower -- and effectively never taken by this codebase. +inline bool fold_fast(const char *src, char *dst, std::size_t len, bool upper) { + const unsigned char c0 = static_cast(src[0]); + if (c0 < 0x80) { + char c = src[0]; + if (upper) { + if (c >= 'a' && c <= 'z') { + c = static_cast(c - 0x20); + } + } else if (c >= 'A' && c <= 'Z') { + c = static_cast(c + 0x20); + } + dst[0] = c; + return true; + } + if (len != 2) { + return false; + } + const unsigned char c1 = static_cast(src[1]); + unsigned char o0 = c0; + unsigned char o1 = c1; + if (upper) { + if (c0 == 0xD0 && c1 >= 0xB0 && c1 <= 0xBF) { // a-p -> A-P + o1 = static_cast(c1 - 0x20); + } else if (c0 == 0xD1 && c1 >= 0x80 && c1 <= 0x8F) { // r-ya -> R-Ya + o0 = 0xD0; + o1 = static_cast(c1 + 0x20); + } else if (c0 == 0xD1 && c1 == 0x91) { // yo -> Yo + o0 = 0xD0; + o1 = 0x81; + } else if (!((c0 == 0xD0 && c1 >= 0x90 && c1 <= 0xAF) || (c0 == 0xD0 && c1 == 0x81))) { + return false; // not Cyrillic: general path + } + } else { + if (c0 == 0xD0 && c1 >= 0x90 && c1 <= 0x9F) { // A-P -> a-p + o1 = static_cast(c1 + 0x20); + } else if (c0 == 0xD0 && c1 >= 0xA0 && c1 <= 0xAF) { // R-Ya -> r-ya + o0 = 0xD1; + o1 = static_cast(c1 - 0x20); + } else if (c0 == 0xD0 && c1 == 0x81) { // Yo -> yo + o0 = 0xD1; + o1 = 0x91; + } else if (!((c0 == 0xD0 && c1 >= 0xB0) || (c0 == 0xD1 && c1 <= 0x8F) || (c0 == 0xD1 && c1 == 0x91))) { + return false; + } + } + dst[0] = static_cast(o0); + dst[1] = static_cast(o1); + return true; +} + +std::size_t copy_folded_char(const char *src, char *dst, bool upper) { const std::size_t len = char_bytes(src); + if (fold_fast(src, dst, len, upper)) { + return len; + } + // General path: decode, fold, re-encode; only taken for characters outside ASCII+Cyrillic. char32_t cp = 0; if (utf8::decode(std::string_view(src, len), 0, cp) != 0) { - std::string folded; - // Only rewrite when the folded form keeps the byte length -- true for ASCII and for the - // whole Russian alphabet, so callers never see a character change size. - if (utf8::encode(fold(cp), folded) == len) { + const char32_t folded = upper ? utf8::to_upper(cp) : utf8::to_lower(cp); + char tmp[4]; + if (folded != cp && utf8::encode(folded, tmp) == len) { for (std::size_t i = 0; i < len; ++i) { - dst[i] = folded[i]; + dst[i] = tmp[i]; } return len; } } - for (std::size_t i = 0; i < len; ++i) { - dst[i] = src[i]; + if (dst != src) { + for (std::size_t i = 0; i < len; ++i) { + dst[i] = src[i]; + } } return len; } @@ -207,13 +296,72 @@ std::size_t copy_folded_char(const char *src, char *dst, char32_t (*fold)(char32 } // namespace std::size_t copy_lower_char(const char *src, char *dst) { - return copy_folded_char(src, dst, utf8::to_lower); + return copy_folded_char(src, dst, false); } std::size_t copy_upper_char(const char *src, char *dst) { - return copy_folded_char(src, dst, utf8::to_upper); + return copy_folded_char(src, dst, true); +} + + +namespace { + +// Whole-buffer case conversion as one tight loop with no calls in the hot path: ASCII and the +// two-byte Cyrillic block are folded straight on the bytes. Anything else falls back to the +// general helper, which this codebase never hits in practice. Written this way deliberately -- +// a per-character dispatch measured several times slower than the byte loop it replaces. +inline void fold_range_utf8(char *p, char *const end, bool upper) { + while (p < end) { + const unsigned char c0 = static_cast(*p); + if (c0 < 0x80) { + char c = *p; + if (upper) { + if (c >= 'a' && c <= 'z') { + c = static_cast(c - 0x20); + } + } else if (c >= 'A' && c <= 'Z') { + c = static_cast(c + 0x20); + } + *p++ = c; + continue; + } + if ((c0 == 0xD0 || c0 == 0xD1) && p + 1 < end) { + const unsigned char c1 = static_cast(p[1]); + if (upper) { + if (c0 == 0xD0 && c1 >= 0xB0) { + p[1] = static_cast(c1 - 0x20); + } else if (c0 == 0xD1 && c1 <= 0x8F) { + p[0] = static_cast(0xD0); + p[1] = static_cast(c1 + 0x20); + } else if (c0 == 0xD1 && c1 == 0x91) { + p[0] = static_cast(0xD0); + p[1] = static_cast(0x81); + } + } else { + if (c0 == 0xD0 && c1 >= 0x90 && c1 <= 0x9F) { + p[1] = static_cast(c1 + 0x20); + } else if (c0 == 0xD0 && c1 >= 0xA0 && c1 <= 0xAF) { + p[0] = static_cast(0xD1); + p[1] = static_cast(c1 - 0x20); + } else if (c0 == 0xD0 && c1 == 0x81) { + p[0] = static_cast(0xD1); + p[1] = static_cast(0x91); + } + } + p += 2; + continue; + } + p += upper ? copy_upper_char(p, p) : copy_lower_char(p, p); + } } +} // namespace + +void to_lower(std::string &s) { fold_range_utf8(s.data(), s.data() + s.size(), false); } +void to_upper(std::string &s) { fold_range_utf8(s.data(), s.data() + s.size(), true); } +void to_lower(char *s) { fold_range_utf8(s, s + std::char_traits::length(s), false); } +void to_upper(char *s) { fold_range_utf8(s, s + std::char_traits::length(s), true); } + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -304,6 +452,31 @@ std::size_t copy_upper_char(const char *src, char *dst) { return 1; } + +void to_lower(std::string &s) { + for (char &c : s) { + c = a_lcc_table[static_cast(c)]; + } +} + +void to_upper(std::string &s) { + for (char &c : s) { + c = a_ucc_table[static_cast(c)]; + } +} + +void to_lower(char *s) { + for (; *s; ++s) { + *s = a_lcc_table[static_cast(*s)]; + } +} + +void to_upper(char *s) { + for (; *s; ++s) { + *s = a_ucc_table[static_cast(*s)]; + } +} + #endif // --------------------------------------------------------------------------------------------- @@ -314,6 +487,22 @@ std::size_t copy_upper_char(const char *src, char *dst) { namespace { +inline std::size_t lead_len_shared(unsigned char c) { + if (c < 0x80) { + return 1; + } + if (c >= 0xC0 && c <= 0xDF) { + return 2; + } + if (c >= 0xE0 && c <= 0xEF) { + return 3; + } + if (c >= 0xF0 && c <= 0xF7) { + return 4; + } + return 1; +} + // Byte length of the character at `pos`, clamped to the end of `s`. std::size_t char_bytes_at(std::string_view s, std::size_t pos) { #ifdef INTERNAL_ENCODING_UTF8 @@ -321,7 +510,7 @@ std::size_t char_bytes_at(std::string_view s, std::size_t pos) { if (lead < 0x80) { return 1; } - const std::size_t want = static_cast(utf8::sequence_length(lead)); + const std::size_t want = lead_len_shared(lead); std::size_t n = 1; while (n < want && pos + n < s.size() && (static_cast(s[pos + n]) & 0xC0) == 0x80) { ++n; @@ -349,30 +538,6 @@ std::size_t CharRange::Iterator::step(std::string_view s, std::size_t pos) { return pos < s.size() ? char_bytes_at(s, pos) : 0; } -void to_lower(std::string &s) { - for (std::size_t i = 0; i < s.size();) { - i += copy_lower_char(&s[i], &s[i]); - } -} - -void to_upper(std::string &s) { - for (std::size_t i = 0; i < s.size();) { - i += copy_upper_char(&s[i], &s[i]); - } -} - -void to_lower(char *s) { - while (*s) { - s += copy_lower_char(s, s); - } -} - -void to_upper(char *s) { - while (*s) { - s += copy_upper_char(s, s); - } -} - std::size_t last_char_offset(std::string_view s) { std::size_t last = 0; std::size_t pos = 0; diff --git a/src/utils/utf8.cpp b/src/utils/utf8.cpp index c50486e17..8941786da 100644 --- a/src/utils/utf8.cpp +++ b/src/utils/utf8.cpp @@ -143,6 +143,35 @@ std::size_t encode(char32_t cp, std::string &out) { return 0; } +std::size_t encode(char32_t cp, char *out) { + if (cp <= 0x7F) { + out[0] = static_cast(cp); + return 1; + } + if (cp <= 0x7FF) { + out[0] = static_cast(0xC0 | (cp >> 6)); + out[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } + if (cp >= 0xD800 && cp <= 0xDFFF) { + return 0; + } + if (cp <= 0xFFFF) { + out[0] = static_cast(0xE0 | (cp >> 12)); + out[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + out[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } + if (cp <= 0x10FFFF) { + out[0] = static_cast(0xF0 | (cp >> 18)); + out[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + out[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + out[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; +} + bool is_valid(std::string_view s) { std::size_t pos = 0; const std::size_t n = s.size(); diff --git a/src/utils/utf8.h b/src/utils/utf8.h index 7991d4c04..f65f32ec3 100644 --- a/src/utils/utf8.h +++ b/src/utils/utf8.h @@ -37,6 +37,10 @@ std::size_t decode(std::string_view s, std::size_t pos, char32_t &cp); // not a valid Unicode scalar (surrogate half or > U+10FFFF), in which case `out` is untouched. std::size_t encode(char32_t cp, std::string &out); +// Same, but writes into a caller-supplied buffer of at least 4 bytes and never allocates. +// Returns the number of bytes written, or 0 for a value that is not a Unicode scalar. +std::size_t encode(char32_t cp, char *out); + // Strict, whole-string well-formedness check per the Unicode Table 3-7 byte-sequence grammar // (rejects overlong forms, surrogates, code points above U+10FFFF and stray continuation bytes). bool is_valid(std::string_view s); From baba4e758da228e7491156a3d584e33a757d95be Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 05:13:21 +0200 Subject: [PATCH 016/110] feat(utf8): encoding-neutral Russian menu keys (A4 groundwork, #3681) Menus and OLC editors dispatch on a Cyrillic character literal (231 case labels across 19 files). Those cannot survive the flip: with UTF-8 sources a Cyrillic 'x' is a multi-character constant, so the compiler folds it to an implementation-defined value and the menu silently stops responding -- and no single literal spelling works for both encodings. Rather than give up the switch (option A, if/else chains) or bundle 231 rewrites into the flip commit (option B), the letters become numeric constants defined per encoding: switch (native_text::first_char_code(arg)) { case 'y': case 'Y': case rus::kDe: case rus::kDeUpper: * src/utils/russian_keys.h -- all 33 letters, upper and lower, as byte values under KOI8-R and code points under UTF-8. Both tables were generated and checked against the actual codecs rather than written from memory: 0 mismatches in 66 entries. * native_text::first_char_code() -- the raw byte under KOI8-R, the code point under UTF-8, so the switch expression means the same thing in both. * ASCII cases stay ordinary literals; they are identical in either encoding. This lands now as a no-op (under KOI8-R the constant is exactly the byte the old literal compiled to) and leaves the flip a one-flag change. At cleanup the KOI8-R half of the table goes away and the constants can collapse into plain U'...' literals. Includes the first converted switch (medit's save prompt) as a worked example, and a test pinning every constant against first_char_code -- verified in both branches, since a drift here would disable a menu key without any visible error. Remaining: ~229 labels in 18 files, mechanical from here. --- src/engine/olc/medit.cpp | 12 ++-- src/utils/native_text.cpp | 13 ++++ src/utils/native_text.h | 6 ++ src/utils/russian_keys.h | 125 ++++++++++++++++++++++++++++++++++++++ tests/native_text.cpp | 26 ++++++++ 5 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 src/utils/russian_keys.h diff --git a/src/engine/olc/medit.cpp b/src/engine/olc/medit.cpp index 8b5abcc0e..16b8f565e 100644 --- a/src/engine/olc/medit.cpp +++ b/src/engine/olc/medit.cpp @@ -8,6 +8,8 @@ ***************************************************************************/ #include "engine/db/world_characters.h" +#include "utils/native_text.h" +#include "utils/russian_keys.h" #include "gameplay/affects/affect_messages.h" #include "gameplay/fight/fight_messages.h" #include "engine/entities/obj_data.h" @@ -1289,11 +1291,11 @@ void medit_parse(DescriptorData *d, char *arg) { case MEDIT_CONFIRM_SAVESTRING: // * Ensure mob has MOB_ISNPC set or things will go pair shaped. OLC_MOB(d)->SetFlag(EMobFlag::kNpc); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': + case rus::kDe: + case rus::kDeUpper: // * Save the mob in memory and to disk. // SendMsgToChar("Saving mobile to memory a.\r\n", d->character.get()); medit_save_internally(d); @@ -1307,8 +1309,8 @@ void medit_parse(DescriptorData *d, char *arg) { case 'n': case 'N': - case 'н': - case 'Н': cleanup_olc(d, CLEANUP_ALL); + case rus::kEn: + case rus::kEnUpper: cleanup_olc(d, CLEANUP_ALL); break; default: SendMsgToChar("Неверный выбор!\r\n", d->character.get()); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 77ef7830d..df1ecc104 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -304,6 +304,15 @@ std::size_t copy_upper_char(const char *src, char *dst) { } +char32_t first_char_code(const char *s) { + if (s == nullptr || *s == '\0') { + return 0; + } + char32_t cp = 0; + utf8::decode(std::string_view(s, char_bytes(s)), 0, cp); + return cp; +} + namespace { // Whole-buffer case conversion as one tight loop with no calls in the hot path: ASCII and the @@ -453,6 +462,10 @@ std::size_t copy_upper_char(const char *src, char *dst) { } +char32_t first_char_code(const char *s) { + return (s == nullptr || *s == '\0') ? 0 : static_cast(*s); +} + void to_lower(std::string &s) { for (char &c : s) { c = a_lcc_table[static_cast(c)]; diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 7d0140f0d..24c45cea9 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -50,6 +50,12 @@ std::size_t truncate_offset(std::string_view s, std::size_t max_bytes); // bytes actually present (never counts past a terminator or a non-continuation byte). std::size_t char_bytes(const char *s); +// Numeric identity of the character starting at `s`, for dispatching a switch on a letter: +// the raw byte under KOI8-R, the code point under UTF-8. Compare against the constants in +// utils/russian_keys.h (Cyrillic) or ordinary character literals (ASCII, identical in both). +// Returns 0 on an empty string. +char32_t first_char_code(const char *s); + // Case-insensitive comparison in the native encoding: lexicographic over lowered characters, // the shorter string orders first, returns the signed difference at the first mismatch (0 when // equal). KOI8-R: per byte, via LOWER() -- matches str_cmp/str/str semantics. UTF-8: per code diff --git a/src/utils/russian_keys.h b/src/utils/russian_keys.h new file mode 100644 index 000000000..85f21c368 --- /dev/null +++ b/src/utils/russian_keys.h @@ -0,0 +1,125 @@ +/** +\file russian_keys.h - a part of the Bylins engine. +\brief Russian letters as switch-able constants, valid in both native encodings (issue #3681). + +Menus and OLC editors dispatch on a single letter the player typed: + + switch (*arg) { + case 'y': case 'Y': case 'd': case 'D': ... + +A Cyrillic *character* literal cannot survive the encoding flip: with UTF-8 sources 'd' (Cyrillic) +is a multi-character constant, so the compiler folds it to an implementation-defined value and the +menu silently stops responding. Nor can such a literal be written portably for both encodings. + +The way out is to keep the switch but dispatch on a number instead of a literal. Under KOI8-R a +letter is one byte, under UTF-8 it is a code point, so each letter is spelled out numerically for +both and the constants below are what the switch compares against: + + switch (native_text::first_char_code(arg)) { + case 'y': case 'Y': case rus::kDa: case rus::kDaUpper: ... + +Note that ASCII cases stay ordinary character literals -- those are identical in both encodings. + +Once the flip is permanent (track D) the KOI8-R half goes away and these can collapse into plain +U'...' literals. +*/ + +#ifndef BYLINS_SRC_UTILS_RUSSIAN_KEYS_H_ +#define BYLINS_SRC_UTILS_RUSSIAN_KEYS_H_ + +namespace rus { + +#ifdef INTERNAL_ENCODING_UTF8 + +// Unicode code points: U+0410..U+042F (upper), U+0430..U+044F (lower), U+0401/U+0451 (Yo). +#define BYLINS_RUS_LETTER(name, upper_cp, lower_cp) \ + constexpr char32_t name##Upper = upper_cp; \ + constexpr char32_t name = lower_cp + +#else + +// KOI8-R byte values. The alphabet is not laid out alphabetically in KOI8-R, so every letter is +// spelled out rather than derived from an offset. +#define BYLINS_RUS_LETTER(name, upper_byte, lower_byte) \ + constexpr char32_t name##Upper = upper_byte; \ + constexpr char32_t name = lower_byte + +#endif + +#ifdef INTERNAL_ENCODING_UTF8 +BYLINS_RUS_LETTER(kA, 0x0410, 0x0430); // А а +BYLINS_RUS_LETTER(kBe, 0x0411, 0x0431); // Б б +BYLINS_RUS_LETTER(kVe, 0x0412, 0x0432); // В в +BYLINS_RUS_LETTER(kGe, 0x0413, 0x0433); // Г г +BYLINS_RUS_LETTER(kDe, 0x0414, 0x0434); // Д д +BYLINS_RUS_LETTER(kIe, 0x0415, 0x0435); // Е е +BYLINS_RUS_LETTER(kYo, 0x0401, 0x0451); // Ё ё +BYLINS_RUS_LETTER(kZhe, 0x0416, 0x0436); // Ж ж +BYLINS_RUS_LETTER(kZe, 0x0417, 0x0437); // З з +BYLINS_RUS_LETTER(kI, 0x0418, 0x0438); // И и +BYLINS_RUS_LETTER(kIi, 0x0419, 0x0439); // Й й +BYLINS_RUS_LETTER(kKa, 0x041A, 0x043A); // К к +BYLINS_RUS_LETTER(kEl, 0x041B, 0x043B); // Л л +BYLINS_RUS_LETTER(kEm, 0x041C, 0x043C); // М м +BYLINS_RUS_LETTER(kEn, 0x041D, 0x043D); // Н н +BYLINS_RUS_LETTER(kO, 0x041E, 0x043E); // О о +BYLINS_RUS_LETTER(kPe, 0x041F, 0x043F); // П п +BYLINS_RUS_LETTER(kEr, 0x0420, 0x0440); // Р р +BYLINS_RUS_LETTER(kEs, 0x0421, 0x0441); // С с +BYLINS_RUS_LETTER(kTe, 0x0422, 0x0442); // Т т +BYLINS_RUS_LETTER(kU, 0x0423, 0x0443); // У у +BYLINS_RUS_LETTER(kEf, 0x0424, 0x0444); // Ф ф +BYLINS_RUS_LETTER(kHa, 0x0425, 0x0445); // Х х +BYLINS_RUS_LETTER(kTse, 0x0426, 0x0446); // Ц ц +BYLINS_RUS_LETTER(kChe, 0x0427, 0x0447); // Ч ч +BYLINS_RUS_LETTER(kSha, 0x0428, 0x0448); // Ш ш +BYLINS_RUS_LETTER(kScha,0x0429, 0x0449); // Щ щ +BYLINS_RUS_LETTER(kHard,0x042A, 0x044A); // Ъ ъ +BYLINS_RUS_LETTER(kYery,0x042B, 0x044B); // Ы ы +BYLINS_RUS_LETTER(kSoft,0x042C, 0x044C); // Ь ь +BYLINS_RUS_LETTER(kE, 0x042D, 0x044D); // Э э +BYLINS_RUS_LETTER(kYu, 0x042E, 0x044E); // Ю ю +BYLINS_RUS_LETTER(kYa, 0x042F, 0x044F); // Я я +#else +BYLINS_RUS_LETTER(kA, 0xE1, 0xC1); // А а +BYLINS_RUS_LETTER(kBe, 0xE2, 0xC2); // Б б +BYLINS_RUS_LETTER(kVe, 0xF7, 0xD7); // В в +BYLINS_RUS_LETTER(kGe, 0xE7, 0xC7); // Г г +BYLINS_RUS_LETTER(kDe, 0xE4, 0xC4); // Д д +BYLINS_RUS_LETTER(kIe, 0xE5, 0xC5); // Е е +BYLINS_RUS_LETTER(kYo, 0xB3, 0xA3); // Ё ё +BYLINS_RUS_LETTER(kZhe, 0xF6, 0xD6); // Ж ж +BYLINS_RUS_LETTER(kZe, 0xFA, 0xDA); // З з +BYLINS_RUS_LETTER(kI, 0xE9, 0xC9); // И и +BYLINS_RUS_LETTER(kIi, 0xEA, 0xCA); // Й й +BYLINS_RUS_LETTER(kKa, 0xEB, 0xCB); // К к +BYLINS_RUS_LETTER(kEl, 0xEC, 0xCC); // Л л +BYLINS_RUS_LETTER(kEm, 0xED, 0xCD); // М м +BYLINS_RUS_LETTER(kEn, 0xEE, 0xCE); // Н н +BYLINS_RUS_LETTER(kO, 0xEF, 0xCF); // О о +BYLINS_RUS_LETTER(kPe, 0xF0, 0xD0); // П п +BYLINS_RUS_LETTER(kEr, 0xF2, 0xD2); // Р р +BYLINS_RUS_LETTER(kEs, 0xF3, 0xD3); // С с +BYLINS_RUS_LETTER(kTe, 0xF4, 0xD4); // Т т +BYLINS_RUS_LETTER(kU, 0xF5, 0xD5); // У у +BYLINS_RUS_LETTER(kEf, 0xE6, 0xC6); // Ф ф +BYLINS_RUS_LETTER(kHa, 0xE8, 0xC8); // Х х +BYLINS_RUS_LETTER(kTse, 0xE3, 0xC3); // Ц ц +BYLINS_RUS_LETTER(kChe, 0xFE, 0xDE); // Ч ч +BYLINS_RUS_LETTER(kSha, 0xFB, 0xDB); // Ш ш +BYLINS_RUS_LETTER(kScha,0xFD, 0xDD); // Щ щ +BYLINS_RUS_LETTER(kHard,0xFF, 0xDF); // Ъ ъ +BYLINS_RUS_LETTER(kYery,0xF9, 0xD9); // Ы ы +BYLINS_RUS_LETTER(kSoft,0xF8, 0xD8); // Ь ь +BYLINS_RUS_LETTER(kE, 0xFC, 0xDC); // Э э +BYLINS_RUS_LETTER(kYu, 0xE0, 0xC0); // Ю ю +BYLINS_RUS_LETTER(kYa, 0xF1, 0xD1); // Я я +#endif + +#undef BYLINS_RUS_LETTER + +} // namespace rus + +#endif // BYLINS_SRC_UTILS_RUSSIAN_KEYS_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 82f66ff64..e0ffe7ff0 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -5,6 +5,7 @@ // flag the library was built with), so this one test file is correct under either build. #include "utils/native_text.h" +#include "utils/russian_keys.h" #include @@ -316,4 +317,29 @@ TEST(NativeText, WholeStringCaseTransforms) { } } +TEST(NativeText, RussianKeysMatchFirstCharCode) { + // The switch-dispatch contract: for every Russian letter, the constant in russian_keys.h must + // equal what first_char_code() returns for that letter in the build's native encoding. If this + // ever drifts, menus and OLC editors silently stop responding to that key. + struct Case { const char *koi8; const char *utf8; char32_t expected; }; + const Case cases[] = { + {"\xC1", "\xD0\xB0", rus::kA}, {"\xE1", "\xD0\x90", rus::kAUpper}, + {"\xC4", "\xD0\xB4", rus::kDe}, {"\xE4", "\xD0\x94", rus::kDeUpper}, + {"\xCE", "\xD0\xBD", rus::kEn}, {"\xEE", "\xD0\x9D", rus::kEnUpper}, + {"\xD1", "\xD1\x8F", rus::kYa}, {"\xF1", "\xD0\xAF", rus::kYaUpper}, + {"\xA3", "\xD1\x91", rus::kYo}, {"\xB3", "\xD0\x81", rus::kYoUpper}, + {"\xD7", "\xD0\xB2", rus::kVe}, {"\xC8", "\xD1\x85", rus::kHa}, + }; + for (const auto &c : cases) { + const char *const input = native_text::native_is_utf8() ? c.utf8 : c.koi8; + EXPECT_EQ(native_text::first_char_code(input), c.expected); + } + + // ASCII keys are unchanged by the flip and stay ordinary character literals in the switches. + EXPECT_EQ(native_text::first_char_code("y"), static_cast('y')); + EXPECT_EQ(native_text::first_char_code("N"), static_cast('N')); + EXPECT_EQ(native_text::first_char_code(""), 0u); + EXPECT_EQ(native_text::first_char_code(nullptr), 0u); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 6132bf0761cbad638d46c996f690b9ff4af237dd Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 05:38:10 +0200 Subject: [PATCH 017/110] feat(utf8): convert all Cyrillic menu keys to encoding-neutral constants (#3681) Completes the A4 sweep: 227 case labels across 15 files plus the remaining single-letter comparisons now dispatch on native_text::first_char_code() against the rus:: constants, so menus and OLC editors keep working after the flip instead of silently going deaf. * 29 switch expressions rewritten: *arg / *argument -> first_char_code(), LOWER(...) -> first_char_code_lower(), UPPER(...) -> first_char_code_upper(). The script refused to touch any switch shape it did not recognise, so nothing was converted blind; do_who's `switch (mode)` was handled through its initialiser. * Comparison sites in named_stuff, modify and login converted the same way. * iosystem's legacy zMUD 'z' -> Cyrillic substitution now writes the letter from a *string* literal: string literals are byte-transparent, so the same code is correct whether the letter is one byte (KOI8-R) or two (UTF-8). A character literal cannot be, which is the whole point of this step. Left untouched on purpose: six occurrences inside comments (prose, not code). One site is deliberately NOT converted -- db.cpp's get_filename(). It transliterates a player name into the save-file name byte by byte, so under UTF-8 it would produce a different filename and every existing player's files would stop being found. That is a data-loss risk, not a mechanical edit: it needs character-wise transliteration plus a test pinning the filename across the flip. Recorded in the issue as C1a. Verified: clean build, full suite 651 passed / 0 failed. --- src/engine/core/iosystem.cpp | 14 ++- src/engine/olc/medit.cpp | 104 +++++++++++------------ src/engine/olc/oedit.cpp | 12 +-- src/engine/olc/redit.cpp | 12 +-- src/engine/olc/zedit.cpp | 12 +-- src/engine/ui/cmd/do_display.cpp | 24 +++--- src/engine/ui/cmd/do_who.cpp | 6 +- src/engine/ui/cmd_god/do_print_armor.cpp | 12 +-- src/engine/ui/login.cpp | 34 ++++---- src/engine/ui/modify.cpp | 10 ++- src/engine/ui/objects_filter.cpp | 30 ++++--- src/gameplay/clans/house.cpp | 35 ++++---- src/gameplay/core/genchar.cpp | 63 +++++++------- src/gameplay/mechanics/glory.cpp | 60 ++++++------- src/gameplay/mechanics/glory_const.cpp | 59 ++++++------- src/gameplay/mechanics/named_stuff.cpp | 18 ++-- src/gameplay/mechanics/obj_sets_olc.cpp | 76 +++++++++-------- src/utils/native_text.cpp | 18 ++++ src/utils/native_text.h | 4 + 19 files changed, 335 insertions(+), 268 deletions(-) diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index 1a75220b7..e99880526 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -7,6 +7,8 @@ */ #include "engine/core/iosystem.h" +#include +#include #include "gameplay/core/experience.h" #include "administration/privilege.h" #include "utils/utils_encoding.h" @@ -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(); } } } diff --git a/src/engine/olc/medit.cpp b/src/engine/olc/medit.cpp index 16b8f565e..2f17d27fa 100644 --- a/src/engine/olc/medit.cpp +++ b/src/engine/olc/medit.cpp @@ -1322,7 +1322,7 @@ void medit_parse(DescriptorData *d, char *arg) { //------------------------------------------------------------------- case MEDIT_MAIN_MENU: i = 0; olc_log("%s command %c", GET_NAME(d->character), *arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'q': case 'Q': if (OLC_VAL(d)) // Anything been changed? @@ -1538,118 +1538,118 @@ void medit_parse(DescriptorData *d, char *arg) { medit_disp_helpers(d); return; - case 'а': - case 'А': OLC_MODE(d) = MEDIT_SKILLS; + case rus::kA: + case rus::kAUpper: OLC_MODE(d) = MEDIT_SKILLS; medit_disp_skills(d); return; - case 'б': - case 'Б': OLC_MODE(d) = MEDIT_SPELLS; + case rus::kBe: + case rus::kBeUpper: OLC_MODE(d) = MEDIT_SPELLS; medit_disp_spells(d); return; - case 'в': - case 'В': OLC_MODE(d) = MEDIT_STR; + case rus::kVe: + case rus::kVeUpper: OLC_MODE(d) = MEDIT_STR; i++; break; - case 'г': - case 'Г': OLC_MODE(d) = MEDIT_DEX; + case rus::kGe: + case rus::kGeUpper: OLC_MODE(d) = MEDIT_DEX; i++; break; - case 'д': - case 'Д': OLC_MODE(d) = MEDIT_CON; + case rus::kDe: + case rus::kDeUpper: OLC_MODE(d) = MEDIT_CON; i++; break; - case 'е': - case 'Е': OLC_MODE(d) = MEDIT_WIS; + case rus::kIe: + case rus::kIeUpper: OLC_MODE(d) = MEDIT_WIS; i++; break; - case 'ж': - case 'Ж': OLC_MODE(d) = MEDIT_INT; + case rus::kZhe: + case rus::kZheUpper: OLC_MODE(d) = MEDIT_INT; i++; break; - case 'з': - case 'З': OLC_MODE(d) = MEDIT_CHA; + case rus::kZe: + case rus::kZeUpper: OLC_MODE(d) = MEDIT_CHA; i++; break; - case 'и': - case 'И': OLC_MODE(d) = MEDIT_HEIGHT; + case rus::kI: + case rus::kIUpper: OLC_MODE(d) = MEDIT_HEIGHT; i++; break; - case 'к': - case 'К': OLC_MODE(d) = MEDIT_WEIGHT; + case rus::kKa: + case rus::kKaUpper: OLC_MODE(d) = MEDIT_WEIGHT; i++; break; - case 'л': - case 'Л': OLC_MODE(d) = MEDIT_SIZE; + case rus::kEl: + case rus::kElUpper: OLC_MODE(d) = MEDIT_SIZE; i++; break; - case 'м': - case 'М': OLC_MODE(d) = MEDIT_EXTRA; + case rus::kEm: + case rus::kEmUpper: OLC_MODE(d) = MEDIT_EXTRA; i++; break; - case 'х': - case 'Х': OLC_MODE(d) = MEDIT_REMORT; + case rus::kHa: + case rus::kHaUpper: OLC_MODE(d) = MEDIT_REMORT; i++; break; - case 'Ю': - case 'ю': OLC_MODE(d) = MEDIT_MAXFACTOR; + case rus::kYuUpper: + case rus::kYu: OLC_MODE(d) = MEDIT_MAXFACTOR; i++; break; - case 'н': - case 'Н': SendMsgToChar(d->character.get(), "\r\nВведите новое значение от 0 до 100%% :"); + case rus::kEn: + case rus::kEnUpper: SendMsgToChar(d->character.get(), "\r\nВведите новое значение от 0 до 100%% :"); OLC_MODE(d) = MEDIT_LIKE; return; - case 'п': - case 'П': OLC_MODE(d) = MEDIT_DLIST_MENU; + case rus::kPe: + case rus::kPeUpper: OLC_MODE(d) = MEDIT_DLIST_MENU; disp_dl_list(d); return; - case 'р': - case 'Р': OLC_MODE(d) = MEDIT_ROLE; + case rus::kEr: + case rus::kErUpper: OLC_MODE(d) = MEDIT_ROLE; medit_disp_role(d); return; - case 'с': - case 'С': OLC_MODE(d) = MEDIT_RESISTANCES; + case rus::kEs: + case rus::kEsUpper: OLC_MODE(d) = MEDIT_RESISTANCES; medit_disp_resistances(d); return; - case 'т': - case 'Т': OLC_MODE(d) = MEDIT_SAVES; + case rus::kTe: + case rus::kTeUpper: OLC_MODE(d) = MEDIT_SAVES; medit_disp_saves(d); return; - case 'у': - case 'У': OLC_MODE(d) = MEDIT_ADD_PARAMETERS; + case rus::kU: + case rus::kUUpper: OLC_MODE(d) = MEDIT_ADD_PARAMETERS; medit_disp_add_parameters(d); return; - case 'ф': - case 'Ф': OLC_MODE(d) = MEDIT_FEATURES; + case rus::kEf: + case rus::kEfUpper: OLC_MODE(d) = MEDIT_FEATURES; medit_disp_features(d); return; - case 'ц': - case 'Ц': OLC_MODE(d) = MEDIT_RACE; + case rus::kTse: + case rus::kTseUpper: OLC_MODE(d) = MEDIT_RACE; medit_disp_race(d); return; - case 'ч': - case 'Ч': OLC_MODE(d) = MEDIT_CLONE; + case rus::kChe: + case rus::kCheUpper: OLC_MODE(d) = MEDIT_CLONE; medit_disp_clone_menu(d); return; @@ -2167,9 +2167,9 @@ void medit_parse(DescriptorData *d, char *arg) { case MEDIT_DLIST_MENU: if (*arg) { // Обрабатываем комнады добавить удалить и.т.п - switch (*arg) { - case 'а': - case 'А': + switch (native_text::first_char_code(arg)) { + case rus::kA: + case rus::kAUpper: // Добавляем запись. OLC_MODE(d) = MEDIT_DLIST_ADD; SendMsgToChar("\r\nVNUM - виртуальный номер прототипа\r\n" @@ -2188,8 +2188,8 @@ void medit_parse(DescriptorData *d, char *arg) { return; - case 'б': - case 'Б': + case rus::kBe: + case rus::kBeUpper: // Удаляем запись. OLC_MODE(d) = MEDIT_DLIST_DEL; SendMsgToChar("\r\nВведите номер удаляемой записи:", d->character.get()); diff --git a/src/engine/olc/oedit.cpp b/src/engine/olc/oedit.cpp index fd18a382d..661979e38 100644 --- a/src/engine/olc/oedit.cpp +++ b/src/engine/olc/oedit.cpp @@ -9,6 +9,8 @@ ************************************************************************/ #include "engine/db/world_objects.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "gameplay/fight/fight_messages.h" #include "engine/db/obj_prototypes.h" #include "engine/core/conf.h" @@ -1478,11 +1480,11 @@ void oedit_parse(DescriptorData *d, char *arg) { switch (OLC_MODE(d)) { case OEDIT_CONFIRM_SAVESTRING: - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': SendMsgToChar("Объект сохранен.\r\n", d->character.get()); + case rus::kDe: + case rus::kDeUpper: SendMsgToChar("Объект сохранен.\r\n", d->character.get()); OLC_OBJ(d)->remove_incorrect_values_keys(OLC_OBJ(d)->get_type()); oedit_save_internally(d); snprintf(buf, sizeof(buf), "OLC: %s edits obj %d", GET_NAME(d->character), OLC_NUM(d)); @@ -1493,8 +1495,8 @@ void oedit_parse(DescriptorData *d, char *arg) { case 'n': case 'N': - case 'н': - case 'Н': cleanup_olc(d, CLEANUP_ALL); + case rus::kEn: + case rus::kEnUpper: cleanup_olc(d, CLEANUP_ALL); break; default: SendMsgToChar("Неверный выбор!\r\n", d->character.get()); diff --git a/src/engine/olc/redit.cpp b/src/engine/olc/redit.cpp index f06b487ae..3f7954312 100644 --- a/src/engine/olc/redit.cpp +++ b/src/engine/olc/redit.cpp @@ -9,6 +9,8 @@ ************************************************************************/ #include "engine/entities/obj_data.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "engine/core/comm.h" #include "engine/db/db.h" #include "engine/db/world_data_source_manager.h" @@ -556,11 +558,11 @@ void redit_parse(DescriptorData *d, char *arg) { switch (OLC_MODE(d)) { case REDIT_CONFIRM_SAVESTRING: - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': redit_save_internally(d); + case rus::kDe: + case rus::kDeUpper: redit_save_internally(d); snprintf(buf, sizeof(buf), "OLC: %s edits room %d.", GET_NAME(d->character), OLC_NUM(d)); olc_log("%s edit room %d", GET_NAME(d->character), OLC_NUM(d)); mudlog(buf, NRM, std::max(kLvlBuilder, GET_INVIS_LEV(d->character)), SYSLOG, true); @@ -571,8 +573,8 @@ void redit_parse(DescriptorData *d, char *arg) { case 'n': case 'N': - case 'н': - case 'Н': + case rus::kEn: + case rus::kEnUpper: // * Free everything up, including strings, etc. cleanup_olc(d, CLEANUP_ALL); break; diff --git a/src/engine/olc/zedit.cpp b/src/engine/olc/zedit.cpp index c97386f1d..1833ccff3 100644 --- a/src/engine/olc/zedit.cpp +++ b/src/engine/olc/zedit.cpp @@ -5,6 +5,8 @@ ************************************************************************/ #include "engine/db/obj_prototypes.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "engine/entities/obj_data.h" #include "engine/core/comm.h" #include "engine/db/db.h" @@ -1362,11 +1364,11 @@ void zedit_parse(DescriptorData *d, char *arg) { switch (OLC_MODE(d)) { case ZEDIT_CONFIRM_SAVESTRING: - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': + case rus::kDe: + case rus::kDeUpper: // * Save the zone in memory, hiding invisible people. SendMsgToChar("Зона сохранена.\r\n", d->character.get()); zedit_save_internally(d); @@ -1376,8 +1378,8 @@ void zedit_parse(DescriptorData *d, char *arg) { // FALL THROUGH case 'n': case 'N': - case 'н': - case 'Н': cleanup_olc(d, CLEANUP_ALL); + case rus::kEn: + case rus::kEnUpper: cleanup_olc(d, CLEANUP_ALL); break; default: SendMsgToChar("Неверный выбор!\r\n", d->character.get()); SendMsgToChar("Вы желаете сохранить зону? : ", d->character.get()); diff --git a/src/engine/ui/cmd/do_display.cpp b/src/engine/ui/cmd/do_display.cpp index ef7ba374c..6cc71f950 100644 --- a/src/engine/ui/cmd/do_display.cpp +++ b/src/engine/ui/cmd/do_display.cpp @@ -7,6 +7,8 @@ */ #include "engine/entities/char_data.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "administration/privilege.h" const char *DISPLAY_HELP = "Формат: статус { { Ж | Э | З | В | Д | У | О | Б | П | К } | все | нет }\r\n"; @@ -38,35 +40,35 @@ void do_display(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { const size_t len = strlen(argument); for (size_t i = 0; i < len; i++) { - switch (LOWER(argument[i])) { + switch (native_text::first_char_code_lower(argument + i)) { case 'h': - case 'ж': ch->SetFlag(EPrf::kDispHp); + case rus::kZhe: ch->SetFlag(EPrf::kDispHp); break; case 'w': - case 'з': ch->SetFlag(EPrf::kDispMana); + case rus::kZe: ch->SetFlag(EPrf::kDispMana); break; case 'm': - case 'э': ch->SetFlag(EPrf::kDispMove); + case rus::kE: ch->SetFlag(EPrf::kDispMove); break; case 'e': - case 'в': ch->SetFlag(EPrf::kDispExits); + case rus::kVe: ch->SetFlag(EPrf::kDispExits); break; case 'g': - case 'д': ch->SetFlag(EPrf::kDispMoney); + case rus::kDe: ch->SetFlag(EPrf::kDispMoney); break; case 'l': - case 'у': ch->SetFlag(EPrf::kDispLvl); + case rus::kU: ch->SetFlag(EPrf::kDispLvl); break; case 'x': - case 'о': ch->SetFlag(EPrf::kDispExp); + case rus::kO: ch->SetFlag(EPrf::kDispExp); break; - case 'б': + case rus::kBe: case 'f': ch->SetFlag(EPrf::kDispFight); break; - case 'п': + case rus::kPe: case 't': ch->SetFlag(EPrf::kDispTimed); break; - case 'к': + case rus::kKa: case 'c': ch->SetFlag(EPrf::kDispCooldowns); break; case ' ': break; diff --git a/src/engine/ui/cmd/do_who.cpp b/src/engine/ui/cmd/do_who.cpp index 482f91e8c..198d7b295 100644 --- a/src/engine/ui/cmd/do_who.cpp +++ b/src/engine/ui/cmd/do_who.cpp @@ -3,6 +3,8 @@ // #include "engine/ui/cmd/do_who.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include #include "administration/privilege.h" #include "utils/grammar/gender.h" @@ -52,10 +54,10 @@ void DoWho(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { sscanf(arg, "%d-%d", &low, &high); strcpy(buf, buf1); } else if (*arg == '-') { - const char mode = *(arg + 1); // just in case; we destroy arg in the switch + const char32_t mode = native_text::first_char_code(arg + 1); // just in case; we destroy arg in the switch switch (mode) { case 'b': - case 'и': + case rus::kI: if (privilege::IsImmortal(ch) || GET_GOD_FLAG(ch, EGf::kDemigod) || ch->IsFlagged(EPrf::kCoderinfo)) showname = true; strcpy(buf, buf1); diff --git a/src/engine/ui/cmd_god/do_print_armor.cpp b/src/engine/ui/cmd_god/do_print_armor.cpp index 950ed1937..d0797169f 100644 --- a/src/engine/ui/cmd_god/do_print_armor.cpp +++ b/src/engine/ui/cmd_god/do_print_armor.cpp @@ -7,6 +7,8 @@ */ #include "engine/entities/char_data.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/obj_prototypes.h" #include "engine/db/global_objects.h" @@ -34,8 +36,8 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { char tmpbuf[kMaxInputLength]; bool find_param = false; while (*argument) { - switch (*argument) { - case 'М': argument = one_argument(++argument, tmpbuf); + switch (native_text::first_char_code(argument)) { + case rus::kEmUpper: argument = one_argument(++argument, tmpbuf); if (utils::IsAbbr(tmpbuf, "булат")) { filter.material = EObjMaterial::kBulat; } else if (utils::IsAbbr(tmpbuf, "бронза")) { @@ -78,7 +80,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } find_param = true; break; - case 'Т': argument = one_argument(++argument, tmpbuf); + case rus::kTeUpper: argument = one_argument(++argument, tmpbuf); if (utils::IsAbbr(tmpbuf, "броня") || utils::IsAbbr(tmpbuf, "armor")) { filter.type = EObjType::kArmor; } else if (utils::IsAbbr(tmpbuf, "легкие") || utils::IsAbbr(tmpbuf, "легкая")) { @@ -93,7 +95,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } find_param = true; break; - case 'О': argument = one_argument(++argument, tmpbuf); + case rus::kOUpper: argument = one_argument(++argument, tmpbuf); if (utils::IsAbbr(tmpbuf, "тело")) { filter.wear = EWearFlag::kBody; filter.wear_message = 3; @@ -118,7 +120,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } find_param = true; break; - case 'А': { + case rus::kAUpper: { bool tmp_find = false; argument = one_argument(++argument, tmpbuf); if (!strlen(tmpbuf)) { diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index 22ff957ef..78b0a0f4b 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -4,6 +4,8 @@ extracted from interpreter.cpp. Entry point ProcessLoginInput (was nanny). */ #include "interpreter.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "engine/ui/system_messages.h" #include "engine/core/config.h" #include "gameplay/mechanics/condition.h" @@ -186,8 +188,8 @@ int _parse_name(char *argument, char *name) { // skip whitespaces for (i = 0; (*name = (i ? LOWER(*argument) : UPPER(*argument))); argument++, i++, name++) { - if (*argument == 'ё' - || *argument == 'Ё' + if (native_text::first_char_code(argument) == rus::kYo + || native_text::first_char_code(argument) == rus::kYoUpper || !a_isalpha(*argument) || *argument > 0) { return (1); @@ -1538,7 +1540,8 @@ static void HandleGetKeytable(DescriptorData *d, char *argument) { static void HandleNameConfirm(DescriptorData *d, char *argument) { char buffer[kMaxStringLength]; - if (UPPER(*argument) == 'Y' || UPPER(*argument) == 'Д') { + if (native_text::first_char_code_upper(argument) == 'Y' + || native_text::first_char_code_upper(argument) == rus::kDeUpper) { if (ban->IsBanned(d->host) >= BanList::BAN_NEW) { sprintf(buffer, "Попытка создания персонажа %s отклонена для [%s] (siteban)", GET_PC_NAME(d->character), d->host); @@ -1576,7 +1579,8 @@ static void HandleNameConfirm(DescriptorData *d, char *argument) { d->state = EConState::kQsex; return; - } else if (UPPER(*argument) == 'N' || UPPER(*argument) == 'Н') { + } else if (native_text::first_char_code_upper(argument) == 'N' + || native_text::first_char_code_upper(argument) == rus::kEnUpper) { iosystem::write_to_output("Итак, чего изволите? Учтите, бананов нет :)\r\n" "Имя : ", d); d->character->SetCharAliases(nullptr); d->state = EConState::kGetName; @@ -1683,12 +1687,12 @@ static void HandleQuerySex(DescriptorData *d, char *argument) { return; } - switch (UPPER(*argument)) { - case 'М': + switch (native_text::first_char_code_upper(argument)) { + case rus::kEmUpper: case 'M': d->character->set_sex(EGender::kMale); break; - case 'Ж': + case rus::kZheUpper: case 'F': d->character->set_sex(EGender::kFemale); break; @@ -1712,9 +1716,9 @@ static void HandleQueryReligion(DescriptorData *d, char *argument) { return; } - switch (UPPER(*argument)) { - case 'Я': - case 'З': + switch (native_text::first_char_code_upper(argument)) { + case rus::kYaUpper: + case rus::kZeUpper: case 'P': if (class_religion[to_underlying(d->character->GetClass())] == kReligionMono) { iosystem::write_to_output("Персонаж выбранной вами профессии не желает быть язычником!\r\n" @@ -1724,7 +1728,7 @@ static void HandleQueryReligion(DescriptorData *d, char *argument) { GET_RELIGION(d->character) = kReligionPoly; break; - case 'Х': + case rus::kHaUpper: case 'C': if (class_religion[to_underlying(d->character->GetClass())] == kReligionPoly) { iosystem::write_to_output("Персонажу выбранной вами профессии противно христианство!\r\n" @@ -2055,9 +2059,9 @@ static void HandleResetReligion(DescriptorData *d, char *argument) { return; } - switch (UPPER(*argument)) { - case 'Я': - case 'З': + switch (native_text::first_char_code_upper(argument)) { + case rus::kYaUpper: + case rus::kZeUpper: case 'P': if (class_religion[to_underlying(d->character->GetClass())] == kReligionMono) { iosystem::write_to_output("Персонаж выбранной вами профессии не желает быть язычником!\r\n" @@ -2067,7 +2071,7 @@ static void HandleResetReligion(DescriptorData *d, char *argument) { GET_RELIGION(d->character) = kReligionPoly; break; - case 'Х': + case rus::kHaUpper: case 'C': if (class_religion[to_underlying(d->character->GetClass())] == kReligionPoly) { iosystem::write_to_output("Персонажу выбранной вами профессии противно христианство!\r\n" diff --git a/src/engine/ui/modify.cpp b/src/engine/ui/modify.cpp index ae3a00537..837841248 100644 --- a/src/engine/ui/modify.cpp +++ b/src/engine/ui/modify.cpp @@ -13,6 +13,7 @@ ************************************************************************ */ #include +#include "utils/russian_keys.h" #include "engine/db/player_index.h" #include @@ -1269,7 +1270,8 @@ void show_string(DescriptorData *d, char *input) { any_one_arg(input, buf); //* Q is for quit. :) - if (LOWER(*buf) == 'q' || LOWER(*buf) == 'к') { + if (native_text::first_char_code_lower(buf) == 'q' + || native_text::first_char_code_lower(buf) == rus::kKa) { free(d->showstr_vector); d->showstr_count = 0; if (d->showstr_head) { @@ -1281,12 +1283,14 @@ void show_string(DescriptorData *d, char *input) { } // R is for refresh, so back up one page internally so we can display // it again. - else if (LOWER(*buf) == 'r' || LOWER(*buf) == 'п') { + else if (native_text::first_char_code_lower(buf) == 'r' + || native_text::first_char_code_lower(buf) == rus::kPe) { d->showstr_page = MAX(0, d->showstr_page - 1); } // B is for back, so back up two pages internally so we can display the // correct page here. - else if (LOWER(*buf) == 'b' || LOWER(*buf) == 'н') { + else if (native_text::first_char_code_lower(buf) == 'b' + || native_text::first_char_code_lower(buf) == rus::kEn) { d->showstr_page = MAX(0, d->showstr_page - 2); } // Feature to 'goto' a page. Just type the number of the page and you diff --git a/src/engine/ui/objects_filter.cpp b/src/engine/ui/objects_filter.cpp index 98ac3494e..ec0b9671b 100644 --- a/src/engine/ui/objects_filter.cpp +++ b/src/engine/ui/objects_filter.cpp @@ -6,6 +6,8 @@ */ #include "objects_filter.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "gameplay/mechanics/sight.h" #include "gameplay/economics/exchange.h" @@ -825,45 +827,45 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch return false; } while (*argument) { - switch (*argument) { - case 'И': argument = one_argument(++argument, buf_tmp); + switch (native_text::first_char_code(argument)) { + case rus::kIUpper: argument = one_argument(++argument, buf_tmp); if (strlen(buf_tmp) == 0) { SendMsgToChar("Укажите имя предмета.\r\n", ch); return false; } filter.name = buf_tmp; break; - case 'Т': argument = one_argument(++argument, buf_tmp); + case rus::kTeUpper: argument = one_argument(++argument, buf_tmp); if (!filter.init_type(buf_tmp)) { SendMsgToChar("Неверный тип предмета.\r\n", ch); return false; } break; - case 'С': argument = one_argument(++argument, buf_tmp); + case rus::kEsUpper: argument = one_argument(++argument, buf_tmp); if (!filter.init_state(buf_tmp)) { SendMsgToChar("Неверное состояние предмета.\r\n", ch); return false; } break; - case 'О': argument = one_argument(++argument, buf_tmp); + case rus::kOUpper: argument = one_argument(++argument, buf_tmp); if (!filter.init_wear(buf_tmp)) { SendMsgToChar("Неверное место одевания предмета.\r\n", ch); return false; } break; - case 'Ц': argument = one_argument(++argument, buf_tmp); + case rus::kTseUpper: argument = one_argument(++argument, buf_tmp); if (!filter.init_cost(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: Ц<цена><+->.\r\n", ch); return false; } break; - case 'К': argument = one_argument(++argument, buf_tmp); + case rus::kKaUpper: argument = one_argument(++argument, buf_tmp); if (!filter.init_weap_class(buf_tmp)) { SendMsgToChar("Неверный класс оружия.\r\n", ch); return false; } break; - case 'А': { + case rus::kAUpper: { argument = one_argument(++argument, buf_tmp); size_t len = strlen(buf_tmp); if (len == 0) { @@ -878,29 +880,29 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch return false; } break; - } // case 'А' - case 'Р':// стоимость ренты + } // case rus::kAUpper + case rus::kErUpper:// стоимость ренты argument = one_argument(++argument, buf_tmp); if (!filter.init_rent(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: Р<стоимость><+->.\r\n", ch); return false; } break; - case 'М':// количество мортов + case rus::kEmUpper:// количество мортов argument = one_argument(++argument, buf_tmp); if (!filter.init_remorts(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: М<количество мортов><+->.\r\n", ch); return false; } break; - case 'У':// умения + case rus::kUUpper:// умения argument = one_argument(++argument, buf_tmp); if (!filter.init_skill(buf_tmp)) { SendMsgToChar("Неверное умение.\r\n", ch); return false; } break; - case 'В':// имя выставившего на базаре + case rus::kVeUpper:// имя выставившего на базаре argument = one_argument(++argument, buf_tmp); if (filter_type != EXCHANGE) { SendMsgToChar("Только для базара.\r\n", ch); @@ -908,7 +910,7 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch } owner = buf_tmp; break; - case 'П':// профессия (отсечь предметы запрещенные данному классу) + case rus::kPeUpper:// профессия (отсечь предметы запрещенные данному классу) argument = one_argument(++argument, buf_tmp); if (!filter.init_profession(buf_tmp)) { SendMsgToChar("Неверное название профессии.\r\n", ch); diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 78da7b0c7..c100a72bf 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -5,6 +5,7 @@ ******************************************************************************/ #include "house.h" +#include "utils/russian_keys.h" #include "utils/native_text.h" #include "engine/db/player_index.h" #include "gameplay/economics/currencies.h" @@ -2756,9 +2757,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { switch (d->clan_olc->mode) { case CLAN_MAIN_MENU: - switch (*arg) { - case 'в': - case 'В': + switch (native_text::first_char_code(arg)) { + case rus::kVe: + case rus::kVeUpper: case 'q': case 'Q': // есть вариант, что за время в олц в клане изменят кол-во званий @@ -2838,9 +2839,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { break; case CLAN_PRIVILEGE_MENU: - switch (*arg) { - case 'в': - case 'В': + switch (native_text::first_char_code(arg)) { + case rus::kVe: + case rus::kVeUpper: case 'q': case 'Q': // выход в общее меню @@ -2883,11 +2884,11 @@ void Clan::Manage(DescriptorData *d, const char *arg) { break; case CLAN_SAVE_MENU: - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': d->clan_olc->clan->privileges.clear(); + case rus::kDe: + case rus::kDeUpper: d->clan_olc->clan->privileges.clear(); d->clan_olc->clan->privileges = d->clan_olc->privileges; d->clan_olc.reset(); // Clan::ClanSave(); @@ -2897,8 +2898,8 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case 'n': case 'N': - case 'н': - case 'Н': d->clan_olc.reset(); + case rus::kEn: + case rus::kEnUpper: d->clan_olc.reset(); d->state = EConState::kPlaying; SendMsgToChar("Редактирование отменено.\r\n", d->character.get()); return; @@ -2913,9 +2914,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { break; case CLAN_ADDALL_MENU: - switch (*arg) { - case 'в': - case 'В': + switch (native_text::first_char_code(arg)) { + case rus::kVe: + case rus::kVeUpper: case 'q': case 'Q': // выход в общее меню с изменением всех званий @@ -2967,9 +2968,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { break; case CLAN_DELALL_MENU: - switch (*arg) { - case 'в': - case 'В': + switch (native_text::first_char_code(arg)) { + case rus::kVe: + case rus::kVeUpper: case 'q': case 'Q': // выход в общее меню с изменением всех званий diff --git a/src/gameplay/core/genchar.cpp b/src/gameplay/core/genchar.cpp index 7edccc100..777d24e88 100644 --- a/src/gameplay/core/genchar.cpp +++ b/src/gameplay/core/genchar.cpp @@ -13,6 +13,7 @@ ************************************************************************ */ #include "genchar.h" +#include "utils/russian_keys.h" #include "engine/core/conf.h" #include "engine/core/sysdep.h" @@ -95,48 +96,48 @@ void genchar_disp_menu(CharData *ch) { int genchar_parse(CharData *ch, char *arg) { const auto &ch_class = MUD::Class(ch->GetClass()); - switch (*arg) { - case 'А': - case 'а': ch->set_str(std::max(ch->GetInbornStr() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kStr))); + switch (native_text::first_char_code(arg)) { + case rus::kAUpper: + case rus::kA: ch->set_str(std::max(ch->GetInbornStr() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kStr))); break; - case 'Б': - case 'б': ch->set_dex(std::max(ch->GetInbornDex() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kDex))); + case rus::kBeUpper: + case rus::kBe: ch->set_dex(std::max(ch->GetInbornDex() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kDex))); break; - case 'Г': - case 'г': ch->set_int(std::max(ch->GetInbornInt() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kInt))); + case rus::kGeUpper: + case rus::kGe: ch->set_int(std::max(ch->GetInbornInt() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kInt))); break; - case 'Д': - case 'д': ch->set_wis(std::max(ch->GetInbornWis() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kWis))); + case rus::kDeUpper: + case rus::kDe: ch->set_wis(std::max(ch->GetInbornWis() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kWis))); break; - case 'Е': - case 'е': ch->set_con(std::max(ch->GetInbornCon() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kCon))); + case rus::kIeUpper: + case rus::kIe: ch->set_con(std::max(ch->GetInbornCon() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kCon))); break; - case 'Ж': - case 'ж': ch->set_cha(std::max(ch->GetInbornCha() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kCha))); + case rus::kZheUpper: + case rus::kZhe: ch->set_cha(std::max(ch->GetInbornCha() - 1, ch_class.GetBaseStatGenMin(EBaseStat::kCha))); break; - case 'З': - case 'з': ch->set_str(std::min(ch->GetInbornStr() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kStr))); + case rus::kZeUpper: + case rus::kZe: ch->set_str(std::min(ch->GetInbornStr() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kStr))); break; - case 'И': - case 'и': ch->set_dex(std::min(ch->GetInbornDex() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kDex))); + case rus::kIUpper: + case rus::kI: ch->set_dex(std::min(ch->GetInbornDex() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kDex))); break; - case 'К': - case 'к': ch->set_int(std::min(ch->GetInbornInt() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kInt))); + case rus::kKaUpper: + case rus::kKa: ch->set_int(std::min(ch->GetInbornInt() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kInt))); break; - case 'Л': - case 'л': ch->set_wis(std::min(ch->GetInbornWis() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kWis))); + case rus::kElUpper: + case rus::kEl: ch->set_wis(std::min(ch->GetInbornWis() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kWis))); break; - case 'М': - case 'м': ch->set_con(std::min(ch->GetInbornCon() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kCon))); + case rus::kEmUpper: + case rus::kEm: ch->set_con(std::min(ch->GetInbornCon() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kCon))); break; - case 'Н': - case 'н': ch->set_cha(std::min(ch->GetInbornCha() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kCha))); + case rus::kEnUpper: + case rus::kEn: ch->set_cha(std::min(ch->GetInbornCha() + 1, ch_class.GetBaseStatGenMax(EBaseStat::kCha))); break; - case 'П': - case 'п': SendMsgToChar(genchar_help, ch); + case rus::kPeUpper: + case rus::kPe: SendMsgToChar(genchar_help, ch); break; - case 'В': - case 'в': + case rus::kVeUpper: + case rus::kVe: if (CalcBasseStatsSum(ch) != kBaseStatsSum) break; // по случаю успешной генерации сохраняем стартовые статы @@ -147,8 +148,8 @@ int genchar_parse(CharData *ch, char *arg) { ch->set_start_stat(G_CON, ch->GetInbornCon()); ch->set_start_stat(G_CHA, ch->GetInbornCha()); return kGencharExit; - case 'О': - case 'о': { + case rus::kOUpper: + case rus::kO: { const auto &tmp_class = MUD::Class(ch->GetClass()); ch->set_str(tmp_class.GetBaseStatGenAuto(EBaseStat::kStr)); ch->set_dex(tmp_class.GetBaseStatGenAuto(EBaseStat::kDex)); diff --git a/src/gameplay/mechanics/glory.cpp b/src/gameplay/mechanics/glory.cpp index e067e9d92..7bae3338d 100644 --- a/src/gameplay/mechanics/glory.cpp +++ b/src/gameplay/mechanics/glory.cpp @@ -3,6 +3,8 @@ // Part of Bylins http://www.mud.ru #include "glory.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" #include "utils/grammar/declensions.h" @@ -478,57 +480,57 @@ void parse_add_stat(CharData *ch, int stat) { // * Парс олц меню 'слава'. bool parse_spend_glory_menu(CharData *ch, const char *arg) { - switch (*arg) { - case 'А': - case 'а': + switch (native_text::first_char_code(arg)) { + case rus::kAUpper: + case rus::kA: if (ch->desc->glory->olc_add_str >= 1) { if (!parse_remove_stat(ch, G_STR)) break; ch->desc->glory->olc_str -= 1; ch->desc->glory->olc_add_str -= 1; } break; - case 'Б': - case 'б': + case rus::kBeUpper: + case rus::kBe: if (ch->desc->glory->olc_add_dex >= 1) { if (!parse_remove_stat(ch, G_DEX)) break; ch->desc->glory->olc_dex -= 1; ch->desc->glory->olc_add_dex -= 1; } break; - case 'Г': - case 'г': + case rus::kGeUpper: + case rus::kGe: if (ch->desc->glory->olc_add_int >= 1) { if (!parse_remove_stat(ch, G_INT)) break; ch->desc->glory->olc_int -= 1; ch->desc->glory->olc_add_int -= 1; } break; - case 'Д': - case 'д': + case rus::kDeUpper: + case rus::kDe: if (ch->desc->glory->olc_add_wis >= 1) { if (!parse_remove_stat(ch, G_WIS)) break; ch->desc->glory->olc_wis -= 1; ch->desc->glory->olc_add_wis -= 1; } break; - case 'Е': - case 'е': + case rus::kIeUpper: + case rus::kIe: if (ch->desc->glory->olc_add_con >= 1) { if (!parse_remove_stat(ch, G_CON)) break; ch->desc->glory->olc_con -= 1; ch->desc->glory->olc_add_con -= 1; } break; - case 'Ж': - case 'ж': + case rus::kZheUpper: + case rus::kZhe: if (ch->desc->glory->olc_add_cha >= 1) { if (!parse_remove_stat(ch, G_CHA)) break; ch->desc->glory->olc_cha -= 1; ch->desc->glory->olc_add_cha -= 1; } break; - case 'З': - case 'з': + case rus::kZeUpper: + case rus::kZe: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_STR); @@ -536,8 +538,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_str += 1; } break; - case 'И': - case 'и': + case rus::kIUpper: + case rus::kI: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_DEX); @@ -545,8 +547,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_dex += 1; } break; - case 'К': - case 'к': + case rus::kKaUpper: + case rus::kKa: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_INT); @@ -554,8 +556,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_int += 1; } break; - case 'Л': - case 'л': + case rus::kElUpper: + case rus::kEl: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_WIS); @@ -563,8 +565,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_wis += 1; } break; - case 'М': - case 'м': + case rus::kEmUpper: + case rus::kEm: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_CON); @@ -572,8 +574,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_con += 1; } break; - case 'Н': - case 'н': + case rus::kEnUpper: + case rus::kEn: if (ch->desc->glory->olc_node->free_glory >= 1000 && ch->desc->glory->olc_add_spend_glory < MAX_STATS_BY_GLORY) { parse_add_stat(ch, G_CHA); @@ -581,8 +583,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { ch->desc->glory->olc_add_cha += 1; } break; - case 'В': - case 'в': { + case rus::kVeUpper: + case rus::kVe: { // проверка, чтобы не записывать зря, а только при изменения // и чтобы нельзя было из стата славу вытащить if ((ch->desc->glory->olc_str == ch->GetInbornStr() @@ -642,8 +644,8 @@ bool parse_spend_glory_menu(CharData *ch, const char *arg) { SendMsgToChar("Ваши изменения сохранены.\r\n", ch); return true; } - case 'Х': - case 'х': ch->desc->glory.reset(); + case rus::kHaUpper: + case rus::kHa: ch->desc->glory.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar("Редактирование прервано.\r\n", ch); return true; diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index c842e993a..c6bb006da 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -4,6 +4,7 @@ // Part of Bylins http://www.mud.ru #include "glory_const.h" +#include "utils/russian_keys.h" #include "utils/native_text.h" #include "engine/db/player_index.h" #include "administration/privilege.h" @@ -467,32 +468,32 @@ int olc_real_stat(CharData *ch, int stat) { } bool parse_spend_glory_menu(CharData *ch, char *arg) { - switch (LOWER(*arg)) { - case 'а': olc_del_stat(ch, GLORY_STR); + switch (native_text::first_char_code_lower(arg)) { + case rus::kA: olc_del_stat(ch, GLORY_STR); break; - case 'б': olc_del_stat(ch, GLORY_DEX); + case rus::kBe: olc_del_stat(ch, GLORY_DEX); break; - case 'г': olc_del_stat(ch, GLORY_INT); + case rus::kGe: olc_del_stat(ch, GLORY_INT); break; - case 'д': olc_del_stat(ch, GLORY_WIS); + case rus::kDe: olc_del_stat(ch, GLORY_WIS); break; - case 'е': olc_del_stat(ch, GLORY_CON); + case rus::kIe: olc_del_stat(ch, GLORY_CON); break; - case 'ж': olc_del_stat(ch, GLORY_CHA); + case rus::kZhe: olc_del_stat(ch, GLORY_CHA); break; - case 'з': olc_del_stat(ch, GLORY_HIT); + case rus::kZe: olc_del_stat(ch, GLORY_HIT); break; - case 'и': olc_del_stat(ch, GLORY_SUCCESS); + case rus::kI: olc_del_stat(ch, GLORY_SUCCESS); break; - case 'к': olc_del_stat(ch, GLORY_WILL); + case rus::kKa: olc_del_stat(ch, GLORY_WILL); break; - case 'л': olc_del_stat(ch, GLORY_STABILITY); + case rus::kEl: olc_del_stat(ch, GLORY_STABILITY); break; - case 'м': olc_del_stat(ch, GLORY_REFLEX); + case rus::kEm: olc_del_stat(ch, GLORY_REFLEX); break; - case 'н': olc_del_stat(ch, GLORY_MIND); + case rus::kEn: olc_del_stat(ch, GLORY_MIND); break; - case 'э': olc_del_stat(ch, GLORY_MANAREG); + case rus::kE: olc_del_stat(ch, GLORY_MANAREG); break; case 'x': olc_add_stat(ch, GLORY_BONUSPSYS); break; @@ -502,33 +503,33 @@ bool parse_spend_glory_menu(CharData *ch, char *arg) { break; case 'd': olc_del_stat(ch, GLORY_BONUSMAG); break; - case 'о': olc_add_stat(ch, GLORY_STR); + case rus::kO: olc_add_stat(ch, GLORY_STR); break; - case 'п': olc_add_stat(ch, GLORY_DEX); + case rus::kPe: olc_add_stat(ch, GLORY_DEX); break; - case 'р': olc_add_stat(ch, GLORY_INT); + case rus::kEr: olc_add_stat(ch, GLORY_INT); break; - case 'с': olc_add_stat(ch, GLORY_WIS); + case rus::kEs: olc_add_stat(ch, GLORY_WIS); break; - case 'т': olc_add_stat(ch, GLORY_CON); + case rus::kTe: olc_add_stat(ch, GLORY_CON); break; - case 'у': olc_add_stat(ch, GLORY_CHA); + case rus::kU: olc_add_stat(ch, GLORY_CHA); break; - case 'ф': olc_add_stat(ch, GLORY_HIT); + case rus::kEf: olc_add_stat(ch, GLORY_HIT); break; - case 'х': olc_add_stat(ch, GLORY_SUCCESS); + case rus::kHa: olc_add_stat(ch, GLORY_SUCCESS); break; - case 'ц': olc_add_stat(ch, GLORY_WILL); + case rus::kTse: olc_add_stat(ch, GLORY_WILL); break; - case 'ч': olc_add_stat(ch, GLORY_STABILITY); + case rus::kChe: olc_add_stat(ch, GLORY_STABILITY); break; - case 'ш': olc_add_stat(ch, GLORY_REFLEX); + case rus::kSha: olc_add_stat(ch, GLORY_REFLEX); break; - case 'щ': olc_add_stat(ch, GLORY_MIND); + case rus::kScha: olc_add_stat(ch, GLORY_MIND); break; - case 'ю': olc_add_stat(ch, GLORY_MANAREG); + case rus::kYu: olc_add_stat(ch, GLORY_MANAREG); break; - case 'в': { + case rus::kVe: { // получившиеся статы ch->set_str(olc_real_stat(ch, GLORY_STR)); ch->set_dex(olc_real_stat(ch, GLORY_DEX)); @@ -574,7 +575,7 @@ bool parse_spend_glory_menu(CharData *ch, char *arg) { save(); return 1; } - case 'я': ch->desc->glory_const.reset(); + case rus::kYa: ch->desc->glory_const.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar("Редактирование прервано.\r\n", ch); return 1; diff --git a/src/gameplay/mechanics/named_stuff.cpp b/src/gameplay/mechanics/named_stuff.cpp index cdbb3dd59..81f793ddf 100644 --- a/src/gameplay/mechanics/named_stuff.cpp +++ b/src/gameplay/mechanics/named_stuff.cpp @@ -3,6 +3,8 @@ // Part of Bylins http://www.mud.ru #include "named_stuff.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include #include "administration/privilege.h" #include "gameplay/mechanics/minions.h" @@ -166,11 +168,15 @@ bool parse_nedit_menu(CharData *ch, char *arg) { if (!*buf1) { return false; } - if ((*buf1 < '1' || *buf1 > '8') && (LOWER(*buf1) != 'в' && LOWER(*buf1) != 'х' && LOWER(*buf1) != 'у')) { + if ((*buf1 < '1' || *buf1 > '8') && (native_text::first_char_code_lower(buf1) != rus::kVe + && native_text::first_char_code_lower(buf1) != rus::kHa + && native_text::first_char_code_lower(buf1) != rus::kU)) { SendMsgToChar(ch, "Неверный параметр %c!\r\n", *buf1); return false; } - if (!*buf2 && LOWER(*buf1) != 'в' && LOWER(*buf1) != 'х' && LOWER(*buf1) != 'у') { + if (!*buf2 && native_text::first_char_code_lower(buf1) != rus::kVe + && native_text::first_char_code_lower(buf1) != rus::kHa + && native_text::first_char_code_lower(buf1) != rus::kU) { if (*buf1 < '5' || *buf1 > '8') { SendMsgToChar("Не указан второй параметр!\r\n", ch); } else { @@ -191,7 +197,7 @@ bool parse_nedit_menu(CharData *ch, char *arg) { return false; } - switch (LOWER(*buf1)) { + switch (native_text::first_char_code_lower(buf1)) { case '1': if (a_isdigit(*buf2) && sscanf(buf2, "%d", &num)) { if (GetObjRnum(num) < 0) { @@ -263,7 +269,7 @@ bool parse_nedit_menu(CharData *ch, char *arg) { } break; - case 'у': + case rus::kU: if (!ch->desc->old_vnum) return false; stuff_list.erase(ch->desc->old_vnum); @@ -272,7 +278,7 @@ bool parse_nedit_menu(CharData *ch, char *arg) { save(); return true; - case 'в': tmp_node->uid = ch->desc->named_obj->uid; + case rus::kVe: tmp_node->uid = ch->desc->named_obj->uid; tmp_node->can_clan = ch->desc->named_obj->can_clan; tmp_node->can_alli = ch->desc->named_obj->can_alli; tmp_node->mail = ch->desc->named_obj->mail; @@ -288,7 +294,7 @@ bool parse_nedit_menu(CharData *ch, char *arg) { save(); return true; - case 'х': ch->desc->state = EConState::kPlaying; + case rus::kHa: ch->desc->state = EConState::kPlaying; SendMsgToChar(CommonMsg(ECommonMsg::kOk) + "\r\n", ch); return true; diff --git a/src/gameplay/mechanics/obj_sets_olc.cpp b/src/gameplay/mechanics/obj_sets_olc.cpp index 5a117df6a..a9860f1ec 100644 --- a/src/gameplay/mechanics/obj_sets_olc.cpp +++ b/src/gameplay/mechanics/obj_sets_olc.cpp @@ -2,6 +2,8 @@ // Part of Bylins http://www.mud.ru #include "obj_sets.h" +#include "utils/russian_keys.h" +#include "utils/native_text.h" #include "utils/grammar/declensions.h" #include @@ -525,19 +527,19 @@ void sedit::save_olc(CharData *ch) { void parse_main_exit(CharData *ch, const char *arg) { skip_spaces(&arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': ch->desc->state = EConState::kPlaying; + case rus::kDe: + case rus::kDeUpper: ch->desc->state = EConState::kPlaying; ch->desc->sedit->save_olc(ch); ch->desc->sedit.reset(); SendMsgToChar("Изменения сохранены.\r\n", ch); break; case 'n': case 'N': - case 'н': - case 'Н': ch->desc->sedit.reset(); + case rus::kEn: + case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar("Редактирование отменено.\r\n", ch); break; @@ -551,11 +553,11 @@ void parse_main_exit(CharData *ch, const char *arg) { void parse_set_remove(CharData *ch, const char *arg) { skip_spaces(&arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': { + case rus::kDe: + case rus::kDeUpper: { for (auto i = sets_list.begin(); i != sets_list.end(); ++i) { if ((*i)->uid == ch->desc->sedit->olc_set.uid) { sets_list.erase(i); @@ -571,8 +573,8 @@ void parse_set_remove(CharData *ch, const char *arg) { } case 'n': case 'N': - case 'н': - case 'Н': SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEn: + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); ch->desc->sedit->show_main(ch); break; default: @@ -585,11 +587,11 @@ void parse_set_remove(CharData *ch, const char *arg) { void sedit::parse_obj_remove(CharData *ch, const char *arg) { skip_spaces(&arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': { + case rus::kDe: + case rus::kDeUpper: { auto i = olc_set.obj_list.find(obj_edit); if (i != olc_set.obj_list.end()) { olc_set.obj_list.erase(i); @@ -601,8 +603,8 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { } case 'n': case 'N': - case 'н': - case 'Н': SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEn: + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); show_obj_edit(ch); break; default: SendMsgToChar("Неверный выбор!\r\n", ch); @@ -613,11 +615,11 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { void sedit::parse_activ_remove(CharData *ch, const char *arg) { skip_spaces(&arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': { + case rus::kDe: + case rus::kDeUpper: { auto i = olc_set.activ_list.find(activ_edit); if (i != olc_set.activ_list.end()) { olc_set.activ_list.erase(i); @@ -629,8 +631,8 @@ void sedit::parse_activ_remove(CharData *ch, const char *arg) { } case 'n': case 'N': - case 'н': - case 'Н': SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEn: + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); show_activ_edit(ch); break; default: SendMsgToChar("Неверный выбор!\r\n", ch); @@ -667,11 +669,11 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { return; } if (!a_isdigit(*arg)) { - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'Q': case 'q': - case 'В': - case 'в': + case rus::kVeUpper: + case rus::kVe: if (msg_edit != global_msg) { SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_GLOBAL_MSG_EXIT; @@ -721,11 +723,11 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { void parse_global_msg_exit(CharData *ch, const char *arg) { skip_spaces(&arg); - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'y': case 'Y': - case 'д': - case 'Д': ch->desc->state = EConState::kPlaying; + case rus::kDe: + case rus::kDeUpper: ch->desc->state = EConState::kPlaying; global_msg = ch->desc->sedit->msg_edit; obj_sets::save(); ch->desc->sedit.reset(); @@ -733,8 +735,8 @@ void parse_global_msg_exit(CharData *ch, const char *arg) { break; case 'n': case 'N': - case 'н': - case 'Н': ch->desc->sedit.reset(); + case rus::kEn: + case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar("Редактирование отменено.\r\n", ch); break; @@ -754,11 +756,11 @@ void sedit::parse_main(CharData *ch, const char *arg) { return; } if (!a_isdigit(*arg)) { - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'Q': case 'q': - case 'В': - case 'в': + case rus::kVeUpper: + case rus::kVe: if (new_entry || changed()) { SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_MAIN_EXIT; @@ -1132,11 +1134,11 @@ void sedit::parse_obj_edit(CharData *ch, const char *arg) { return; } if (!a_isdigit(*arg)) { - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'Q': case 'q': - case 'В': - case 'в': show_main(ch); + case rus::kVeUpper: + case rus::kVe: show_main(ch); break; default: SendMsgToChar("Неверный выбор!\r\n", ch); show_obj_edit(ch); @@ -1337,11 +1339,11 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { return; } if (!a_isdigit(*arg)) { - switch (*arg) { + switch (native_text::first_char_code(arg)) { case 'Q': case 'q': - case 'В': - case 'в': show_main(ch); + case rus::kVeUpper: + case rus::kVe: show_main(ch); break; default: SendMsgToChar("Неверный выбор!\r\n", ch); show_activ_edit(ch); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index df1ecc104..bea8cbb26 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -313,6 +313,14 @@ char32_t first_char_code(const char *s) { return cp; } +char32_t first_char_code_lower(const char *s) { + return utf8::to_lower(first_char_code(s)); +} + +char32_t first_char_code_upper(const char *s) { + return utf8::to_upper(first_char_code(s)); +} + namespace { // Whole-buffer case conversion as one tight loop with no calls in the hot path: ASCII and the @@ -466,6 +474,16 @@ char32_t first_char_code(const char *s) { return (s == nullptr || *s == '\0') ? 0 : static_cast(*s); } +char32_t first_char_code_lower(const char *s) { + return (s == nullptr || *s == '\0') + ? 0 : static_cast(a_lcc_table[static_cast(*s)]); +} + +char32_t first_char_code_upper(const char *s) { + return (s == nullptr || *s == '\0') + ? 0 : static_cast(a_ucc_table[static_cast(*s)]); +} + void to_lower(std::string &s) { for (char &c : s) { c = a_lcc_table[static_cast(c)]; diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 24c45cea9..ae945ab07 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -56,6 +56,10 @@ std::size_t char_bytes(const char *s); // Returns 0 on an empty string. char32_t first_char_code(const char *s); +// The same, case-folded -- the replacements for switch (LOWER(*s)) / switch (UPPER(*s)). +char32_t first_char_code_lower(const char *s); +char32_t first_char_code_upper(const char *s); + // Case-insensitive comparison in the native encoding: lexicographic over lowered characters, // the shorter string orders first, returns the signed difference at the first mismatch (0 when // equal). KOI8-R: per byte, via LOWER() -- matches str_cmp/str/str semantics. UTF-8: per code From 118f1e9a6dfd1b0ab9edf758c0e165941ae44486 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 06:28:33 +0200 Subject: [PATCH 018/110] fix(utf8): keep save-file names identical across the flip (C1a, #3681) get_filename() derives a player's save-file name by transliterating the character name byte by byte. Under UTF-8 a Russian letter is two bytes, so the loop would have produced a different name and every existing player's files -- saves, aliases, depots, all of which go through this function -- would have stopped being found. This is the one place in the migration where a mistake costs characters, so it is fixed with the mapping pinned rather than re-derived. * The mapping was extracted from the actual tables (AltToLat + the lowercase table) rather than written from memory, so it reproduces today's output exactly: a->a, zh->1, ya->q, yo->9, and upper/lower collapse together because the original lowercased after transliterating. * native_text::translit_to_filename() implements it in both encodings -- byte-wise under KOI8-R (unchanged behaviour), code-point-wise under UTF-8. * A test pins all 33 letters in both cases plus a whole name, and it is the kind of test that must fail loudly: a silent drift here orphans player files. Verified in both branches: the full alphabet transliterates to the identical string ("abvgde91zijklmnoprstyfhc74683250q") and "Vasya" in Cyrillic gives "vasq" either way. Writing the check also caught a bug the normal build cannot see: the UTF-8 branch used char_bytes_at() before its declaration, so that branch did not compile at all -- it would only have surfaced at the flip. It now takes the length from utf8::decode(). Full suite 652 passed / 0 failed. --- src/engine/db/db.cpp | 11 ++--- src/utils/native_text.cpp | 92 +++++++++++++++++++++++++++++++++++++++ src/utils/native_text.h | 6 +++ tests/native_text.cpp | 91 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 7 deletions(-) diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index 47cbb7665..d42eecf62 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -1,4 +1,5 @@ #include +#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" @@ -3654,13 +3655,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': diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index bea8cbb26..7fdd79f66 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -8,6 +8,7 @@ through them changes nothing until the encoding flip. */ #include "native_text.h" +#include "utils_encoding.h" #ifdef INTERNAL_ENCODING_UTF8 #include "utf8.h" @@ -19,6 +20,10 @@ extern const char a_lcc_table[]; extern const bool a_isalnum_table[]; extern const bool a_isalpha_table[]; extern const bool a_isupper_table[]; + +// Yo is absent from the Latin table and always had a special case in get_filename(). +constexpr unsigned char kYoLowerByte = 0xA3; +constexpr unsigned char kYoUpperByte = 0xB3; #endif #include @@ -379,6 +384,76 @@ void to_upper(std::string &s) { fold_range_utf8(s.data(), s.data() + s.size(), t void to_lower(char *s) { fold_range_utf8(s, s + std::char_traits::length(s), false); } void to_upper(char *s) { fold_range_utf8(s, s + std::char_traits::length(s), true); } + +std::string translit_to_filename(std::string_view name) { + // Code point -> the very same Latin character the KOI8-R byte table yields, so a player's + // file name is identical before and after the flip. Upper and lower case collapse together + // because the byte-wise original lowercased after transliterating. + static const struct { char32_t cp; char latin; } kMap[] = { + {0x0430, 'a'}, {0x0410, 'a'}, + {0x0431, 'b'}, {0x0411, 'b'}, + {0x0432, 'v'}, {0x0412, 'v'}, + {0x0433, 'g'}, {0x0413, 'g'}, + {0x0434, 'd'}, {0x0414, 'd'}, + {0x0435, 'e'}, {0x0415, 'e'}, + {0x0451, '9'}, {0x0401, '9'}, + {0x0436, '1'}, {0x0416, '1'}, + {0x0437, 'z'}, {0x0417, 'z'}, + {0x0438, 'i'}, {0x0418, 'i'}, + {0x0439, 'j'}, {0x0419, 'j'}, + {0x043A, 'k'}, {0x041A, 'k'}, + {0x043B, 'l'}, {0x041B, 'l'}, + {0x043C, 'm'}, {0x041C, 'm'}, + {0x043D, 'n'}, {0x041D, 'n'}, + {0x043E, 'o'}, {0x041E, 'o'}, + {0x043F, 'p'}, {0x041F, 'p'}, + {0x0440, 'r'}, {0x0420, 'r'}, + {0x0441, 's'}, {0x0421, 's'}, + {0x0442, 't'}, {0x0422, 't'}, + {0x0443, 'y'}, {0x0423, 'y'}, + {0x0444, 'f'}, {0x0424, 'f'}, + {0x0445, 'h'}, {0x0425, 'h'}, + {0x0446, 'c'}, {0x0426, 'c'}, + {0x0447, '7'}, {0x0427, '7'}, + {0x0448, '4'}, {0x0428, '4'}, + {0x0449, '6'}, {0x0429, '6'}, + {0x044A, '8'}, {0x042A, '8'}, + {0x044B, '3'}, {0x042B, '3'}, + {0x044C, '2'}, {0x042C, '2'}, + {0x044D, '5'}, {0x042D, '5'}, + {0x044E, '0'}, {0x042E, '0'}, + {0x044F, 'q'}, {0x042F, 'q'}, + }; + std::string out; + out.reserve(name.size()); + std::size_t pos = 0; + while (pos < name.size()) { + char32_t cp = 0; + const std::size_t len = utf8::decode(name, pos, cp); // decode() already reports the length + if (len == 0) { + break; + } + if (cp < 0x80) { + char c = static_cast(cp); + if (c >= 'A' && c <= 'Z') { + c = static_cast(c + 0x20); + } + out.push_back(c); + } else { + char mapped = '_'; + for (const auto &e : kMap) { + if (e.cp == cp) { + mapped = e.latin; + break; + } + } + out.push_back(mapped); + } + pos += len; + } + return out; +} + #else // KOI8-R: 1 byte == 1 character bool native_is_utf8() { @@ -508,6 +583,23 @@ void to_upper(char *s) { } } + +std::string translit_to_filename(std::string_view name) { + // Byte-wise, exactly as get_filename() did before this was extracted: transliterate through + // the Latin table, then lowercase. Yo is not in that table and had its own special case. + std::string out; + out.reserve(name.size()); + for (const char ch : name) { + const unsigned char b = static_cast(ch); + if (b == kYoLowerByte || b == kYoUpperByte) { + out.push_back('9'); + } else { + out.push_back(a_lcc_table[static_cast(codepages::AtoL(ch))]); + } + } + return out; +} + #endif // --------------------------------------------------------------------------------------------- diff --git a/src/utils/native_text.h b/src/utils/native_text.h index ae945ab07..30c15d1c8 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -145,6 +145,12 @@ void to_upper(std::string &s); void to_lower(char *s); void to_upper(char *s); +// Transliterate `name` into the ASCII form used for save-file names: Russian letters become +// Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation +// produced before the migration and MUST NOT drift -- the result is the on-disk file name of a +// player, so a change would orphan every existing character. Pinned by a test in both encodings. +std::string translit_to_filename(std::string_view name); + // Does the single character `ch` occur in `list`? The replacement for strchr() over a literal // list of letters: `list` is walked one whole character at a time, so a multibyte character can // never match on a partial byte sequence. Comparison is exact (case-sensitive), like strchr. diff --git a/tests/native_text.cpp b/tests/native_text.cpp index e0ffe7ff0..c351efa16 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -342,4 +342,95 @@ TEST(NativeText, RussianKeysMatchFirstCharCode) { EXPECT_EQ(native_text::first_char_code(nullptr), 0u); } +TEST(NativeText, TransliterationIsStableAcrossTheFlip) { + // A player's save file is named after the transliterated character name, so this mapping is + // on-disk state: if it ever changes, every existing character stops being found. Each row is + // the letter in both encodings and the single ASCII character it must always produce -- the + // values were taken from what the byte-wise implementation produced before the migration. + struct Row { const char *koi8; const char *utf8; char expected; }; + static const Row kRows[] = { + {"\xC1", "\xD0\xB0", 'a'}, + {"\xE1", "\xD0\x90", 'a'}, + {"\xC2", "\xD0\xB1", 'b'}, + {"\xE2", "\xD0\x91", 'b'}, + {"\xD7", "\xD0\xB2", 'v'}, + {"\xF7", "\xD0\x92", 'v'}, + {"\xC7", "\xD0\xB3", 'g'}, + {"\xE7", "\xD0\x93", 'g'}, + {"\xC4", "\xD0\xB4", 'd'}, + {"\xE4", "\xD0\x94", 'd'}, + {"\xC5", "\xD0\xB5", 'e'}, + {"\xE5", "\xD0\x95", 'e'}, + {"\xA3", "\xD1\x91", '9'}, + {"\xB3", "\xD0\x81", '9'}, + {"\xD6", "\xD0\xB6", '1'}, + {"\xF6", "\xD0\x96", '1'}, + {"\xDA", "\xD0\xB7", 'z'}, + {"\xFA", "\xD0\x97", 'z'}, + {"\xC9", "\xD0\xB8", 'i'}, + {"\xE9", "\xD0\x98", 'i'}, + {"\xCA", "\xD0\xB9", 'j'}, + {"\xEA", "\xD0\x99", 'j'}, + {"\xCB", "\xD0\xBA", 'k'}, + {"\xEB", "\xD0\x9A", 'k'}, + {"\xCC", "\xD0\xBB", 'l'}, + {"\xEC", "\xD0\x9B", 'l'}, + {"\xCD", "\xD0\xBC", 'm'}, + {"\xED", "\xD0\x9C", 'm'}, + {"\xCE", "\xD0\xBD", 'n'}, + {"\xEE", "\xD0\x9D", 'n'}, + {"\xCF", "\xD0\xBE", 'o'}, + {"\xEF", "\xD0\x9E", 'o'}, + {"\xD0", "\xD0\xBF", 'p'}, + {"\xF0", "\xD0\x9F", 'p'}, + {"\xD2", "\xD1\x80", 'r'}, + {"\xF2", "\xD0\xA0", 'r'}, + {"\xD3", "\xD1\x81", 's'}, + {"\xF3", "\xD0\xA1", 's'}, + {"\xD4", "\xD1\x82", 't'}, + {"\xF4", "\xD0\xA2", 't'}, + {"\xD5", "\xD1\x83", 'y'}, + {"\xF5", "\xD0\xA3", 'y'}, + {"\xC6", "\xD1\x84", 'f'}, + {"\xE6", "\xD0\xA4", 'f'}, + {"\xC8", "\xD1\x85", 'h'}, + {"\xE8", "\xD0\xA5", 'h'}, + {"\xC3", "\xD1\x86", 'c'}, + {"\xE3", "\xD0\xA6", 'c'}, + {"\xDE", "\xD1\x87", '7'}, + {"\xFE", "\xD0\xA7", '7'}, + {"\xDB", "\xD1\x88", '4'}, + {"\xFB", "\xD0\xA8", '4'}, + {"\xDD", "\xD1\x89", '6'}, + {"\xFD", "\xD0\xA9", '6'}, + {"\xDF", "\xD1\x8A", '8'}, + {"\xFF", "\xD0\xAA", '8'}, + {"\xD9", "\xD1\x8B", '3'}, + {"\xF9", "\xD0\xAB", '3'}, + {"\xD8", "\xD1\x8C", '2'}, + {"\xF8", "\xD0\xAC", '2'}, + {"\xDC", "\xD1\x8D", '5'}, + {"\xFC", "\xD0\xAD", '5'}, + {"\xC0", "\xD1\x8E", '0'}, + {"\xE0", "\xD0\xAE", '0'}, + {"\xD1", "\xD1\x8F", 'q'}, + {"\xF1", "\xD0\xAF", 'q'}, + }; + for (const auto &r : kRows) { + const char *const input = native_text::native_is_utf8() ? r.utf8 : r.koi8; + EXPECT_EQ(native_text::translit_to_filename(input), std::string(1, r.expected)) + << "transliteration drifted for " << (native_text::native_is_utf8() ? r.utf8 : r.koi8); + } + + // ASCII is lowercased and digits pass through, as before. + EXPECT_EQ(native_text::translit_to_filename("Vasya"), "vasya"); + EXPECT_EQ(native_text::translit_to_filename("Abc123"), "abc123"); + EXPECT_EQ(native_text::translit_to_filename(""), ""); + + // A whole name: "Vasya" in Cyrillic must give the same file name in both encodings. + const char *const name = native_text::native_is_utf8() + ? "\xD0\x92\xD0\xB0\xD1\x81\xD1\x8F" : "\xF7\xC1\xD3\xD1"; + EXPECT_EQ(native_text::translit_to_filename(name), "vasq"); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 7567c29d6e5546e6dff6c01e5d9de9de2e1744be Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 4 Aug 2026 06:55:45 +0200 Subject: [PATCH 019/110] ci(utf8): rehearse the encoding flip on every build (#3681) The UTF-8 branch of native_text had no CI coverage at all -- nothing built it, which is how a branch that did not even compile survived until a hand-written check found it. A unit test cannot close that gap on its own: it only ever exercises the branch its build selected. Adds a Linux job that builds the *coherent* UTF-8 configuration. Sources and runtime flip together, never separately: git already stores src/ and tests/ as UTF-8, so dropping working-tree-encoding and checking out again yields UTF-8 sources, and -Dinternal_encoding=utf8 switches the runtime to character semantics. That is exactly what C2 will do, so the job is a continuous rehearsal of the flip rather than a synthetic configuration. It paid for itself immediately -- two real flip blockers, neither of which any existing test or the audit script could see: * utils.cpp sized the Russian month names as char[12][10]. That fits KOI8-R ("Sentyabrya" is 8 bytes) and overflows in UTF-8 (16). Now an array of pointers, so the cell width stops depending on the encoding at all. * where_format indented continuation lines by prefix.size() -- bytes, not columns -- so the location column drifted apart for names with Cyrillic in them. The test that was supposed to catch this measured alignment in bytes too, so it agreed with the bug; both now measure characters. Verified in both configurations: 652 passed / 0 failed, no warnings. Incidentally confirms the literal-repertoire rule this migration imposes: a comment written with guillemets could not be encoded back to KOI8-R, and git refused it. --- .github/workflows/_linux.yml | 33 +++++++++++++++++++++++++++++++++ src/engine/db/db.cpp | 2 +- src/engine/ui/cmd/do_where.cpp | 9 ++++++--- src/utils/utils.cpp | 8 +++++--- tests/where.format.cpp | 20 +++++++++++++++----- 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.github/workflows/_linux.yml b/.github/workflows/_linux.yml index 8dc3ca035..06db4c4cc 100644 --- a/.github/workflows/_linux.yml +++ b/.github/workflows/_linux.yml @@ -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 diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index d42eecf62..12455155c 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -3620,7 +3620,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); diff --git a/src/engine/ui/cmd/do_where.cpp b/src/engine/ui/cmd/do_where.cpp index 2d95e0d0d..2dd5fc28e 100644 --- a/src/engine/ui/cmd/do_where.cpp +++ b/src/engine/ui/cmd/do_where.cpp @@ -3,6 +3,7 @@ // #include "engine/entities/char_data.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/world_objects.h" #include "gameplay/economics/exchange.h" @@ -195,9 +196,11 @@ std::string where_format::FormatWhere(const std::vector &rows) { for (const auto &row : rows) { const std::string prefix = fmt::format("{:>{}}. {:<5} [{:>7}] {:<25} - ", row.num, num_width, RowKindLabel(row.kind), row.vnum, row.name); - // Отступ строк-продолжений = длине префикса, чтобы разделитель " - " - // встал ровно под разделителем первой строки. - const std::string cont = fmt::format("{:>{}}", " - ", static_cast(prefix.size())); + // Отступ строк-продолжений = ШИРИНЕ префикса в символах, чтобы разделитель " - " + // встал ровно под разделителем первой строки. Именно в символах, а не в байтах: + // prefix.size() под UTF-8 больше числа колонок, и отступ уезжал (issue #3681). + const std::string cont = + fmt::format("{:>{}}", " - ", static_cast(native_text::char_count(prefix))); out += prefix; if (!row.location_lines.empty()) { diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 85c5a6c85..c9c172754 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -312,10 +312,12 @@ void format_text(const utils::AbstractStringWriter::shared_ptr &writer, char *rustime(const struct tm *timeptr) { - static char mon_name[12][10] = + // Массив указателей, а не char[12][10]: ширина ячейки зависела бы от кодировки + // (в UTF-8 русская буква занимает два байта, и названия перестают влезать). + static const char *const mon_name[12] = { - "Января\0", "Февраля\0", "Марта\0", "Апреля\0", "Мая\0", "Июня\0", - "Июля\0", "Августа\0", "Сентября\0", "Октября\0", "Ноября\0", "Декабря\0" + "Января", "Февраля", "Марта", "Апреля", "Мая", "Июня", + "Июля", "Августа", "Сентября", "Октября", "Ноября", "Декабря" }; static char result[100]; diff --git a/tests/where.format.cpp b/tests/where.format.cpp index 99810445b..e0f2b6d3a 100644 --- a/tests/where.format.cpp +++ b/tests/where.format.cpp @@ -6,6 +6,7 @@ // строки == ширине колонки, и индексная арифметика ниже корректна. #include "engine/ui/cmd/do_where.h" +#include "utils/native_text.h" #include @@ -92,14 +93,23 @@ TEST(WhereFormat, LocationColumnAligned) { const auto lines = SplitLines(FormatWhere(SampleRows())); ASSERT_GE(lines.size(), 5u); - const auto ref = lines[1].rfind(" - "); + // Колонка разделителя измеряется в СИМВОЛАХ, а не в байтах: под UTF-8 байтовое смещение + // зависит от того, сколько в имени кириллицы, и "выровнено" перестаёт значить "в одной + // колонке" (issue #3681). + const auto column_of_separator = [](const std::string &line) { + const auto at = line.rfind(" - "); + return at == std::string::npos + ? std::string::npos : native_text::char_count(std::string_view(line).substr(0, at)); + }; + + const auto ref = column_of_separator(lines[1]); ASSERT_NE(ref, std::string::npos); - EXPECT_EQ(lines[2].rfind(" - "), ref) << "предмет с именем ровно 25 симв."; - EXPECT_EQ(lines[3].rfind(" - "), ref) << "строка-продолжение контейнера"; - EXPECT_EQ(lines[4].rfind(" - "), ref) << "короткое имя"; + EXPECT_EQ(column_of_separator(lines[2]), ref) << "предмет с именем ровно 25 симв."; + EXPECT_EQ(column_of_separator(lines[3]), ref) << "строка-продолжение контейнера"; + EXPECT_EQ(column_of_separator(lines[4]), ref) << "короткое имя"; // Имя моба (29 симв.) длиннее поля в 25 -> разделитель уезжает вправо на 4. - EXPECT_EQ(lines[0].rfind(" - "), ref + 4); + EXPECT_EQ(column_of_separator(lines[0]), ref + 4); } // Предмет в контейнере даёт ровно две строки: первая оканчивается на From bd49034756138aa21bf6817ccf3d71c7879cf710 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 01:14:17 +0200 Subject: [PATCH 020/110] feat(utf8): funnel world text through the encoding boundary (#3681) Adds native_text::from_koi8() -- identity under KOI8-R, a transcode under UTF-8 -- and routes the YAML loader's GetText() through it. That is the single point all world text passes, so the loader stops being encoding-specific and the world files can stay KOI8-R on disk (track B remains deferred). No-op on the current build; 652 passed / 0 failed. Booting the flip build on the production world surfaced two things worth recording now rather than on the day of the flip: * The server aborted during boot in HelpSystem::SetsHelp -> libfort, on data from an XML config. So converting the world loader is not sufficient: every KOI8-R source (cfg/**, help text, boards, mail, saves) needs the same boundary before C2. * libfort's utf8_table does not degrade on malformed input -- the visible-width calculation underflows and the allocation check aborts the process. An unconverted string reaching any table is therefore a crash, not a cosmetic defect, which raises C3 from "convert what players see" to "convert every source". Neither is reachable from unit tests; both came out of running the real binary against the real world, which is what the rehearsal job is for. --- src/engine/db/yaml_world_data_source.cpp | 5 +++-- src/utils/native_text.cpp | 15 +++++++++++++++ src/utils/native_text.h | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/engine/db/yaml_world_data_source.cpp b/src/engine/db/yaml_world_data_source.cpp index c6f8d97cf..72b5a4508 100644 --- a/src/engine/db/yaml_world_data_source.cpp +++ b/src/engine/db/yaml_world_data_source.cpp @@ -731,8 +731,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(); + // Файлы мира лежат на диске в KOI8-R; переводим в нативную кодировку движка + // (под KOI8-R это тождество, под UTF-8 - перекодировка). Issue #3681. + std::string text = native_text::from_koi8(node[key].as()); // Convert line endings if configured for DOS format if (m_convert_lf_to_crlf) { diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 7fdd79f66..9ebddeb3b 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -27,6 +27,7 @@ constexpr unsigned char kYoUpperByte = 0xB3; #endif #include +#include namespace native_text { @@ -385,6 +386,16 @@ void to_lower(char *s) { fold_range_utf8(s, s + std::char_traits::length(s void to_upper(char *s) { fold_range_utf8(s, s + std::char_traits::length(s), true); } +std::string from_koi8(const std::string &text) { + if (text.empty()) { + return text; + } + // koi_to_utf8() can grow the text; utils_encoding sizes its own buffers at 6x, so match that. + std::vector out(text.size() * 6 + 1, '\0'); + codepages::koi_to_utf8(const_cast(text.c_str()), out.data()); + return std::string(out.data()); +} + std::string translit_to_filename(std::string_view name) { // Code point -> the very same Latin character the KOI8-R byte table yields, so a player's // file name is identical before and after the flip. Upper and lower case collapse together @@ -584,6 +595,10 @@ void to_upper(char *s) { } +std::string from_koi8(const std::string &text) { + return text; // the native encoding already is KOI8-R +} + std::string translit_to_filename(std::string_view name) { // Byte-wise, exactly as get_filename() did before this was extracted: transliterate through // the Latin table, then lowercase. Yo is not in that table and had its own special case. diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 30c15d1c8..f7715296e 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -145,6 +145,11 @@ void to_upper(std::string &s); void to_lower(char *s); void to_upper(char *s); +// Bring text stored on disk in KOI8-R (world files, configs, saves) into the engine's native +// encoding. Identity under KOI8-R, a transcode under UTF-8. Having it here keeps the loaders +// free of #ifdefs and gives one place to revisit when the data files themselves move. +std::string from_koi8(const std::string &text); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a From 2ea4dc0f8c03d0f818fa33c3a965514f655fcaa1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 06:10:00 +0200 Subject: [PATCH 021/110] feat(utf8): put the encoding boundary at XML load, not at each field (C3a, #3681) Wiring AttrStr() alone was not enough -- class names, and anything else that reads an element value rather than an attribute, bypassed it. Converting per field would mean finding every reader; converting per document is one place and cannot be forgotten, so DataNode now reads the file itself, passes it through native_text::from_koi8() and hands the buffer to pugixml. Identity under KOI8-R. Also: * from_koi8() gains an ASCII fast path. It runs per field during boot and most fields (keys, aliases, numbers) are pure ASCII, where the two encodings agree byte for byte. * A unit test for from_koi8 in both directions -- it had none, which was an omission: the wrapper is a no-op today but perfectly testable, including under the flip build. * help.cpp built a class list and then wrote '\0' over the trailing newline. That leaves a NUL *inside* a std::string without changing its length; the byte-based table tolerated it, libfort's utf8_table walks off the end and aborts. Now pop_back(). Milestone: with these, the flip build boots the production world without SYSERR. Text still arrives mangled at the client, for two reasons already on the C3 list and not yet done -- the descriptor still runs koi_to_utf8() for UTF-8 clients (a second conversion of text that is already UTF-8), and plain-text data files (greetings, help) have no boundary yet. Both builds green: KOI8-R 653 passed / 0 failed and the production world still boots. --- src/engine/db/help.cpp | 8 ++++++-- src/utils/native_text.cpp | 16 +++++++++++++--- src/utils/parser_wrapper.cpp | 15 ++++++++++++++- src/utils/utils_parse.cpp | 4 +++- tests/native_text.cpp | 22 ++++++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/engine/db/help.cpp b/src/engine/db/help.cpp index d19348f6b..afd1b82c7 100644 --- a/src/engine/db/help.cpp +++ b/src/engine/db/help.cpp @@ -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); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 9ebddeb3b..bc3d89b5f 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -387,11 +387,21 @@ void to_upper(char *s) { fold_range_utf8(s, s + std::char_traits::length(s std::string from_koi8(const std::string &text) { - if (text.empty()) { + // This runs per attribute/field while the world and the configs load, and the overwhelming + // majority of those are pure ASCII (keys, aliases, numbers), which KOI8-R and UTF-8 spell + // identically. Detect that first and hand the text back untouched instead of transcoding. + bool has_high_byte = false; + for (const char c : text) { + if (static_cast(c) >= 0x80) { + has_high_byte = true; + break; + } + } + if (!has_high_byte) { return text; } - // koi_to_utf8() can grow the text; utils_encoding sizes its own buffers at 6x, so match that. - std::vector out(text.size() * 6 + 1, '\0'); + // Every KOI8-R character lives below U+FFFF, so three bytes per input byte is a hard bound. + std::vector out(text.size() * 3 + 1, '\0'); codepages::koi_to_utf8(const_cast(text.c_str()), out.data()); return std::string(out.data()); } diff --git a/src/utils/parser_wrapper.cpp b/src/utils/parser_wrapper.cpp index 972e85d16..d99aedd6e 100644 --- a/src/utils/parser_wrapper.cpp +++ b/src/utils/parser_wrapper.cpp @@ -1,4 +1,6 @@ #include "parser_wrapper.h" +#include +#include "utils/native_text.h" #include "utils/logger.h" @@ -20,7 +22,18 @@ DataNode::DataNode() : DataNode::DataNode(const std::filesystem::path &file_name) : DataNode() { - if (auto result = impl_->xml_doc->load_file(file_name.c_str()); !result) { + // Файлы конфигов лежат на диске в KOI8-R. Переводим содержимое в нативную кодировку + // движка ОДИН раз на документ, а не на каждое поле: тогда всё, что читается через + // DataNode, приходит уже в нужной кодировке (issue #3681). Под KOI8-R это тождество. + std::string raw; + { + std::ifstream in(file_name, std::ios::binary); + if (in) { + raw.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + } + const std::string converted = native_text::from_koi8(raw); + if (auto result = impl_->xml_doc->load_buffer(converted.data(), converted.size()); !result) { std::ostringstream buffer; buffer << "..." << result.description() << "\r\n" << " (file: " << file_name << ")" << "\r\n"; err_log("%s", buffer.str().c_str()); diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 622ddd0fd..2fff35b6e 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -352,7 +352,9 @@ int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { std::string AttrStr(const parser_wrapper::DataNode &node, const char *key, const char *def) { const char *v = node.GetValue(key); - return (v && *v) ? std::string(v) : std::string(def); + // XML-конфиги лежат на диске в KOI8-R; переводим в нативную кодировку движка + // (под KOI8-R это тождество, под UTF-8 - перекодировка). Issue #3681. + return native_text::from_koi8((v && *v) ? std::string(v) : std::string(def)); } } // namespace parse diff --git a/tests/native_text.cpp b/tests/native_text.cpp index c351efa16..8a574cb4a 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -5,6 +5,7 @@ // flag the library was built with), so this one test file is correct under either build. #include "utils/native_text.h" +#include "utils/utf8.h" #include "utils/russian_keys.h" #include @@ -433,4 +434,25 @@ TEST(NativeText, TransliterationIsStableAcrossTheFlip) { EXPECT_EQ(native_text::translit_to_filename(name), "vasq"); } +TEST(NativeText, FromKoi8BringsDiskTextIntoTheNativeEncoding) { + // Data files (world, configs, help, boards, saves) are stored in KOI8-R. from_koi8() is the + // boundary that brings them into whatever the engine runs on: a no-op today, a transcode + // after the flip. Both directions are asserted here so the wiring can be trusted before the + // sources that need it are converted. + const char *const koi8_privet = "\xF0\xD2\xC9\xD7\xC5\xD4"; // "Privet", KOI8-R + const char *const utf8_privet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; // the same, UTF-8 + + EXPECT_EQ(native_text::from_koi8(""), ""); + EXPECT_EQ(native_text::from_koi8("plain ascii 123"), "plain ascii 123"); // ASCII never changes + + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::from_koi8(koi8_privet), utf8_privet); + // The result must be well-formed UTF-8 -- libfort aborts the process on anything else. + EXPECT_TRUE(utf8::is_valid(native_text::from_koi8(koi8_privet))); + EXPECT_EQ(native_text::char_count(native_text::from_koi8(koi8_privet)), 6u); + } else { + EXPECT_EQ(native_text::from_koi8(koi8_privet), koi8_privet); // identity under KOI8-R + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From bb4558574e7edc7ff8548c3a3e02853149e6e55f Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 06:58:20 +0200 Subject: [PATCH 022/110] feat(utf8): make the client encoding boundary work in both runtimes (C3, #3681) The descriptor converted KOI8-R to the client's code page on the way out and back on the way in. Under a UTF-8 runtime both directions were wrong: text already in UTF-8 was run through koi_to_utf8 a second time (which is exactly what the first flip build showed on screen), and a UTF-8 client's input was converted down to KOI8-R. Output: legacy code pages are KOI8-R -> target byte tables, so the text is brought to KOI8-R first (a no-op under KOI8-R) and the tables are left untouched; a UTF-8 client now gets the text as is when the runtime is already UTF-8. Input: the per-byte tables still produce KOI8-R, so the assembled line is lifted to the native encoding afterwards; a UTF-8 client's line is left alone when the runtime is UTF-8 instead of being converted down. Also fixes the legacy zMUD 'z' substitution: at that point the buffer still holds KOI8-R (that is what the tables above produce), so the letter has to be KOI8-R too -- it is now taken through to_koi8() once instead of being pasted in the source encoding. Adds native_text::to_koi8(), the inverse boundary, with a round-trip test. KOI8-R build: 654 passed / 0 failed. --- src/engine/core/iosystem.cpp | 39 ++++++++++++++++++-------- src/engine/network/descriptor_data.cpp | 21 +++++++++++++- src/utils/native_text.cpp | 24 ++++++++++++++++ src/utils/native_text.h | 6 ++++ tests/native_text.cpp | 18 ++++++++++++ 5 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index e99880526..84f096c01 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -7,6 +7,7 @@ */ #include "engine/core/iosystem.h" +#include "utils/native_text.h" #include #include #include "gameplay/core/experience.h" @@ -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; @@ -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)) { diff --git a/src/engine/network/descriptor_data.cpp b/src/engine/network/descriptor_data.cpp index ab69b11b6..bb385928f 100644 --- a/src/engine/network/descriptor_data.cpp +++ b/src/engine/network/descriptor_data.cpp @@ -6,6 +6,9 @@ */ #include "descriptor_data.h" +#include +#include +#include "utils/native_text.h" #include "utils/utils_encoding.h" #include "engine/entities/char_player.h" @@ -164,6 +167,16 @@ void DescriptorData::msdp_report_changed_vars() { } void DescriptorData::string_to_client_encoding(const char *in_str, char *out_str) const { + // Легаси-кодировки клиентов заданы таблицами "байт KOI8-R -> байт целевой кодировки", + // поэтому перед ними текст надо привести к KOI8-R. Под KOI8-R-рантаймом это тождество, + // под UTF-8 - настоящая перекодировка (issue #3681). Для UTF-8-клиента ничего приводить + // не нужно: см. case kCodePageUTF8 ниже. + std::string koi8_text; + if (keytable != kCodePageUTF8) { + koi8_text = native_text::to_koi8(in_str); + in_str = koi8_text.c_str(); + } + switch (keytable) { case kCodePageAlt: for (; *in_str; *out_str = codepages::KtoA(*in_str), in_str++, out_str++); @@ -204,7 +217,13 @@ void DescriptorData::string_to_client_encoding(const char *in_str, char *out_str // Anton Gorev (2016-04-25): we have to be careful. String in UTF-8 encoding may // contain character with code 0xff which telnet interprets as IAC. // II: FE and FF were never defined for any purpose in UTF-8, we are safe - codepages::koi_to_utf8(const_cast(in_str), out_str); + if (native_text::native_is_utf8()) { + // Рантайм уже в UTF-8 - отдаём как есть. Повторная перекодировка испортила бы + // текст (именно так и выглядела первая флип-сборка). + strcpy(out_str, in_str); + } else { + codepages::koi_to_utf8(const_cast(in_str), out_str); + } break; default: diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index bc3d89b5f..6ac2a559e 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -406,6 +406,26 @@ std::string from_koi8(const std::string &text) { return std::string(out.data()); } +std::string to_koi8(const std::string &text) { + if (text.empty()) { + return text; + } + bool has_high_byte = false; + for (const char c : text) { + if (static_cast(c) >= 0x80) { + has_high_byte = true; + break; + } + } + if (!has_high_byte) { + return text; // pure ASCII is spelled identically in both encodings + } + // KOI8-R is never longer than the UTF-8 it came from, so the input size is a safe bound. + std::vector out(text.size() + 1, '\0'); + codepages::utf8_to_koi(const_cast(text.c_str()), out.data()); + return std::string(out.data()); +} + std::string translit_to_filename(std::string_view name) { // Code point -> the very same Latin character the KOI8-R byte table yields, so a player's // file name is identical before and after the flip. Upper and lower case collapse together @@ -609,6 +629,10 @@ std::string from_koi8(const std::string &text) { return text; // the native encoding already is KOI8-R } +std::string to_koi8(const std::string &text) { + return text; // the native encoding already is KOI8-R +} + std::string translit_to_filename(std::string_view name) { // Byte-wise, exactly as get_filename() did before this was extracted: transliterate through // the Latin table, then lowercase. Yo is not in that table and had its own special case. diff --git a/src/utils/native_text.h b/src/utils/native_text.h index f7715296e..f18d22afe 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -150,6 +150,12 @@ void to_upper(char *s); // free of #ifdefs and gives one place to revisit when the data files themselves move. std::string from_koi8(const std::string &text); +// The inverse: take native text down to KOI8-R. Needed wherever something downstream is defined +// in terms of KOI8-R bytes -- the legacy client code pages are KOI8-R -> target byte tables, and +// the on-disk formats are KOI8-R. Identity under KOI8-R. Characters absent from KOI8-R are +// replaced by the converter (it substitutes '+'), which is unavoidable for a narrower encoding. +std::string to_koi8(const std::string &text); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 8a574cb4a..f0574070f 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -455,4 +455,22 @@ TEST(NativeText, FromKoi8BringsDiskTextIntoTheNativeEncoding) { } } +TEST(NativeText, ToKoi8IsTheInverseBoundary) { + // The counterpart of from_koi8: used where something downstream speaks KOI8-R -- the legacy + // client code pages (their tables are indexed by KOI8-R bytes) and the on-disk formats. + const char *const koi8_privet = "\xF0\xD2\xC9\xD7\xC5\xD4"; + const char *const utf8_privet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; + + EXPECT_EQ(native_text::to_koi8(""), ""); + EXPECT_EQ(native_text::to_koi8("plain ascii 123"), "plain ascii 123"); + + if (native_text::native_is_utf8()) { + EXPECT_EQ(native_text::to_koi8(utf8_privet), koi8_privet); + // Round trip through the boundary must return the original text. + EXPECT_EQ(native_text::from_koi8(native_text::to_koi8(utf8_privet)), utf8_privet); + } else { + EXPECT_EQ(native_text::to_koi8(koi8_privet), koi8_privet); + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 129d918b6f554995032ec6784408c9da2b3f321a Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 07:08:20 +0200 Subject: [PATCH 023/110] fix(utf8): spell the code-page tables as byte escapes, not string literals (#3681) The six conversion tables (AltToKoi, KoiToAlt, WinToKoi, KoiToWin, KoiToWin2, AltToLat) were written as string literals made of high-byte characters, so their contents depended on the encoding of the source file. With UTF-8 sources every one of those characters becomes two or three bytes and the tables silently grow past their 128 entries: AltToKoi 296, KoiToAlt 298, WinToKoi 214, KoiToWin 218, KoiToWin2 217, AltToLat 248 Indexing them then reads whatever follows, so every legacy client (Alt/CP866, Windows-1251, zMUD) would have received garbage after the flip -- and AltToLat also feeds save-file name transliteration. Nothing would have failed to build or crashed; the output would just have been wrong. The tables are now \xNN escapes, which are plain ASCII and mean the same in any source encoding. Byte-for-byte identical to what the KOI8-R build produced before -- verified against the previous contents, all six tables, 128 bytes each. Found by connecting an Alt client to the flip build: its text came out mangled while the UTF-8 and KOI8-R clients were already correct. The tables looked innocent because they are untouched legacy code -- it is the source encoding that changes underneath them. KOI8-R build: 654 passed / 0 failed. --- src/utils/utils_encoding.cpp | 59 ++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/utils/utils_encoding.cpp b/src/utils/utils_encoding.cpp index 5d25112cb..341bc5138 100644 --- a/src/utils/utils_encoding.cpp +++ b/src/utils/utils_encoding.cpp @@ -16,23 +16,70 @@ namespace codepages { +// Tables of raw bytes, spelled as \xNN escapes on purpose. They used to be written as string +// literals of high-byte characters, which made their contents depend on the encoding of this +// file: with UTF-8 sources every Cyrillic/pseudographic character became two or three bytes and +// the tables silently grew past 128 entries, corrupting every legacy client's output. Escapes +// are plain ASCII and mean the same thing in any source encoding (issue #3681). char AltToKoi[] = { - "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп░▒▓│┤╡+++╣║╗╝+╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨++╙╘╒++╪┘┌█▄▌▐▀рстуфхцчшщъыьэюяЁё╫╜╢╓╤╕╥╖·√??■ " + "\xE1\xE2\xF7\xE7\xE4\xE5\xF6\xFA\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0" + "\xF2\xF3\xF4\xF5\xE6\xE8\xE3\xFE\xFB\xFD\xFF\xF9\xF8\xFC\xE0\xF1" + "\xC1\xC2\xD7\xC7\xC4\xC5\xD6\xDA\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0" + "\x90\x91\x92\x81\x87\xB2\x2B\x2B\x2B\xB5\xA1\xA8\xAE\x2B\xAC\x83" + "\x84\x89\x88\x86\x80\x8A\xAF\xB0\xAB\xA5\xBB\xB8\xB1\xA0\xBE\xB9" + "\xBA\x2B\x2B\xAA\xA9\xA2\x2B\x2B\xBC\x85\x82\x8D\x8C\x8E\x8F\x8B" + "\xD2\xD3\xD4\xD5\xC6\xC8\xC3\xDE\xDB\xDD\xDF\xD9\xD8\xDC\xC0\xD1" + "\xB3\xA3\xBD\xAD\xB4\xA4\xB6\xA6\xB7\xA7\x9E\x96\x3F\x3F\x94\x9A" }; char KoiToAlt[] = { - "дЁз©юыц╢баеъэшщч╟╠╡+Ч+Ш+++Ъ+++З+м╨уЯУиВЫ╩тсх╬С╪фгл╣ПТ╧ЖЬкопйьРн+Н═║Ф╓╔ДёЕ╗╘╙╚╛╜╝╞ОЮАБЦ╕╒ЛК╖ХМИГЙ·─│√└┘■┐∙┬┴┼▀▄█▌▐÷░▒▓⌠├┌°⌡┤≤²≥≈ " + "\xC4\xB3\xDA\xBF\xC0\xD9\xC3\xB4\xC2\xC1\xC5\xDF\xDC\xDB\xDD\xDE" + "\xB0\xB1\xB2\x2B\xFE\x2B\xFB\x2B\x2B\x2B\xFF\x2B\x2B\x2B\xFA\x2B" + "\xCD\xBA\xD5\xF1\xF5\xC9\xF7\xF9\xBB\xD4\xD3\xC8\xBE\xF3\xBC\xC6" + "\xC7\xCC\xB5\xF0\xF4\xB9\xF6\xF8\xCB\xCF\xD0\xCA\xD8\xF2\xCE\x2B" + "\xEE\xA0\xA1\xE6\xA4\xA5\xE4\xA3\xE5\xA8\xA9\xAA\xAB\xAC\xAD\xAE" + "\xAF\xEF\xE0\xE1\xE2\xE3\xA6\xA2\xEC\xEB\xA7\xE8\xED\xE9\xE7\xEA" + "\x9E\x80\x81\x96\x84\x85\x94\x83\x95\x88\x89\x8A\x8B\x8C\x8D\x8E" + "\x8F\x9F\x90\x91\x92\x93\x86\x82\x9C\x9B\x87\x98\x9D\x99\x97\x9A" }; char WinToKoi[] = { - "++++++++++++++++++++++++++++++++ ++++╫++Ё©╢++++╥°+╤╕╜++·ё+╓++++╖АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B" + "\x9A\x2B\x2B\x2B\x2B\xBD\x2B\x2B\xB3\xBF\xB4\x2B\x2B\x2B\x2B\xB7" + "\x9C\x2B\xB6\xA6\xAD\x2B\x2B\x9E\xA3\x2B\xA4\x2B\x2B\x2B\x2B\xA7" + "\xE1\xE2\xF7\xE7\xE4\xE5\xF6\xFA\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0" + "\xF2\xF3\xF4\xF5\xE6\xE8\xE3\xFE\xFB\xFD\xFF\xF9\xF8\xFC\xE0\xF1" + "\xC1\xC2\xD7\xC7\xC4\xC5\xD6\xDA\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0" + "\xD2\xD3\xD4\xD5\xC6\xC8\xC3\xDE\xDB\xDD\xDF\xD9\xD8\xDC\xC0\xD1" }; char KoiToWin[] = { - "++++++++++++++++++++++++++═+╟+╥++++╦╨+Ё©+++++╢+++++╗╙+╡╞+++++╔+╘ЧЮАЖДЕТЦУХИЙКЛМНОЪПЯРСФБЭШГЬЩЫВЗчюаждетцухийклмноъпярсфбэшгьщывз" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\xA0\x2B\xB0\x2B\xB7\x2B" + "\x2B\x2B\x2B\xB8\xBA\x2B\xB3\xBF\x2B\x2B\x2B\x2B\x2B\xB4\x2B\x2B" + "\x2B\x2B\x2B\xA8\xAA\x2B\xB2\xAF\x2B\x2B\x2B\x2B\x2B\xA5\x2B\xA9" + "\xFE\xE0\xE1\xF6\xE4\xE5\xF4\xE3\xF5\xE8\xE9\xEA\xEB\xEC\xED\xEE" + "\xEF\xFF\xF0\xF1\xF2\xF3\xE6\xE2\xFC\xFB\xE7\xF8\xFD\xF9\xF7\xFA" + "\xDE\xC0\xC1\xD6\xC4\xC5\xD4\xC3\xD5\xC8\xC9\xCA\xCB\xCC\xCD\xCE" + "\xCF\xDF\xD0\xD1\xD2\xD3\xC6\xC2\xDC\xDB\xC7\xD8\xDD\xD9\xD7\xDA" }; char KoiToWin2[] = { - "++++++++++++++++++++++++++═+╟+╥++++╦╨+Ё©+++++╢+++++╗╙+╡╞+++++╔+╘ЧЮАЖДЕТЦУХИЙКЛМНОzПЯРСФБЭШГЬЩЫВЗчюаждетцухийклмноъпярсфбэшгьщывз" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B" + "\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\x2B\xA0\x2B\xB0\x2B\xB7\x2B" + "\x2B\x2B\x2B\xB8\xBA\x2B\xB3\xBF\x2B\x2B\x2B\x2B\x2B\xB4\x2B\x2B" + "\x2B\x2B\x2B\xA8\xAA\x2B\xB2\xAF\x2B\x2B\x2B\x2B\x2B\xA5\x2B\xA9" + "\xFE\xE0\xE1\xF6\xE4\xE5\xF4\xE3\xF5\xE8\xE9\xEA\xEB\xEC\xED\xEE" + "\xEF\x7A\xF0\xF1\xF2\xF3\xE6\xE2\xFC\xFB\xE7\xF8\xFD\xF9\xF7\xFA" + "\xDE\xC0\xC1\xD6\xC4\xC5\xD4\xC3\xD5\xC8\xC9\xCA\xCB\xCC\xCD\xCE" + "\xCF\xDF\xD0\xD1\xD2\xD3\xC6\xC2\xDC\xDB\xC7\xD8\xDD\xD9\xD7\xDA" }; char AltToLat[] = { - "─│┌┐└┘├┤┬┴┼▀▄█▌▐░▒▓⌠■∙√≈≤≥ ⌡°²·÷═║╒ё╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡Ё╢╣╤╥╦╧╨╩╪╫╬©0abcdefghijklmnopqrstY1v23z456780ABCDEFGHIJKLMNOPQRSTY1V23Z45678" + "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F" + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" + "\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF" + "\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF" + "\x30\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" + "\x70\x71\x72\x73\x74\x59\x31\x76\x32\x33\x7A\x34\x35\x36\x37\x38" + "\x30\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" + "\x50\x51\x52\x53\x54\x59\x31\x56\x32\x33\x5A\x34\x35\x36\x37\x38" }; void koi_to_win(char *str, int size) { From 254e0ed611bf5a2e032b3b8ac63e4dce12ae2314 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 14:37:43 +0200 Subject: [PATCH 024/110] feat(utf8): take every data source through the encoding boundary (C3, #3681) Completes the data side of C3 so the engine can actually run on UTF-8: * The eight loaders that called pugixml's load_file() directly (craft, named_stuff, shop_ext, sets_drop, mail, mob_stat, glory_const, db) now read through native_text::read_data_file() and parse a buffer, like the DataNode path already did. * Files read via FBFILE -- player saves above all -- are converted when the file is opened rather than line by line. Line-by-line would have been the obvious place, but fbgetline() has no idea how large the caller's buffer is, and Cyrillic grows when it is transcoded, so it could overrun it. At open time the size is ours to control. * The original bytes are kept in FBFILE::raw for the player-file CRC, which is computed over the file as it is stored; checksumming the converted text would fail every load. * native_text::from_disk_line() implements the read-both rule: text that is already well-formed UTF-8 is taken as native, anything else is transcoded. Cyrillic in KOI8-R is essentially never valid UTF-8, so validity is a dependable discriminator and old and new save files can coexist without a version field. Dockerfile gains INTERNAL_ENCODING (koi8r default). For utf8 it transcodes the C++ tree in place with iconv rather than re-checking-out: the build context may be a worktree, where .git is a link file, or an exported archive with no git at all. KOI8-R build: 654 passed / 0 failed. --- Dockerfile | 16 +++++++++++++++- src/engine/db/db.cpp | 5 ++++- src/engine/entities/char_player.cpp | 2 +- src/gameplay/communication/mail.cpp | 6 +++++- src/gameplay/crafting/craft.cpp | 6 +++++- src/gameplay/economics/shop_ext.cpp | 6 +++++- src/gameplay/mechanics/glory_const.cpp | 5 ++++- src/gameplay/mechanics/named_stuff.cpp | 5 ++++- src/gameplay/mechanics/sets_drop.cpp | 6 +++++- src/gameplay/statistics/mob_stat.cpp | 6 +++++- src/utils/diskio.cpp | 25 +++++++++++++++++++++++++ src/utils/diskio.h | 5 +++++ src/utils/native_text.cpp | 24 ++++++++++++++++++++++++ src/utils/native_text.h | 14 ++++++++++++++ 14 files changed, 121 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index c3b23fb80..c8facdf7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,9 @@ 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 RUN apk add --no-cache \ build-base make meson ninja git cmake samurai \ @@ -61,6 +64,16 @@ 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 + 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); \ @@ -69,7 +82,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: рантайм ───────────────────────── diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index 12455155c..63e1c1231 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -1034,7 +1034,10 @@ void ZoneTrafficSave() { } void zone_traffic_load() { pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(MUD::StateManager().Path(state::EStateFile::kZoneTraffic).c_str()); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_db = native_text::read_data_file(MUD::StateManager().Path(state::EStateFile::kZoneTraffic)); + 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); diff --git a/src/engine/entities/char_player.cpp b/src/engine/entities/char_player.cpp index fd130e2cd..4bc9462ae 100644 --- a/src/engine/entities/char_player.cpp +++ b/src/engine/entities/char_player.cpp @@ -1859,7 +1859,7 @@ int Player::load_char_ascii(const char *name, const int load_flags) { // иначе в таблице crc будут пустые имена, т.к. сама плеер-таблица еще не сформирована // и в любом случае при ребуте это все пересчитывать не нужно if (!(load_flags & ELoadCharFlags::kNoCrcCheck)) { - FileCRC::verify_from_content(this->get_uid(), FileCRC::kPlayer, fl->buf, fl->size); + FileCRC::verify_from_content(this->get_uid(), FileCRC::kPlayer, fl->raw, fl->raw_size); } fbclose(fl); diff --git a/src/gameplay/communication/mail.cpp b/src/gameplay/communication/mail.cpp index 707e03d6b..5090724bd 100644 --- a/src/gameplay/communication/mail.cpp +++ b/src/gameplay/communication/mail.cpp @@ -9,6 +9,7 @@ ************************************************************************ */ #include "mail.h" +#include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/global_objects.h" #include "gameplay/economics/currencies.h" @@ -594,7 +595,10 @@ void save() { void load() { pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(MAIL_XML_FILE); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_mail = native_text::read_data_file(MAIL_XML_FILE); + pugi::xml_parse_result result = doc.load_buffer(xml_mail.data(), xml_mail.size()); if (!result) { snprintf(buf, kMaxStringLength, "...%s", result.description()); mudlog(buf, CMP, kLvlImmortal, SYSLOG, true); diff --git a/src/gameplay/crafting/craft.cpp b/src/gameplay/crafting/craft.cpp index 80baeccf4..4f68ecef7 100644 --- a/src/gameplay/crafting/craft.cpp +++ b/src/gameplay/crafting/craft.cpp @@ -5,6 +5,7 @@ */ #include "craft.h" +#include "utils/native_text.h" #include "gameplay/mechanics/magic_item.h" #include "engine/db/obj_prototypes.h" @@ -1074,7 +1075,10 @@ bool CCraftModel::load() { Logger::CPrefix prefix(logger, BODY_PREFIX); pugi::xml_document doc; - const auto result = doc.load_file(FILE_NAME.c_str()); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_craft = native_text::read_data_file(FILE_NAME.c_str()); + const auto result = doc.load_buffer(xml_craft.data(), xml_craft.size()); if (!result) { logger("Craft load error: '%s' at offset %zu\n", diff --git a/src/gameplay/economics/shop_ext.cpp b/src/gameplay/economics/shop_ext.cpp index bf12430e0..d6f1a9221 100644 --- a/src/gameplay/economics/shop_ext.cpp +++ b/src/gameplay/economics/shop_ext.cpp @@ -3,6 +3,7 @@ // Part of Bylins http://www.mud.ru #include "shop_ext.h" +#include "utils/native_text.h" #include "engine/olc/vedun/enum_registry.h" // vedun::RegisterEditorEnums (refresh ShopItemSetId) #include #include "gameplay/economics/currencies.h" @@ -148,7 +149,10 @@ void log_shop_load() { void load_item_desc() { pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(LIB_CLANS"item_desc.xml"); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_shop_ext = native_text::read_data_file(LIB_CLANS"item_desc.xml"); + pugi::xml_parse_result result = doc.load_buffer(xml_shop_ext.data(), xml_shop_ext.size()); if (!result) { snprintf(buf, kMaxStringLength, "...%s", result.description()); mudlog(buf, CMP, kLvlImmortal, SYSLOG, true); diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index c6bb006da..d63f9d5b2 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -900,7 +900,10 @@ void save() { void load() { int ver = 0; pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(LIB_USERDATA"glory_const.xml"); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_glory_const = native_text::read_data_file(LIB_USERDATA"glory_const.xml"); + pugi::xml_parse_result result = doc.load_buffer(xml_glory_const.data(), xml_glory_const.size()); if (!result) { snprintf(buf, kMaxStringLength, "WARNING: glory_const.xml not found or unreadable (%s), skipping (non-fatal)", result.description()); perror(buf); diff --git a/src/gameplay/mechanics/named_stuff.cpp b/src/gameplay/mechanics/named_stuff.cpp index 81f793ddf..8655b97c4 100644 --- a/src/gameplay/mechanics/named_stuff.cpp +++ b/src/gameplay/mechanics/named_stuff.cpp @@ -553,7 +553,10 @@ void load() { stuff_list.clear(); pugi::xml_document doc; - doc.load_file(LIB_USERDATA"named_items.xml"); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_named_stuff = native_text::read_data_file(LIB_USERDATA"named_items.xml"); + doc.load_buffer(xml_named_stuff.data(), xml_named_stuff.size()); pugi::xml_node obj_list = doc.child("named_stuff_list"); for (pugi::xml_node node = obj_list.child("obj"); node; node = node.next_sibling("obj")) { diff --git a/src/gameplay/mechanics/sets_drop.cpp b/src/gameplay/mechanics/sets_drop.cpp index ad7493ec8..56e5272e6 100644 --- a/src/gameplay/mechanics/sets_drop.cpp +++ b/src/gameplay/mechanics/sets_drop.cpp @@ -2,6 +2,7 @@ // Part of Bylins http://www.mud.ru #include "sets_drop.h" +#include "utils/native_text.h" #include #include @@ -216,7 +217,10 @@ void create_clone_miniset(int vnum) { // * Инициализация списка сетов на лоад. void init_obj_list() { pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(CONFIG_FILE); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_sets_drop = native_text::read_data_file(CONFIG_FILE); + pugi::xml_parse_result result = doc.load_buffer(xml_sets_drop.data(), xml_sets_drop.size()); if (!result) { snprintf(buf, kMaxStringLength, "...%s", result.description()); mudlog(buf, CMP, kLvlImmortal, SYSLOG, true); diff --git a/src/gameplay/statistics/mob_stat.cpp b/src/gameplay/statistics/mob_stat.cpp index 3db05fcf1..99aff072d 100644 --- a/src/gameplay/statistics/mob_stat.cpp +++ b/src/gameplay/statistics/mob_stat.cpp @@ -2,6 +2,7 @@ // Part of Bylins http://www.mud.ru #include "mob_stat.h" +#include "utils/native_text.h" #include "third_party_libs/pugixml/pugixml.h" #include "utils/utils_parse.h" @@ -272,7 +273,10 @@ static void LoadXmlLegacy() { char buf_[kMaxInputLength]; pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(MUD::StateManager().Path(state::EStateFile::kMobStat).c_str()); + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + const std::string xml_mob_stat = native_text::read_data_file(MUD::StateManager().Path(state::EStateFile::kMobStat)); + pugi::xml_parse_result result = doc.load_buffer(xml_mob_stat.data(), xml_mob_stat.size()); if (!result) { snprintf(buf_, sizeof(buf_), "...%s", result.description()); mudlog(buf_, CMP, kLvlImmortal, SYSLOG, true); diff --git a/src/utils/diskio.cpp b/src/utils/diskio.cpp index f60cd12e4..a4af5eb95 100644 --- a/src/utils/diskio.cpp +++ b/src/utils/diskio.cpp @@ -10,6 +10,9 @@ **************************************************************************/ #include +#include +#include +#include "utils/native_text.h" #include "engine/core/sysdep.h" #include "utils/utils.h" #include "diskio.h" @@ -146,6 +149,24 @@ FBFILE *fbopen_for_read(char *fname) { strcpy(fbfl->name, fname); auto dummy = fread(fbfl->buf, sizeof(char), fbfl->size, fl); + fbfl->raw = fbfl->buf; + fbfl->raw_size = fbfl->size; + + // Данные на диске лежат в KOI8-R. Поднимаем весь буфер до нативной кодировки движка здесь, + // где размер под нашим контролем -- построчная конверсия рисковала бы переполнить буфер + // вызывающего, ведь кириллица при перекодировке удлиняется. Исходные байты остаются в raw + // для CRC. Под KOI8-R ветка не компилируется вовсе, buf и raw -- один и тот же указатель. + if (native_text::native_is_utf8()) { + const std::string native = native_text::from_disk_line(fbfl->buf); + if (native.size() != static_cast(fbfl->size)) { + char *converted = nullptr; + CREATE(converted, native.size() + 1); + memcpy(converted, native.c_str(), native.size() + 1); + fbfl->buf = converted; + fbfl->size = static_cast(native.size()); + fbfl->ptr = fbfl->buf; + } + } UNUSED_ARG(dummy); fclose(fl); @@ -193,6 +214,10 @@ size_t fbclose_for_read(FBFILE *fbfl) { return 0; } + // buf мог быть перекодирован в отдельный буфер; raw при этом указывает на исходный. + if (fbfl->raw && fbfl->raw != fbfl->buf) { + free(fbfl->raw); + } if (fbfl->buf) { free(fbfl->buf); } diff --git a/src/utils/diskio.h b/src/utils/diskio.h index 8d1bb08ad..dfc563586 100644 --- a/src/utils/diskio.h +++ b/src/utils/diskio.h @@ -38,6 +38,11 @@ typedef struct { int size; // size in bytes of buffer // int flags; // read/write/append, future expansion // char *name; // filename (for delayed writing) // + // Исходные байты файла, как они лежат на диске. buf может быть перекодирован в нативную + // кодировку движка (issue #3681), а CRC файлов игроков считается по ИСХОДНОМУ содержимому, + // иначе сумма разъедется. Под KOI8-R это тот же указатель, что и buf. + char *raw; // original on-disk bytes (for CRC) // + int raw_size; // size of raw in bytes // } FBFILE; void ExtractTagFromArgument(char *argument, char *tag); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 6ac2a559e..a6fac9d90 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -8,6 +8,7 @@ through them changes nothing until the encoding flip. */ #include "native_text.h" +#include "utf8.h" #include "utils_encoding.h" #ifdef INTERNAL_ENCODING_UTF8 @@ -26,6 +27,8 @@ constexpr unsigned char kYoLowerByte = 0xA3; constexpr unsigned char kYoUpperByte = 0xB3; #endif +#include +#include #include #include @@ -735,6 +738,27 @@ bool list_contains_char(std::string_view list, std::string_view ch) { return false; } +std::string from_disk_line(const char *line) { +#ifdef INTERNAL_ENCODING_UTF8 + if (line == nullptr || *line == '\0') { + return {}; + } + const std::string_view view(line); + return utf8::is_valid(view) ? std::string(view) : from_koi8(std::string(view)); +#else + return line == nullptr ? std::string() : std::string(line); +#endif +} + +std::string read_data_file(const std::string &path) { + std::ifstream in(path, std::ios::binary); + if (!in) { + return {}; + } + std::string raw((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + return from_koi8(raw); +} + } // namespace native_text // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/native_text.h b/src/utils/native_text.h index f18d22afe..fb15a72e3 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -156,6 +156,20 @@ std::string from_koi8(const std::string &text); // replaced by the converter (it substitutes '+'), which is unavoidable for a narrower encoding. std::string to_koi8(const std::string &text); +// Read a data file (all of which are stored in KOI8-R) and hand back its contents in the +// engine's native encoding. The counterpart of from_koi8 for whole files: parsers that take a +// buffer should go through this instead of reading the path themselves, so the boundary stays +// in one place. Returns an empty string if the file cannot be read -- callers report that the +// same way they did when the parser failed to open it. +std::string read_data_file(const std::string &path); + +// Bring one line read from a data file into the native encoding, tolerating a file that has +// already been converted. Under KOI8-R this is the identity. Under UTF-8 the text is taken as +// already-native when it is well-formed UTF-8 and transcoded otherwise -- Cyrillic in KOI8-R is +// almost never valid UTF-8, which makes validity a reliable discriminator and lets old and new +// player files coexist without a version field. +std::string from_disk_line(const char *line); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a From 16dc44c2822459442dcee569e1ff7c05931069c9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 14:56:35 +0200 Subject: [PATCH 025/110] fix(utf8): parse player names per character, not per byte (#3681) _parse_name() and parse_exist_name() walked the name a byte at a time and rejected it unless every byte passed a_isalpha(). A multibyte letter fails that on its trailing byte, so under UTF-8 every Russian name was refused -- the login screen just asked for the name again, including for characters that already exist. Both now step character by character. The old "*argument > 0" test meant "this character is not ASCII" (a high byte is negative as a signed char); it is now a direct check on the code point. Case folding is likewise per character -- first letter upper, the rest lower -- and stays length-preserving, so the destination buffer cannot overflow. Found by connecting to the UTF-8 container and typing an existing character's name. KOI8-R build unchanged: 654 passed / 0 failed, and an existing name is still recognised. --- src/engine/ui/login.cpp | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index 78b0a0f4b..3dd999e3f 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -183,18 +183,28 @@ std::map new_loc_codes; // имя чара на код, отправленный на почту для подтверждения мыла при создании std::map new_char_codes; +// Посимвольно, а не побайтно (issue #3681): у многобайтной буквы хвостовой байт не проходит +// проверку "это буква", и раньше здесь отвергалось любое русское имя. Условие "*argument > 0" +// означало "символ не ASCII" (у старших байтов знаковый char отрицателен) - теперь это прямая +// проверка кодовой точки. Регистр тоже сворачивается по символу: первая буква заглавная, +// остальные строчные; длина при этом не меняется, поэтому буфер name не переполняется. int _parse_name(char *argument, char *name) { - int i; + int i = 0; - // skip whitespaces - for (i = 0; (*name = (i ? LOWER(*argument) : UPPER(*argument))); argument++, i++, name++) { - if (native_text::first_char_code(argument) == rus::kYo - || native_text::first_char_code(argument) == rus::kYoUpper - || !a_isalpha(*argument) - || *argument > 0) { + while (*argument) { + const char32_t code = native_text::first_char_code(argument); + if (code == rus::kYo || code == rus::kYoUpper + || !native_text::is_alpha_char(argument) + || code < 0x80) { return (1); } + const size_t bytes = i ? native_text::copy_lower_char(argument, name) + : native_text::copy_upper_char(argument, name); + argument += bytes; + name += bytes; + ++i; } + *name = '\0'; if (!i) { return (1); @@ -208,12 +218,19 @@ int _parse_name(char *argument, char *name) { * чтобы их в игру вообще пускало, а новых с Ё/ё соответственно брило. */ int parse_exist_name(char *argument, char *name) { - int i; + int i = 0; - // skip whitespaces - for (i = 0; (*name = (i ? LOWER(*argument) : UPPER(*argument))); argument++, i++, name++) - if (!a_isalpha(*argument) || *argument > 0) + while (*argument) { + if (!native_text::is_alpha_char(argument) || native_text::first_char_code(argument) < 0x80) { return (1); + } + const size_t bytes = i ? native_text::copy_lower_char(argument, name) + : native_text::copy_upper_char(argument, name); + argument += bytes; + name += bytes; + ++i; + } + *name = '\0'; if (!i) return (1); From 822bc12dd06bad775fe1cf91c4c529151af8ee8a Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 15:10:21 +0200 Subject: [PATCH 026/110] fix(utf8): convert the player index when it is read (#3681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit players.lst is read with plain fopen/get_line, so it bypassed the FBFILE boundary and the index kept KOI8-R names while the runtime ran on UTF-8. Every existing character then looked new: the login screen offered to create "Дрегвий" instead of asking for a password. The name now goes through the boundary as it is read. Found on the UTF-8 container by typing the name of a character that exists in the production world -- the KOI8-R build recognised it, the UTF-8 one did not. 654 passed / 0 failed. --- src/engine/db/player_index.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/engine/db/player_index.cpp b/src/engine/db/player_index.cpp index 349ba6d86..67ef667c1 100644 --- a/src/engine/db/player_index.cpp +++ b/src/engine/db/player_index.cpp @@ -7,6 +7,7 @@ */ #include "player_index.h" +#include "utils/native_text.h" #include "administration/accounts.h" #include "global_objects.h" @@ -346,6 +347,13 @@ void BuildPlayerIndexNew() { if (sscanf(name, "%s ", playername) == 0) continue; + // players.lst читается обычным fopen, минуя FBFILE, поэтому границу кодировки надо + // пройти здесь: иначе индекс останется в KOI8-R, а поиск пойдёт по нативной кодировке + // и ни одного существующего персонажа не найдёт (issue #3681). + const std::string native_name = native_text::from_disk_line(playername); + strncpy(playername, native_name.c_str(), sizeof(playername) - 1); + playername[sizeof(playername) - 1] = '\0'; + if (!player_table.IsPlayerExists(playername)) { ActualizePlayersIndex(playername); } From f135eebaffe52bbd3b60e9eb6a0d620f31cbee68 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 15:31:49 +0200 Subject: [PATCH 027/110] fix(utf8): fold case per character in the player-name index (#3681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index's hasher and comparator lowered the name a byte at a time through the KOI8-R table. Under UTF-8 that table leaves a multibyte letter untouched, so "Дрегвий" and "дрегвий" hashed differently and the exact lookup missed: an existing character was offered as a new one at the login screen. The prefix check next to it uses strn_cmp, which is already character-aware, so it still matched -- which is why the symptom was "your name matches an existing character" rather than a clean miss. Both now fold per character: the hasher over a lowered copy, the comparator through native_text::compare_ci, so the pair stays consistent. Isolated by pointing both builds at the same freshly extracted world: KOI8-R recognised the name, UTF-8 did not, which ruled the data out. 654 passed / 0 failed, and the KOI8-R build still recognises the same name. --- src/engine/db/player_index.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/engine/db/player_index.cpp b/src/engine/db/player_index.cpp index 67ef667c1..ee7f948d1 100644 --- a/src/engine/db/player_index.cpp +++ b/src/engine/db/player_index.cpp @@ -104,24 +104,24 @@ std::size_t PlayersIndex::hasher::operator()(const std::string &value) const { // FNV-1a implementation using p = fnv_params; std::size_t result = p::offset_basis; - for (unsigned char c : value) { - result ^= static_cast(LOWER(c)); + // Регистр сворачивается ПО СИМВОЛУ, а не по байту (issue #3681): побайтная свёртка + // оставляет многобайтные буквы нетронутыми, из-за чего "Дрегвий" и "дрегвий" дают разные + // хэши и точный поиск персонажа промахивается. Компаратор ниже обязан согласоваться. + std::string folded = value; + native_text::to_lower(folded); + for (unsigned char c : folded) { + result ^= static_cast(c); result *= p::prime; } return result; } bool PlayersIndex::equal_to::operator()(const std::string &left, const std::string &right) const { - if (left.size() != right.size()) { + // Посимвольное сравнение без учёта регистра -- в паре к hasher выше. + if (native_text::compare_ci(left, right) != 0) { return false; } - for (std::size_t i = 0; i < left.size(); ++i) { - if (LOWER(left[i]) != LOWER(right[i])) { - return false; - } - } - return true; } From c685b9da77763cb518295cfd2bc4b4d8a24359e9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 5 Aug 2026 16:07:37 +0200 Subject: [PATCH 028/110] fix(utf8): fold the name per character when building the player index (#3681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ActualizePlayersIndex() lowercased the name a byte at a time. For "г" the second UTF-8 byte is 0xB3, which the KOI8-R table reads as "Ё" and lowers to 0xA3 -- so the letter turned into "У". The mangled name produced a wrong save-file path, the character failed to load, and it was silently left out of the index. Measured on the production world: the index held 9117 entries under KOI8-R and only 7839 under UTF-8, i.e. 1278 characters were missing. Every one of them would have been offered their own name as "available" at the login screen. Isolated by counting index insertions in both builds (2 skips vs 1280) and dumping the names that were skipped -- "свароУ", "сиУурд", "раУнар" made the corrupted letter obvious. 654 passed / 0 failed. --- src/engine/db/player_index.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/db/player_index.cpp b/src/engine/db/player_index.cpp index ee7f948d1..535b3da75 100644 --- a/src/engine/db/player_index.cpp +++ b/src/engine/db/player_index.cpp @@ -259,7 +259,11 @@ void ActualizePlayersIndex(char *name) { int deleted; char filename[kMaxStringLength]; - for (int i = 0; (name[i] = LOWER(name[i])); i++); + // По символу, а не по байту (issue #3681). Побайтная свёртка бьёт многобайтные буквы: + // у "г" второй байт UTF-8 равен 0xB3, а в KOI8-R это "Ё", и таблица опускала его в 0xA3 -- + // буква превращалась в "У". Имя файла получалось неверным, персонаж не загружался и просто + // не попадал в индекс (в боевом мире так терялось 1278 из 9117 записей). + native_text::to_lower(name); if (get_filename(name, filename, kPlayersFile)) { Player t_short_ch; Player *short_ch = &t_short_ch; From ca7ae79452398e23b6560586e62b39f2e6bc48f9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 04:24:45 +0200 Subject: [PATCH 029/110] feat: report the internal encoding, and keep the revision in container builds (#3681) "version" now prints the encoding the engine actually runs on. It is read from native_text at runtime rather than baked in as a build string, so it cannot drift from the truth. The revision was empty in container builds (and the release counter fell back to 0, hence "0.1.32.0"). git is installed in the builder, but the build context is a worktree whose .git is a link file pointing outside it, so rev-parse produced nothing. Both values can now be passed in through the environment, which also covers builds from an unpacked archive; the Dockerfile forwards them as GIT_REV / GIT_COUNT build args. Without git and without them the revision reads "unknown" instead of being blank. Build the image with: docker build --build-arg GIT_REV=$(git rev-parse --short HEAD) \ --build-arg GIT_COUNT=$(git rev-list --count HEAD) ... --- Dockerfile | 9 +++++++++ src/version.cpp.in | 13 +++++++++++-- tools/meson/generate_version.py | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c8facdf7c..2030fda14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,12 @@ 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 \ @@ -74,6 +80,9 @@ RUN if [ "$INTERNAL_ENCODING" = "utf8" ]; then \ 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); \ diff --git a/src/version.cpp.in b/src/version.cpp.in index 0136dacf9..a3b268279 100644 --- a/src/version.cpp.in +++ b/src/version.cpp.in @@ -9,6 +9,7 @@ #include "engine/entities/char_data.h" #include "engine/structs/structs.h" #include "utils/logger.h" +#include "utils/native_text.h" const char* REVISION = "$ Build revision: ${REVISION} $"; const char* revision = "${REVISION}"; @@ -21,8 +22,15 @@ const char* build_datetime = "${BUILD_DATETIME}"; const char* build_compiler = "${BUILD_COMPILER}"; const char* build_features = "${BUILD_FEATURES}"; +// Кодировка берётся из рантайма, а не из строки сборки: так она не может разойтись с тем, +// как движок на самом деле работает с текстом (issue #3681). +static const char *InternalEncodingName() { + return native_text::native_is_utf8() ? "UTF-8" : "KOI8-R"; +} + void ShowBuildInfo(CharData *ch) { SendMsgToChar(ch, "%s %s, build from %s, revision %s\n", engine_name, engine_version, build_datetime, revision); + SendMsgToChar(ch, "Internal encoding: %s\n", InternalEncodingName()); SendMsgToChar(ch, "Based on CircleMUD, version 3.00 beta patchlevel 16\n"); if (privilege::IsImmortal(ch)) { SendMsgToChar(ch, "Compiler: %s\nEnabled features: %s\n", build_compiler, build_features); @@ -30,8 +38,9 @@ void ShowBuildInfo(CharData *ch) { } void LogBuildInfo() { - log("%s %s, build from %s, revision: %s\r\nCompiler: %s\r\nEnabled features: %s", - engine_name, engine_version, build_datetime, revision, build_compiler, build_features); + log("%s %s, build from %s, revision: %s\r\nInternal encoding: %s\r\nCompiler: %s\r\nEnabled features: %s", + engine_name, engine_version, build_datetime, revision, InternalEncodingName(), + build_compiler, build_features); } // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tools/meson/generate_version.py b/tools/meson/generate_version.py index 851fef316..852eaa690 100755 --- a/tools/meson/generate_version.py +++ b/tools/meson/generate_version.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import sys +import os import subprocess from datetime import datetime @@ -20,6 +21,11 @@ except Exception: pass +# Сборка может идти там, где git недоступен или бесполезен: контейнер, worktree (у него .git -- +# файл-ссылка наружу контекста), распакованный архив. Тогда значения пробрасываются окружением. +if not git_rev or git_rev == 'unknown': + git_rev = os.environ.get('BYLINS_GIT_REV', '') or git_rev or 'unknown' + # issue.versioning: the 4th version part ("release") = total commit count -- monotonic, # +1 per commit. Computed at build time like the revision hash. If git is unavailable # (e.g. build from an unpacked archive) it falls back to 0, mirroring git_rev above. @@ -34,6 +40,9 @@ except Exception: pass +if commit_count == "0": + commit_count = os.environ.get('BYLINS_COMMIT_COUNT', '') or "0" + # major.minor.patch: explicit, from the repo-root VERSION file (single source of truth). try: with open(f'{source_root}/VERSION.txt', encoding='ascii') as vf: From 353507f7226f975d8d0eed94920525049821ccfc Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 04:28:01 +0200 Subject: [PATCH 030/110] feat(utf8): take boards and the changelog through the encoding boundary (C3, #3681) Board files and the changelog are read straight from std::ifstream, so they bypassed the FBFILE boundary and their text stayed KOI8-R while the runtime ran on UTF-8. Author, subject and message body now cross the boundary as they are parsed, as does every line of the changelog. Identity under KOI8-R. 654 passed / 0 failed. --- .../communication/boards/boards_changelog_loaders.cpp | 5 +++++ src/gameplay/communication/boards/boards_types.cpp | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/gameplay/communication/boards/boards_changelog_loaders.cpp b/src/gameplay/communication/boards/boards_changelog_loaders.cpp index f8f363a5b..2a3b4cdfc 100644 --- a/src/gameplay/communication/boards/boards_changelog_loaders.cpp +++ b/src/gameplay/communication/boards/boards_changelog_loaders.cpp @@ -1,4 +1,5 @@ #include "boards_changelog_loaders.h" +#include "utils/native_text.h" #include "utils/logger.h" #include "boards_constants.h" @@ -203,6 +204,9 @@ bool GitChangeLogLoader::GitCommitReader::next_line(std::string &buffer, std::is if (!std::getline(is, buffer)) { return false; } + // Changelog лежит на диске в KOI8-R и читается потоком; границу кодировки проходим здесь + // (issue #3681). Под KOI8-R это тождество. + buffer = native_text::from_disk_line(buffer.c_str()); ++line; return true; @@ -221,6 +225,7 @@ bool GitChangeLogLoader::load(std::istream &is) { int line = 0; std::string buffer; while (std::getline(is, buffer)) { + buffer = native_text::from_disk_line(buffer.c_str()); ++line; switch (parser_state) { case REVISION: diff --git a/src/gameplay/communication/boards/boards_types.cpp b/src/gameplay/communication/boards/boards_types.cpp index ff411c350..e970474b6 100644 --- a/src/gameplay/communication/boards/boards_types.cpp +++ b/src/gameplay/communication/boards/boards_types.cpp @@ -1,4 +1,5 @@ #include "boards_types.h" +#include "utils/native_text.h" #include "boards_constants.h" #include "utils/logger.h" @@ -58,6 +59,13 @@ void Board::Load() { ReadEndString(file); std::getline(file, message->text, '~'); + // Доски лежат на диске в KOI8-R и читаются потоком, минуя FBFILE, поэтому границу + // кодировки проходим здесь -- по текстовым полям (issue #3681). Под KOI8-R это + // тождество. + message->author = native_text::from_disk_line(message->author.c_str()); + message->subject = native_text::from_disk_line(message->subject.c_str()); + message->text = native_text::from_disk_line(message->text.c_str()); + // не помешает глянуть че мы там залоадили if (message->author.empty() || !message->unique From b7bdd1c569a0f8eb02bc1d7f73b7d14f4ae93b43 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 04:30:14 +0200 Subject: [PATCH 031/110] test(utf8): cover board loading, which had only a stub test (#3681) tests/boards.news.cpp never asserted anything -- it is a placeholder waiting on global state to be untangled -- so the board file path had no coverage at all, even though news.sample carries Russian text. The new test loads that sample through the real Board::Load() and checks the messages come out in the engine's native encoding: valid UTF-8 exactly when the runtime is UTF-8, KOI8-R otherwise. It also matches an author name literally, which fails loudly if the text arrives in the wrong encoding. Meaningful in both builds: the literals compile in whatever encoding the engine runs on. --- tests/boards.encoding.cpp | 73 +++++++++++++++++++++++++++++++++++++++ tests/meson.build | 1 + 2 files changed, 74 insertions(+) create mode 100644 tests/boards.encoding.cpp diff --git a/tests/boards.encoding.cpp b/tests/boards.encoding.cpp new file mode 100644 index 000000000..66921192d --- /dev/null +++ b/tests/boards.encoding.cpp @@ -0,0 +1,73 @@ +// Загрузка доски с русским текстом: проверяет, что сообщения приходят в НАТИВНОЙ кодировке +// движка. Файл доски лежит на диске в KOI8-R и читается потоком, минуя FBFILE, поэтому это +// отдельная граница кодировки (issue #3681). Тест осмыслен в обеих сборках: литералы ниже +// компилируются в той же кодировке, в какой работает движок. + +#include "gameplay/communication/boards/boards_types.h" +#include "utils/native_text.h" +#include "utils/utf8.h" + +#include + +#include +#include + +namespace { + +const char *const kNewsSample = "data/boards/news.sample"; + +Boards::Board::shared_ptr LoadSampleBoard() { + auto board = std::make_shared(Boards::NEWS_BOARD); + board->set_file_name(kNewsSample); + board->Load(); + return board; +} + +} // namespace + +TEST(BoardsEncoding, MessagesLoadInTheNativeEncoding) { + std::ifstream probe(kNewsSample); + ASSERT_TRUE(probe.is_open()) << "нет тестовых данных " << kNewsSample; + probe.close(); + + const auto board = LoadSampleBoard(); + ASSERT_FALSE(board->empty()) << "доска не загрузилась"; + + bool seen_cyrillic = false; + for (std::size_t i = 0; i < board->messages_count(); ++i) { + const auto message = board->get_message(i); + for (const std::string *field : {&message->author, &message->subject, &message->text}) { + if (field->empty()) { + continue; + } + const bool has_high_byte = std::any_of(field->begin(), field->end(), + [](char c) { return static_cast(c) >= 0x80; }); + if (!has_high_byte) { + continue; + } + seen_cyrillic = true; + // В UTF-8-сборке текст обязан быть корректным UTF-8; в KOI8-R-сборке -- наоборот, + // это KOI8-R, который валидным UTF-8 не является. + EXPECT_EQ(utf8::is_valid(*field), native_text::native_is_utf8()) + << "поле не в нативной кодировке: " << *field; + } + } + EXPECT_TRUE(seen_cyrillic) << "в тестовых данных не оказалось русского текста -- проверять нечего"; +} + +TEST(BoardsEncoding, AuthorNameSurvivesTheBoundary) { + const auto board = LoadSampleBoard(); + ASSERT_FALSE(board->empty()); + + // Автор одного из сообщений в news.sample -- "Стрибог". + bool found = false; + for (std::size_t i = 0; i < board->messages_count(); ++i) { + if (board->get_message(i)->author == "Стрибог") { + found = true; + break; + } + } + EXPECT_TRUE(found) << "имя автора не совпало -- текст доски пришёл не в той кодировке"; +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/meson.build b/tests/meson.build index 4879e6b9d..5a03b297c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -24,6 +24,7 @@ test_sources = files( 'blocking.queue.cpp', 'boards.changelog.cpp', 'boards.news.cpp', + 'boards.encoding.cpp', 'msdp.parser.cpp', 'msdp.builder.cpp', 'char.affects.cpp', From 1358d530474ae277c3f4a4581de0408c0aeb546a Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:05:38 +0200 Subject: [PATCH 032/110] =?UTF-8?q?feat(utf8):=20=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D1=81=D0=BB=D0=B8=D1=82=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=BF=D0=BE=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20KOI?= =?UTF-8?q?8-R=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Клиент в KOI8-R/CP866/Win-1251 получает текст через KOI8-R, а движок под UTF-8 может держать в строках то, чего в KOI8-R нет: типографское тире, «ёлочки», латиницу с диакритикой, знак евро. Раньше всё это одинаково схлопывалось в один символ-заглушку, хотя читателю большая часть из этого была бы понятна. Добавлена таблица ближайших соответствий (333 записи, сгенерирована и сверена с кодеком KOI8-R: каждая запись действительно отсутствует в KOI8-R, латиница взята из NFKD со снятыми комбинирующими знаками). Подстановка идёт предпроходом по UTF-8 до конвертации -- именно это позволяет замене быть длиннее одного символа ("..." для многоточия, "(tm)", "EUR"), чего сам конвертер не умеет: он пишет ровно один байт на кодовую точку. Заглушка сменена с '+' на '?': плюс в игре -- обычный текст ("+5 к урону"), и потерянный символ читался как содержимое. --- meson.build | 1 + src/utils/native_text.cpp | 26 ++- src/utils/translit_koi8.cpp | 379 +++++++++++++++++++++++++++++++++++ src/utils/translit_koi8.h | 20 ++ src/utils/utils_encoding.cpp | 5 +- tests/meson.build | 1 + tests/translit_koi8.cpp | 110 ++++++++++ tests/utils.encoding.cpp | 6 +- 8 files changed, 543 insertions(+), 5 deletions(-) create mode 100644 src/utils/translit_koi8.cpp create mode 100644 src/utils/translit_koi8.h create mode 100644 tests/translit_koi8.cpp diff --git a/meson.build b/meson.build index 0327c0fe5..741eef286 100644 --- a/meson.build +++ b/meson.build @@ -653,6 +653,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', diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index a6fac9d90..113c6d6fd 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -10,6 +10,7 @@ through them changes nothing until the encoding flip. #include "native_text.h" #include "utf8.h" #include "utils_encoding.h" +#include "translit_koi8.h" #ifdef INTERNAL_ENCODING_UTF8 #include "utf8.h" @@ -423,9 +424,30 @@ std::string to_koi8(const std::string &text) { if (!has_high_byte) { return text; // pure ASCII is spelled identically in both encodings } + // Characters KOI8-R does not have are first reduced to the closest thing it does have + // (a typographic dash to "-", curly quotes to '"', an accented letter to its base one); + // only what has no equivalent reaches the converter's placeholder. Doing it here, before + // the conversion, is what allows a replacement to be longer than one character. + std::string reduced; + reduced.reserve(text.size()); + for (std::size_t pos = 0; pos < text.size();) { + char32_t cp = 0; + const std::size_t len = utf8::decode(text, pos, cp); + if (len == 0) { + break; + } + const char *const replacement = codepages::TranslitToKoi8(cp); + if (replacement != nullptr) { + reduced.append(replacement); + } else { + reduced.append(text, pos, len); + } + pos += len; + } + // KOI8-R is never longer than the UTF-8 it came from, so the input size is a safe bound. - std::vector out(text.size() + 1, '\0'); - codepages::utf8_to_koi(const_cast(text.c_str()), out.data()); + std::vector out(reduced.size() + 1, '\0'); + codepages::utf8_to_koi(const_cast(reduced.c_str()), out.data()); return std::string(out.data()); } diff --git a/src/utils/translit_koi8.cpp b/src/utils/translit_koi8.cpp new file mode 100644 index 000000000..dcc9fc635 --- /dev/null +++ b/src/utils/translit_koi8.cpp @@ -0,0 +1,379 @@ +/** +\file translit_koi8.cpp - a part of the Bylins engine. +\brief Reducing text to what KOI8-R can actually represent (issue #3681). + +Legacy clients speak KOI8-R, CP866 or Windows-1251, and all of those are reached through +KOI8-R. When the engine runs on UTF-8 the text may contain characters none of them has -- a +typographic dash, curly quotes, an accented Latin letter, an emoji. Dropping such a character +to a single placeholder loses information that a reader could still have used, so this table +maps each one to the closest thing KOI8-R does have ("--", '"', "e", "..."), and only what has +no sensible equivalent falls back to the placeholder. + +The table is generated, not written by hand: every entry was checked against the KOI8-R codec +to be genuinely absent from it, and the Latin replacements come from Unicode NFKD with the +combining marks stripped. Replacements are UTF-8, because the substitution happens before the +conversion, and are spelled as \xNN escapes so they do not depend on this file's encoding. +*/ + +#include "translit_koi8.h" + +#include + +namespace codepages { + +namespace { + +struct TranslitEntry { + char32_t code_point; + const char *replacement; // UTF-8 +}; + +// Sorted by code point: looked up with a binary search. +constexpr TranslitEntry kTable[] = { + {0x00A2, "\x63"}, // CENT SIGN + {0x00A3, "\x47\x42\x50"}, // POUND SIGN + {0x00A5, "\x4A\x50\x59"}, // YEN SIGN + {0x00AB, "\x22"}, // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + {0x00AD, ""}, // SOFT HYPHEN + {0x00AE, "\x28\x52\x29"}, // REGISTERED SIGN + {0x00BB, "\x22"}, // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + {0x00C0, "\x41"}, // LATIN CAPITAL LETTER A WITH GRAVE + {0x00C1, "\x41"}, // LATIN CAPITAL LETTER A WITH ACUTE + {0x00C2, "\x41"}, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + {0x00C3, "\x41"}, // LATIN CAPITAL LETTER A WITH TILDE + {0x00C4, "\x41"}, // LATIN CAPITAL LETTER A WITH DIAERESIS + {0x00C5, "\x41"}, // LATIN CAPITAL LETTER A WITH RING ABOVE + {0x00C6, "\x41\x45"}, // LATIN CAPITAL LETTER AE + {0x00C7, "\x43"}, // LATIN CAPITAL LETTER C WITH CEDILLA + {0x00C8, "\x45"}, // LATIN CAPITAL LETTER E WITH GRAVE + {0x00C9, "\x45"}, // LATIN CAPITAL LETTER E WITH ACUTE + {0x00CA, "\x45"}, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + {0x00CB, "\x45"}, // LATIN CAPITAL LETTER E WITH DIAERESIS + {0x00CC, "\x49"}, // LATIN CAPITAL LETTER I WITH GRAVE + {0x00CD, "\x49"}, // LATIN CAPITAL LETTER I WITH ACUTE + {0x00CE, "\x49"}, // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + {0x00CF, "\x49"}, // LATIN CAPITAL LETTER I WITH DIAERESIS + {0x00D0, "\x44"}, // LATIN CAPITAL LETTER ETH + {0x00D1, "\x4E"}, // LATIN CAPITAL LETTER N WITH TILDE + {0x00D2, "\x4F"}, // LATIN CAPITAL LETTER O WITH GRAVE + {0x00D3, "\x4F"}, // LATIN CAPITAL LETTER O WITH ACUTE + {0x00D4, "\x4F"}, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + {0x00D5, "\x4F"}, // LATIN CAPITAL LETTER O WITH TILDE + {0x00D6, "\x4F"}, // LATIN CAPITAL LETTER O WITH DIAERESIS + {0x00D7, "\x78"}, // MULTIPLICATION SIGN + {0x00D8, "\x4F"}, // LATIN CAPITAL LETTER O WITH STROKE + {0x00D9, "\x55"}, // LATIN CAPITAL LETTER U WITH GRAVE + {0x00DA, "\x55"}, // LATIN CAPITAL LETTER U WITH ACUTE + {0x00DB, "\x55"}, // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + {0x00DC, "\x55"}, // LATIN CAPITAL LETTER U WITH DIAERESIS + {0x00DD, "\x59"}, // LATIN CAPITAL LETTER Y WITH ACUTE + {0x00DE, "\x54\x68"}, // LATIN CAPITAL LETTER THORN + {0x00DF, "\x73\x73"}, // LATIN SMALL LETTER SHARP S + {0x00E0, "\x61"}, // LATIN SMALL LETTER A WITH GRAVE + {0x00E1, "\x61"}, // LATIN SMALL LETTER A WITH ACUTE + {0x00E2, "\x61"}, // LATIN SMALL LETTER A WITH CIRCUMFLEX + {0x00E3, "\x61"}, // LATIN SMALL LETTER A WITH TILDE + {0x00E4, "\x61"}, // LATIN SMALL LETTER A WITH DIAERESIS + {0x00E5, "\x61"}, // LATIN SMALL LETTER A WITH RING ABOVE + {0x00E6, "\x61\x65"}, // LATIN SMALL LETTER AE + {0x00E7, "\x63"}, // LATIN SMALL LETTER C WITH CEDILLA + {0x00E8, "\x65"}, // LATIN SMALL LETTER E WITH GRAVE + {0x00E9, "\x65"}, // LATIN SMALL LETTER E WITH ACUTE + {0x00EA, "\x65"}, // LATIN SMALL LETTER E WITH CIRCUMFLEX + {0x00EB, "\x65"}, // LATIN SMALL LETTER E WITH DIAERESIS + {0x00EC, "\x69"}, // LATIN SMALL LETTER I WITH GRAVE + {0x00ED, "\x69"}, // LATIN SMALL LETTER I WITH ACUTE + {0x00EE, "\x69"}, // LATIN SMALL LETTER I WITH CIRCUMFLEX + {0x00EF, "\x69"}, // LATIN SMALL LETTER I WITH DIAERESIS + {0x00F0, "\x64"}, // LATIN SMALL LETTER ETH + {0x00F1, "\x6E"}, // LATIN SMALL LETTER N WITH TILDE + {0x00F2, "\x6F"}, // LATIN SMALL LETTER O WITH GRAVE + {0x00F3, "\x6F"}, // LATIN SMALL LETTER O WITH ACUTE + {0x00F4, "\x6F"}, // LATIN SMALL LETTER O WITH CIRCUMFLEX + {0x00F5, "\x6F"}, // LATIN SMALL LETTER O WITH TILDE + {0x00F6, "\x6F"}, // LATIN SMALL LETTER O WITH DIAERESIS + {0x00F8, "\x6F"}, // LATIN SMALL LETTER O WITH STROKE + {0x00F9, "\x75"}, // LATIN SMALL LETTER U WITH GRAVE + {0x00FA, "\x75"}, // LATIN SMALL LETTER U WITH ACUTE + {0x00FB, "\x75"}, // LATIN SMALL LETTER U WITH CIRCUMFLEX + {0x00FC, "\x75"}, // LATIN SMALL LETTER U WITH DIAERESIS + {0x00FD, "\x79"}, // LATIN SMALL LETTER Y WITH ACUTE + {0x00FE, "\x74\x68"}, // LATIN SMALL LETTER THORN + {0x00FF, "\x79"}, // LATIN SMALL LETTER Y WITH DIAERESIS + {0x0100, "\x41"}, // LATIN CAPITAL LETTER A WITH MACRON + {0x0101, "\x61"}, // LATIN SMALL LETTER A WITH MACRON + {0x0102, "\x41"}, // LATIN CAPITAL LETTER A WITH BREVE + {0x0103, "\x61"}, // LATIN SMALL LETTER A WITH BREVE + {0x0104, "\x41"}, // LATIN CAPITAL LETTER A WITH OGONEK + {0x0105, "\x61"}, // LATIN SMALL LETTER A WITH OGONEK + {0x0106, "\x43"}, // LATIN CAPITAL LETTER C WITH ACUTE + {0x0107, "\x63"}, // LATIN SMALL LETTER C WITH ACUTE + {0x0108, "\x43"}, // LATIN CAPITAL LETTER C WITH CIRCUMFLEX + {0x0109, "\x63"}, // LATIN SMALL LETTER C WITH CIRCUMFLEX + {0x010A, "\x43"}, // LATIN CAPITAL LETTER C WITH DOT ABOVE + {0x010B, "\x63"}, // LATIN SMALL LETTER C WITH DOT ABOVE + {0x010C, "\x43"}, // LATIN CAPITAL LETTER C WITH CARON + {0x010D, "\x63"}, // LATIN SMALL LETTER C WITH CARON + {0x010E, "\x44"}, // LATIN CAPITAL LETTER D WITH CARON + {0x010F, "\x64"}, // LATIN SMALL LETTER D WITH CARON + {0x0112, "\x45"}, // LATIN CAPITAL LETTER E WITH MACRON + {0x0113, "\x65"}, // LATIN SMALL LETTER E WITH MACRON + {0x0114, "\x45"}, // LATIN CAPITAL LETTER E WITH BREVE + {0x0115, "\x65"}, // LATIN SMALL LETTER E WITH BREVE + {0x0116, "\x45"}, // LATIN CAPITAL LETTER E WITH DOT ABOVE + {0x0117, "\x65"}, // LATIN SMALL LETTER E WITH DOT ABOVE + {0x0118, "\x45"}, // LATIN CAPITAL LETTER E WITH OGONEK + {0x0119, "\x65"}, // LATIN SMALL LETTER E WITH OGONEK + {0x011A, "\x45"}, // LATIN CAPITAL LETTER E WITH CARON + {0x011B, "\x65"}, // LATIN SMALL LETTER E WITH CARON + {0x011C, "\x47"}, // LATIN CAPITAL LETTER G WITH CIRCUMFLEX + {0x011D, "\x67"}, // LATIN SMALL LETTER G WITH CIRCUMFLEX + {0x011E, "\x47"}, // LATIN CAPITAL LETTER G WITH BREVE + {0x011F, "\x67"}, // LATIN SMALL LETTER G WITH BREVE + {0x0120, "\x47"}, // LATIN CAPITAL LETTER G WITH DOT ABOVE + {0x0121, "\x67"}, // LATIN SMALL LETTER G WITH DOT ABOVE + {0x0122, "\x47"}, // LATIN CAPITAL LETTER G WITH CEDILLA + {0x0123, "\x67"}, // LATIN SMALL LETTER G WITH CEDILLA + {0x0124, "\x48"}, // LATIN CAPITAL LETTER H WITH CIRCUMFLEX + {0x0125, "\x68"}, // LATIN SMALL LETTER H WITH CIRCUMFLEX + {0x0128, "\x49"}, // LATIN CAPITAL LETTER I WITH TILDE + {0x0129, "\x69"}, // LATIN SMALL LETTER I WITH TILDE + {0x012A, "\x49"}, // LATIN CAPITAL LETTER I WITH MACRON + {0x012B, "\x69"}, // LATIN SMALL LETTER I WITH MACRON + {0x012C, "\x49"}, // LATIN CAPITAL LETTER I WITH BREVE + {0x012D, "\x69"}, // LATIN SMALL LETTER I WITH BREVE + {0x012E, "\x49"}, // LATIN CAPITAL LETTER I WITH OGONEK + {0x012F, "\x69"}, // LATIN SMALL LETTER I WITH OGONEK + {0x0130, "\x49"}, // LATIN CAPITAL LETTER I WITH DOT ABOVE + {0x0132, "\x49\x4A"}, // LATIN CAPITAL LIGATURE IJ + {0x0133, "\x69\x6A"}, // LATIN SMALL LIGATURE IJ + {0x0134, "\x4A"}, // LATIN CAPITAL LETTER J WITH CIRCUMFLEX + {0x0135, "\x6A"}, // LATIN SMALL LETTER J WITH CIRCUMFLEX + {0x0136, "\x4B"}, // LATIN CAPITAL LETTER K WITH CEDILLA + {0x0137, "\x6B"}, // LATIN SMALL LETTER K WITH CEDILLA + {0x0139, "\x4C"}, // LATIN CAPITAL LETTER L WITH ACUTE + {0x013A, "\x6C"}, // LATIN SMALL LETTER L WITH ACUTE + {0x013B, "\x4C"}, // LATIN CAPITAL LETTER L WITH CEDILLA + {0x013C, "\x6C"}, // LATIN SMALL LETTER L WITH CEDILLA + {0x013D, "\x4C"}, // LATIN CAPITAL LETTER L WITH CARON + {0x013E, "\x6C"}, // LATIN SMALL LETTER L WITH CARON + {0x013F, "\x4C"}, // LATIN CAPITAL LETTER L WITH MIDDLE DOT + {0x0140, "\x6C"}, // LATIN SMALL LETTER L WITH MIDDLE DOT + {0x0141, "\x4C"}, // LATIN CAPITAL LETTER L WITH STROKE + {0x0142, "\x6C"}, // LATIN SMALL LETTER L WITH STROKE + {0x0143, "\x4E"}, // LATIN CAPITAL LETTER N WITH ACUTE + {0x0144, "\x6E"}, // LATIN SMALL LETTER N WITH ACUTE + {0x0145, "\x4E"}, // LATIN CAPITAL LETTER N WITH CEDILLA + {0x0146, "\x6E"}, // LATIN SMALL LETTER N WITH CEDILLA + {0x0147, "\x4E"}, // LATIN CAPITAL LETTER N WITH CARON + {0x0148, "\x6E"}, // LATIN SMALL LETTER N WITH CARON + {0x0149, "\x6E"}, // LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + {0x014C, "\x4F"}, // LATIN CAPITAL LETTER O WITH MACRON + {0x014D, "\x6F"}, // LATIN SMALL LETTER O WITH MACRON + {0x014E, "\x4F"}, // LATIN CAPITAL LETTER O WITH BREVE + {0x014F, "\x6F"}, // LATIN SMALL LETTER O WITH BREVE + {0x0150, "\x4F"}, // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + {0x0151, "\x6F"}, // LATIN SMALL LETTER O WITH DOUBLE ACUTE + {0x0154, "\x52"}, // LATIN CAPITAL LETTER R WITH ACUTE + {0x0155, "\x72"}, // LATIN SMALL LETTER R WITH ACUTE + {0x0156, "\x52"}, // LATIN CAPITAL LETTER R WITH CEDILLA + {0x0157, "\x72"}, // LATIN SMALL LETTER R WITH CEDILLA + {0x0158, "\x52"}, // LATIN CAPITAL LETTER R WITH CARON + {0x0159, "\x72"}, // LATIN SMALL LETTER R WITH CARON + {0x015A, "\x53"}, // LATIN CAPITAL LETTER S WITH ACUTE + {0x015B, "\x73"}, // LATIN SMALL LETTER S WITH ACUTE + {0x015C, "\x53"}, // LATIN CAPITAL LETTER S WITH CIRCUMFLEX + {0x015D, "\x73"}, // LATIN SMALL LETTER S WITH CIRCUMFLEX + {0x015E, "\x53"}, // LATIN CAPITAL LETTER S WITH CEDILLA + {0x015F, "\x73"}, // LATIN SMALL LETTER S WITH CEDILLA + {0x0160, "\x53"}, // LATIN CAPITAL LETTER S WITH CARON + {0x0161, "\x73"}, // LATIN SMALL LETTER S WITH CARON + {0x0162, "\x54"}, // LATIN CAPITAL LETTER T WITH CEDILLA + {0x0163, "\x74"}, // LATIN SMALL LETTER T WITH CEDILLA + {0x0164, "\x54"}, // LATIN CAPITAL LETTER T WITH CARON + {0x0165, "\x74"}, // LATIN SMALL LETTER T WITH CARON + {0x0168, "\x55"}, // LATIN CAPITAL LETTER U WITH TILDE + {0x0169, "\x75"}, // LATIN SMALL LETTER U WITH TILDE + {0x016A, "\x55"}, // LATIN CAPITAL LETTER U WITH MACRON + {0x016B, "\x75"}, // LATIN SMALL LETTER U WITH MACRON + {0x016C, "\x55"}, // LATIN CAPITAL LETTER U WITH BREVE + {0x016D, "\x75"}, // LATIN SMALL LETTER U WITH BREVE + {0x016E, "\x55"}, // LATIN CAPITAL LETTER U WITH RING ABOVE + {0x016F, "\x75"}, // LATIN SMALL LETTER U WITH RING ABOVE + {0x0170, "\x55"}, // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + {0x0171, "\x75"}, // LATIN SMALL LETTER U WITH DOUBLE ACUTE + {0x0172, "\x55"}, // LATIN CAPITAL LETTER U WITH OGONEK + {0x0173, "\x75"}, // LATIN SMALL LETTER U WITH OGONEK + {0x0174, "\x57"}, // LATIN CAPITAL LETTER W WITH CIRCUMFLEX + {0x0175, "\x77"}, // LATIN SMALL LETTER W WITH CIRCUMFLEX + {0x0176, "\x59"}, // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX + {0x0177, "\x79"}, // LATIN SMALL LETTER Y WITH CIRCUMFLEX + {0x0178, "\x59"}, // LATIN CAPITAL LETTER Y WITH DIAERESIS + {0x0179, "\x5A"}, // LATIN CAPITAL LETTER Z WITH ACUTE + {0x017A, "\x7A"}, // LATIN SMALL LETTER Z WITH ACUTE + {0x017B, "\x5A"}, // LATIN CAPITAL LETTER Z WITH DOT ABOVE + {0x017C, "\x7A"}, // LATIN SMALL LETTER Z WITH DOT ABOVE + {0x017D, "\x5A"}, // LATIN CAPITAL LETTER Z WITH CARON + {0x017E, "\x7A"}, // LATIN SMALL LETTER Z WITH CARON + {0x017F, "\x73"}, // LATIN SMALL LETTER LONG S + {0x01A0, "\x4F"}, // LATIN CAPITAL LETTER O WITH HORN + {0x01A1, "\x6F"}, // LATIN SMALL LETTER O WITH HORN + {0x01AF, "\x55"}, // LATIN CAPITAL LETTER U WITH HORN + {0x01B0, "\x75"}, // LATIN SMALL LETTER U WITH HORN + {0x01C4, "\x44\x5A"}, // LATIN CAPITAL LETTER DZ WITH CARON + {0x01C5, "\x44\x7A"}, // LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON + {0x01C6, "\x64\x7A"}, // LATIN SMALL LETTER DZ WITH CARON + {0x01C7, "\x4C\x4A"}, // LATIN CAPITAL LETTER LJ + {0x01C8, "\x4C\x6A"}, // LATIN CAPITAL LETTER L WITH SMALL LETTER J + {0x01C9, "\x6C\x6A"}, // LATIN SMALL LETTER LJ + {0x01CA, "\x4E\x4A"}, // LATIN CAPITAL LETTER NJ + {0x01CB, "\x4E\x6A"}, // LATIN CAPITAL LETTER N WITH SMALL LETTER J + {0x01CC, "\x6E\x6A"}, // LATIN SMALL LETTER NJ + {0x01CD, "\x41"}, // LATIN CAPITAL LETTER A WITH CARON + {0x01CE, "\x61"}, // LATIN SMALL LETTER A WITH CARON + {0x01CF, "\x49"}, // LATIN CAPITAL LETTER I WITH CARON + {0x01D0, "\x69"}, // LATIN SMALL LETTER I WITH CARON + {0x01D1, "\x4F"}, // LATIN CAPITAL LETTER O WITH CARON + {0x01D2, "\x6F"}, // LATIN SMALL LETTER O WITH CARON + {0x01D3, "\x55"}, // LATIN CAPITAL LETTER U WITH CARON + {0x01D4, "\x75"}, // LATIN SMALL LETTER U WITH CARON + {0x01D5, "\x55"}, // LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON + {0x01D6, "\x75"}, // LATIN SMALL LETTER U WITH DIAERESIS AND MACRON + {0x01D7, "\x55"}, // LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE + {0x01D8, "\x75"}, // LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE + {0x01D9, "\x55"}, // LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON + {0x01DA, "\x75"}, // LATIN SMALL LETTER U WITH DIAERESIS AND CARON + {0x01DB, "\x55"}, // LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE + {0x01DC, "\x75"}, // LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE + {0x01DE, "\x41"}, // LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON + {0x01DF, "\x61"}, // LATIN SMALL LETTER A WITH DIAERESIS AND MACRON + {0x01E0, "\x41"}, // LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON + {0x01E1, "\x61"}, // LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON + {0x01E6, "\x47"}, // LATIN CAPITAL LETTER G WITH CARON + {0x01E7, "\x67"}, // LATIN SMALL LETTER G WITH CARON + {0x01E8, "\x4B"}, // LATIN CAPITAL LETTER K WITH CARON + {0x01E9, "\x6B"}, // LATIN SMALL LETTER K WITH CARON + {0x01EA, "\x4F"}, // LATIN CAPITAL LETTER O WITH OGONEK + {0x01EB, "\x6F"}, // LATIN SMALL LETTER O WITH OGONEK + {0x01EC, "\x4F"}, // LATIN CAPITAL LETTER O WITH OGONEK AND MACRON + {0x01ED, "\x6F"}, // LATIN SMALL LETTER O WITH OGONEK AND MACRON + {0x01F0, "\x6A"}, // LATIN SMALL LETTER J WITH CARON + {0x01F1, "\x44\x5A"}, // LATIN CAPITAL LETTER DZ + {0x01F2, "\x44\x7A"}, // LATIN CAPITAL LETTER D WITH SMALL LETTER Z + {0x01F3, "\x64\x7A"}, // LATIN SMALL LETTER DZ + {0x01F4, "\x47"}, // LATIN CAPITAL LETTER G WITH ACUTE + {0x01F5, "\x67"}, // LATIN SMALL LETTER G WITH ACUTE + {0x01F8, "\x4E"}, // LATIN CAPITAL LETTER N WITH GRAVE + {0x01F9, "\x6E"}, // LATIN SMALL LETTER N WITH GRAVE + {0x01FA, "\x41"}, // LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE + {0x01FB, "\x61"}, // LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE + {0x0200, "\x41"}, // LATIN CAPITAL LETTER A WITH DOUBLE GRAVE + {0x0201, "\x61"}, // LATIN SMALL LETTER A WITH DOUBLE GRAVE + {0x0202, "\x41"}, // LATIN CAPITAL LETTER A WITH INVERTED BREVE + {0x0203, "\x61"}, // LATIN SMALL LETTER A WITH INVERTED BREVE + {0x0204, "\x45"}, // LATIN CAPITAL LETTER E WITH DOUBLE GRAVE + {0x0205, "\x65"}, // LATIN SMALL LETTER E WITH DOUBLE GRAVE + {0x0206, "\x45"}, // LATIN CAPITAL LETTER E WITH INVERTED BREVE + {0x0207, "\x65"}, // LATIN SMALL LETTER E WITH INVERTED BREVE + {0x0208, "\x49"}, // LATIN CAPITAL LETTER I WITH DOUBLE GRAVE + {0x0209, "\x69"}, // LATIN SMALL LETTER I WITH DOUBLE GRAVE + {0x020A, "\x49"}, // LATIN CAPITAL LETTER I WITH INVERTED BREVE + {0x020B, "\x69"}, // LATIN SMALL LETTER I WITH INVERTED BREVE + {0x020C, "\x4F"}, // LATIN CAPITAL LETTER O WITH DOUBLE GRAVE + {0x020D, "\x6F"}, // LATIN SMALL LETTER O WITH DOUBLE GRAVE + {0x020E, "\x4F"}, // LATIN CAPITAL LETTER O WITH INVERTED BREVE + {0x020F, "\x6F"}, // LATIN SMALL LETTER O WITH INVERTED BREVE + {0x0210, "\x52"}, // LATIN CAPITAL LETTER R WITH DOUBLE GRAVE + {0x0211, "\x72"}, // LATIN SMALL LETTER R WITH DOUBLE GRAVE + {0x0212, "\x52"}, // LATIN CAPITAL LETTER R WITH INVERTED BREVE + {0x0213, "\x72"}, // LATIN SMALL LETTER R WITH INVERTED BREVE + {0x0214, "\x55"}, // LATIN CAPITAL LETTER U WITH DOUBLE GRAVE + {0x0215, "\x75"}, // LATIN SMALL LETTER U WITH DOUBLE GRAVE + {0x0216, "\x55"}, // LATIN CAPITAL LETTER U WITH INVERTED BREVE + {0x0217, "\x75"}, // LATIN SMALL LETTER U WITH INVERTED BREVE + {0x0218, "\x53"}, // LATIN CAPITAL LETTER S WITH COMMA BELOW + {0x0219, "\x73"}, // LATIN SMALL LETTER S WITH COMMA BELOW + {0x021A, "\x54"}, // LATIN CAPITAL LETTER T WITH COMMA BELOW + {0x021B, "\x74"}, // LATIN SMALL LETTER T WITH COMMA BELOW + {0x021E, "\x48"}, // LATIN CAPITAL LETTER H WITH CARON + {0x021F, "\x68"}, // LATIN SMALL LETTER H WITH CARON + {0x0226, "\x41"}, // LATIN CAPITAL LETTER A WITH DOT ABOVE + {0x0227, "\x61"}, // LATIN SMALL LETTER A WITH DOT ABOVE + {0x0228, "\x45"}, // LATIN CAPITAL LETTER E WITH CEDILLA + {0x0229, "\x65"}, // LATIN SMALL LETTER E WITH CEDILLA + {0x022A, "\x4F"}, // LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON + {0x022B, "\x6F"}, // LATIN SMALL LETTER O WITH DIAERESIS AND MACRON + {0x022C, "\x4F"}, // LATIN CAPITAL LETTER O WITH TILDE AND MACRON + {0x022D, "\x6F"}, // LATIN SMALL LETTER O WITH TILDE AND MACRON + {0x022E, "\x4F"}, // LATIN CAPITAL LETTER O WITH DOT ABOVE + {0x022F, "\x6F"}, // LATIN SMALL LETTER O WITH DOT ABOVE + {0x0230, "\x4F"}, // LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON + {0x0231, "\x6F"}, // LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON + {0x0232, "\x59"}, // LATIN CAPITAL LETTER Y WITH MACRON + {0x0233, "\x79"}, // LATIN SMALL LETTER Y WITH MACRON + {0x0404, "\xD0\xAD"}, // CYRILLIC CAPITAL LETTER UKRAINIAN IE + {0x0405, "\xD0\xA1"}, // CYRILLIC CAPITAL LETTER DZE + {0x0406, "\xD0\x98"}, // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I + {0x0407, "\xD0\x98"}, // CYRILLIC CAPITAL LETTER YI + {0x0408, "\x4A"}, // CYRILLIC CAPITAL LETTER JE + {0x040E, "\xD0\xA3"}, // CYRILLIC CAPITAL LETTER SHORT U + {0x0454, "\xD1\x8D"}, // CYRILLIC SMALL LETTER UKRAINIAN IE + {0x0455, "\xD1\x81"}, // CYRILLIC SMALL LETTER DZE + {0x0456, "\xD0\xB8"}, // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + {0x0457, "\xD0\xB8"}, // CYRILLIC SMALL LETTER YI + {0x0458, "\x6A"}, // CYRILLIC SMALL LETTER JE + {0x045E, "\xD1\x83"}, // CYRILLIC SMALL LETTER SHORT U + {0x0490, "\xD0\x93"}, // CYRILLIC CAPITAL LETTER GHE WITH UPTURN + {0x0491, "\xD0\xB3"}, // CYRILLIC SMALL LETTER GHE WITH UPTURN + {0x2007, "\x20"}, // FIGURE SPACE + {0x2009, "\x20"}, // THIN SPACE + {0x200B, ""}, // ZERO WIDTH SPACE + {0x2010, "\x2D"}, // HYPHEN + {0x2011, "\x2D"}, // NON-BREAKING HYPHEN + {0x2012, "\x2D"}, // FIGURE DASH + {0x2013, "\x2D"}, // EN DASH + {0x2014, "\x2D"}, // EM DASH + {0x2015, "\x2D"}, // HORIZONTAL BAR + {0x2018, "\x27"}, // LEFT SINGLE QUOTATION MARK -> "'" + {0x2019, "\x27"}, // RIGHT SINGLE QUOTATION MARK -> "'" + {0x201A, "\x2C"}, // SINGLE LOW-9 QUOTATION MARK + {0x201B, "\x27"}, // SINGLE HIGH-REVERSED-9 QUOTATION MARK -> "'" + {0x201C, "\x22"}, // LEFT DOUBLE QUOTATION MARK + {0x201D, "\x22"}, // RIGHT DOUBLE QUOTATION MARK + {0x201E, "\x22"}, // DOUBLE LOW-9 QUOTATION MARK + {0x201F, "\x22"}, // DOUBLE HIGH-REVERSED-9 QUOTATION MARK + {0x2022, "\x2A"}, // BULLET + {0x2026, "\x2E\x2E\x2E"}, // HORIZONTAL ELLIPSIS + {0x2027, "\x2A"}, // HYPHENATION POINT + {0x2028, "\x20"}, // LINE SEPARATOR + {0x2029, "\x20"}, // PARAGRAPH SEPARATOR + {0x202F, "\x20"}, // NARROW NO-BREAK SPACE + {0x2032, "\x27"}, // PRIME -> "'" + {0x2033, "\x22"}, // DOUBLE PRIME + {0x2039, "\x3C"}, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + {0x203A, "\x3E"}, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + {0x20AC, "\x45\x55\x52"}, // EURO SIGN + {0x2122, "\x28\x74\x6D\x29"}, // TRADE MARK SIGN + {0x2190, "\x3C\x2D"}, // LEFTWARDS ARROW + {0x2191, "\x5E"}, // UPWARDS ARROW + {0x2192, "\x2D\x3E"}, // RIGHTWARDS ARROW + {0x2193, "\x76"}, // DOWNWARDS ARROW + {0x21D0, "\x3C\x3D"}, // LEFTWARDS DOUBLE ARROW + {0x21D2, "\x3D\x3E"}, // RIGHTWARDS DOUBLE ARROW + {0x2212, "\x2D"}, // MINUS SIGN + {0x221E, "\x69\x6E\x66"}, // INFINITY + {0x2260, "\x21\x3D"}, // NOT EQUAL TO +}; + +} // namespace + +const char *TranslitToKoi8(char32_t code_point) { + const auto *end = kTable + std::size(kTable); + const auto *it = std::lower_bound(kTable, end, code_point, + [](const TranslitEntry &entry, char32_t value) { return entry.code_point < value; }); + return (it != end && it->code_point == code_point) ? it->replacement : nullptr; +} + +} // namespace codepages + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/translit_koi8.h b/src/utils/translit_koi8.h new file mode 100644 index 000000000..c446dc88a --- /dev/null +++ b/src/utils/translit_koi8.h @@ -0,0 +1,20 @@ +/** +\file translit_koi8.h - a part of the Bylins engine. +\brief Closest KOI8-R equivalent for characters KOI8-R does not have (issue #3681). +*/ + +#ifndef BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ +#define BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ + +namespace codepages { + +// UTF-8 replacement for a code point KOI8-R cannot represent, or nullptr when there is no +// sensible one (the caller then falls back to its placeholder). Replacements may be longer +// than one character: "..." for an ellipsis, "(tm)" for a trademark sign. +const char *TranslitToKoi8(char32_t code_point); + +} // namespace codepages + +#endif // BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/utils_encoding.cpp b/src/utils/utils_encoding.cpp index 341bc5138..e842d685f 100644 --- a/src/utils/utils_encoding.cpp +++ b/src/utils/utils_encoding.cpp @@ -150,7 +150,10 @@ void utf8_to_koi(char *str_i, char *str_o) #else // HAVE_ICONV -#define KOI8_UNKNOWN_CHAR '+' // char to use when cannot represent unicode codepoint in KOI8-R +// Placeholder for a code point KOI8-R cannot represent AND for which translit_koi8 has no +// equivalent. '?' rather than the former '+': a plus is ordinary text in this game ("+5 to +// damage"), so it read as content rather than as a lost character (issue #3681). +#define KOI8_UNKNOWN_CHAR '?' // Simple implementation of UTF-8/KOI8-R converter, supports all codes available in KOI8-R void utf8_to_koi(char *str_i, char *str_o) { diff --git a/tests/meson.build b/tests/meson.build index 5a03b297c..b0e0d32e7 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -44,6 +44,7 @@ test_sources = files( 'utils.string.cpp', 'utils.encoding.cpp', 'accounts.storage.cpp', + 'translit_koi8.cpp', 'utf8.cpp', 'native_text.cpp', 'text_semantics.cpp', diff --git a/tests/translit_koi8.cpp b/tests/translit_koi8.cpp new file mode 100644 index 000000000..36f90ab28 --- /dev/null +++ b/tests/translit_koi8.cpp @@ -0,0 +1,110 @@ +// Unit tests for the KOI8-R transliteration table (src/utils/translit_koi8.*, issue #3681). +// +// Pure ASCII: every non-ASCII fixture is spelled as UTF-8 byte escapes, so the file means the +// same thing under either source encoding. The end-to-end expectations go through +// native_text::to_koi8, which only converts in the UTF-8 build, so they branch on +// native_text::native_is_utf8(). + +#include "utils/translit_koi8.h" +#include "utils/native_text.h" + +#include + +#include + +using codepages::TranslitToKoi8; + +namespace { + +// Convenience: the replacement as a std::string, or "" so a failure prints readably. +std::string Tr(char32_t cp) { + const char *const r = TranslitToKoi8(cp); + return r == nullptr ? std::string("") : std::string(r); +} + +} // namespace + +TEST(TranslitKoi8, TypographyBecomesPlainAscii) { + EXPECT_EQ(Tr(0x2014), "-"); // EM DASH + EXPECT_EQ(Tr(0x2013), "-"); // EN DASH + EXPECT_EQ(Tr(0x2026), "..."); // HORIZONTAL ELLIPSIS + EXPECT_EQ(Tr(0x00AB), "\""); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + EXPECT_EQ(Tr(0x00BB), "\""); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + EXPECT_EQ(Tr(0x201C), "\""); // LEFT DOUBLE QUOTATION MARK + EXPECT_EQ(Tr(0x2019), "'"); // RIGHT SINGLE QUOTATION MARK + EXPECT_EQ(Tr(0x2122), "(tm)"); // TRADE MARK SIGN + EXPECT_EQ(Tr(0x00AD), ""); // SOFT HYPHEN: drops out entirely + // A no-break space needs no replacement: KOI8-R has one of its own (0x9A). + EXPECT_EQ(TranslitToKoi8(0x00A0), nullptr); +} + +TEST(TranslitKoi8, AccentedLatinLosesItsAccent) { + EXPECT_EQ(Tr(0x00E9), "e"); // e with acute + EXPECT_EQ(Tr(0x00FC), "u"); // u with diaeresis + EXPECT_EQ(Tr(0x00C0), "A"); // A with grave: case is kept + EXPECT_EQ(Tr(0x0141), "L"); // L with stroke (no NFKD decomposition; entered by hand) +} + +TEST(TranslitKoi8, NonRussianCyrillicMapsToItsRussianNeighbour) { + EXPECT_EQ(Tr(0x0404), "\xD0\xAD"); // Ukrainian Ye -> Russian E + EXPECT_EQ(Tr(0x0456), "\xD0\xB8"); // Ukrainian i -> Russian i + EXPECT_EQ(Tr(0x0490), "\xD0\x93"); // Ukrainian Ghe with upturn -> Russian Ghe +} + +TEST(TranslitKoi8, WhatKoi8AlreadyHasIsNotTouched) { + // ASCII and the whole Russian alphabet are representable, so there is nothing to replace + // and the table must stay out of the way. + for (char32_t cp = 0x20; cp < 0x7F; ++cp) { + EXPECT_EQ(TranslitToKoi8(cp), nullptr) << "ASCII " << static_cast(cp); + } + for (char32_t cp = 0x0410; cp <= 0x044F; ++cp) { + EXPECT_EQ(TranslitToKoi8(cp), nullptr) << "Cyrillic U+" << static_cast(cp); + } + EXPECT_EQ(TranslitToKoi8(0x0401), nullptr); // Yo + EXPECT_EQ(TranslitToKoi8(0x0451), nullptr); // yo + EXPECT_EQ(TranslitToKoi8(0x2500), nullptr); // box drawing: KOI8-R has these +} + +TEST(TranslitKoi8, NoEquivalentYieldsNullptr) { + EXPECT_EQ(TranslitToKoi8(0x1F600), nullptr); // grinning face + EXPECT_EQ(TranslitToKoi8(0x4E00), nullptr); // CJK ideograph + EXPECT_EQ(TranslitToKoi8(0x05D0), nullptr); // Hebrew alef +} + +TEST(TranslitKoi8, TableIsSortedAndUnique) { + // The lookup is a binary search; a table out of order would silently miss entries. + char32_t previous = 0; + unsigned found = 0; + for (char32_t cp = 1; cp <= 0x2FFF; ++cp) { + if (TranslitToKoi8(cp) != nullptr) { + EXPECT_LT(previous, cp); + previous = cp; + ++found; + } + } + EXPECT_GT(found, 200u) << "the generated table looks truncated"; +} + +TEST(TranslitKoi8, ToKoi8AppliesTheTable) { + if (!native_text::native_is_utf8()) { + GTEST_SKIP() << "to_koi8 is a no-op when the native encoding already is KOI8-R"; + } + // "-- privet ..." with an em dash and an ellipsis around Cyrillic text. + const std::string source = + "\xE2\x80\x94" " \xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" "\xE2\x80\xA6"; + const std::string koi = native_text::to_koi8(source); + EXPECT_EQ(koi.front(), '-'); + EXPECT_EQ(koi.substr(koi.size() - 3), "..."); + // The Cyrillic in the middle survived as KOI8-R "privet". + EXPECT_EQ(koi, "- \xD0\xD2\xC9\xD7\xC5\xD4..."); +} + +TEST(TranslitKoi8, ToKoi8FallsBackToThePlaceholder) { + if (!native_text::native_is_utf8()) { + GTEST_SKIP() << "to_koi8 is a no-op when the native encoding already is KOI8-R"; + } + // An emoji has no KOI8-R equivalent at all, so it becomes the single placeholder character. + EXPECT_EQ(native_text::to_koi8("a\xF0\x9F\x98\x80" "b"), "a?b"); +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/utils.encoding.cpp b/tests/utils.encoding.cpp index 871643815..84e28dc7c 100644 --- a/tests/utils.encoding.cpp +++ b/tests/utils.encoding.cpp @@ -119,11 +119,13 @@ TEST(Utils_Encoding, Utf8ToKoi_YoLowercase) TEST(Utils_Encoding, Utf8ToKoi_UnknownCharReplacement) { - // Characters not in KOI8-R should be replaced with KOI8_UNKNOWN_CHAR (+) + // Characters not in KOI8-R should be replaced with KOI8_UNKNOWN_CHAR (?) // For example, Euro sign: U+20AC = UTF-8 E2 82 AC + // (This is the raw converter. native_text::to_koi8 runs the transliteration table first, + // so through that path the euro sign becomes "EUR" and never reaches the placeholder.) std::string utf8_euro = "\xE2\x82\xAC"; std::string koi_euro = Utf8ToKoi(utf8_euro.c_str()); - EXPECT_EQ("+", koi_euro); + EXPECT_EQ("?", koi_euro); } TEST(Utils_Encoding, Utf8ToKoi_BoxDrawingChars) From 8bf64874c05a78cb27c5105b5b9d8f6be91c9e95 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:05:38 +0200 Subject: [PATCH 033/110] =?UTF-8?q?chore(version):=20=D1=81=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=83=D1=82=D1=8C=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8,=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20--=20=D0=B2=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D1=83=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8?= =?UTF-8?q?=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=B0=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/version.cpp.in | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/version.cpp.in b/src/version.cpp.in index a3b268279..97a4b8237 100644 --- a/src/version.cpp.in +++ b/src/version.cpp.in @@ -30,17 +30,17 @@ static const char *InternalEncodingName() { void ShowBuildInfo(CharData *ch) { SendMsgToChar(ch, "%s %s, build from %s, revision %s\n", engine_name, engine_version, build_datetime, revision); - SendMsgToChar(ch, "Internal encoding: %s\n", InternalEncodingName()); SendMsgToChar(ch, "Based on CircleMUD, version 3.00 beta patchlevel 16\n"); if (privilege::IsImmortal(ch)) { - SendMsgToChar(ch, "Compiler: %s\nEnabled features: %s\n", build_compiler, build_features); + SendMsgToChar(ch, "Compiler: %s; internal encoding: %s\nEnabled features: %s\n", + build_compiler, InternalEncodingName(), build_features); } } void LogBuildInfo() { - log("%s %s, build from %s, revision: %s\r\nInternal encoding: %s\r\nCompiler: %s\r\nEnabled features: %s", - engine_name, engine_version, build_datetime, revision, InternalEncodingName(), - build_compiler, build_features); + log("%s %s, build from %s, revision: %s\r\nCompiler: %s; internal encoding: %s\r\nEnabled features: %s", + engine_name, engine_version, build_datetime, revision, + build_compiler, InternalEncodingName(), build_features); } // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From eda833d47dbbeb343f094b3476fdd7887d54e4f0 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:15:51 +0200 Subject: [PATCH 034/110] =?UTF-8?q?feat(utf8):=20=D0=BD=D0=B0=D1=80=D1=8F?= =?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BB=D1=8F=20UTF-8-=D0=BA=D0=BB=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шапка входа в игру осталась такой же, какой была во времена, когда весь текст жил в одном байте: рамка из дефисов, кавычки-палочки, дефис вместо тире. Под UTF-8 движок может отдать её нормально -- скруглённая рамка, типографское тире, кавычки-лапки, сплошные блоки в логотипе. Текст лежит отдельным файлом lib/text/greeting.utf8, а не в system_msg.xml, потому что XML пока в KOI8-R и половину этих символов просто не вмещает. Файл читается сырым и уходит клиенту байт в байт -- поэтому показывается только при сочетании "движок нативно в UTF-8" + "клиент выбрал UTF-8": лишь тогда текст не проходит через перекодировку. Во всех остальных случаях отдаётся прежняя шапка, так что для игроков на alt/win/koi8 ничего не меняется. Файл необязателен: если его нет или в нём битый UTF-8 -- в лог уходит SYSERR и берётся прежняя шапка. --- .gitattributes | 1 + lib/greeting.utf8 | 22 ++++++++++++++ src/engine/boot/boot_constants.h | 4 +++ src/engine/ui/login.cpp | 52 +++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 lib/greeting.utf8 diff --git a/.gitattributes b/.gitattributes index 4a6f9d103..ab544fed0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,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/greeting.utf8 working-tree-encoding=UTF-8 eol=lf diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 new file mode 100644 index 000000000..0d13f7a22 --- /dev/null +++ b/lib/greeting.utf8 @@ -0,0 +1,22 @@ + +╭──────────────────────────────────────────────────────────────────────────╮ +│ Based on CircleMUD v3.0 — created by Jeremy Elson │ +│ DikuMUD Gamma 0.0 — Sebastian Hammer, Michael Seifert, │ +│ Hans Henrik Stærfeldt, Tom Madsen, Katja Nyboe │ +╰──────────────────────────────────────────────────────────────────────────╯ + + ███████ + ██ + ██ ████ ████ ████ ████ ████ ████ ████ ████ ████ + ███████ ██████ ██ ██ ██ ██ ████ ██ ██ ██████ ██ + ██ ██ ██ ██ ██ ██ ██ ████ ██ ████████ ██ ██ ██ + ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ + ███████ ███████ ████ ███ ███ ████ ████ ████ ████ ███████ ████ + + ✦ ПО ПРЕДАНИЯМ РУССКИХ НАРОДНЫХ СКАЗОК И БЫЛИН ✦ + + По административным вопросам — Стрибог ‹stribog@bylins.su› + Сайт: www.bylins.su + Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики + +Введите имя персонажа (или «новый», чтобы создать нового): \ No newline at end of file diff --git a/src/engine/boot/boot_constants.h b/src/engine/boot/boot_constants.h index 69873f5e3..c5ad8280d 100644 --- a/src/engine/boot/boot_constants.h +++ b/src/engine/boot/boot_constants.h @@ -108,6 +108,10 @@ enum SetStuffMode { #define PLAYER_Z_PREFIX LIB_PLRS "" LIB_Z #define HELP_PAGE_FILE LIB_HELP "screen" // for HELP +// Нарядный экран входа, уходит клиенту байт в байт (issue #3681). Необязателен: если файла нет +// или в нём битый UTF-8 -- берётся обычная шапка из system_msg.xml. Лежит в корне lib: каталог +// text/ в мастере разобран, а этот файл ни к справке, ни к конфигам не относится. +#define GREETING_UTF8_FILE "greeting.utf8" // issue.daily-quest: daily_quest.xml перенесён в cfg/quests и грузится через CfgManager. diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index 3dd999e3f..2399e5a08 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -6,6 +6,11 @@ #include "interpreter.h" #include "utils/russian_keys.h" #include "utils/native_text.h" +#include "utils/utf8.h" +#include "engine/boot/boot_constants.h" + +#include +#include #include "engine/ui/system_messages.h" #include "engine/core/config.h" #include "gameplay/mechanics/condition.h" @@ -1537,6 +1542,51 @@ static void HandleInit(DescriptorData *d, char * /*argument*/) { return; } +// Экран приветствия в нарядном виде: рамка со скругленными углами, типографское тире, кавычки +// с "лапками". Всё это символы, которых в KOI8-R попросту нет, поэтому файл лежит отдельно от +// system_msg.xml (тот пока в KOI8-R) и уходит клиенту байт в байт (issue #3681). +// +// Показываем его только когда движок работает нативно в UTF-8 И клиент выбрал UTF-8: лишь в +// этом сочетании текст не проходит через перекодировку и доезжает целым. Во всех остальных +// случаях (движок под KOI8-R, клиент в alt/win/koi8) отдаём прежнюю шапку из system_msg.xml. +static const std::string &GetGreeting(DescriptorData *d) { + static std::string fancy; + static bool fancy_loaded = false; + if (!fancy_loaded) { + fancy_loaded = true; + if (native_text::native_is_utf8()) { + std::ifstream in(GREETING_UTF8_FILE, std::ios::binary); + if (in) { + std::ostringstream body; + body << in.rdbuf(); + // В файле переводы строк обычные, а выводу нужен CRLF -- ровно та же + // нормализация, что делает загрузчик system_msg.xml. + for (const char c : body.str()) { + if (c == '\r') { + continue; + } + if (c == '\n') { + fancy += "\r\n"; + } else { + fancy += c; + } + } + // Файл читается сырым, без перекодировки, поэтому битый UTF-8 доехал бы до + // клиента как есть. Дешевле проверить один раз здесь, чем ловить это в игре. + if (!utf8::is_valid(fancy)) { + log("SYSERR: %s is not valid UTF-8, falling back to the plain greeting", + GREETING_UTF8_FILE); + fancy.clear(); + } + } + } + } + if (!fancy.empty() && d->keytable == kCodePageUTF8) { + return fancy; + } + return system_messages::GetText(system_messages::ESystemMsg::kGreetings); +} + static void HandleGetKeytable(DescriptorData *d, char *argument) { if (strlen(argument) > 0) argument[0] = argument[strlen(argument) - 1]; @@ -1550,7 +1600,7 @@ static void HandleGetKeytable(DescriptorData *d, char *argument) { } d->keytable = (ubyte) *argument - (ubyte) '0'; ip_log(d->host); - iosystem::write_to_output(system_messages::GetText(system_messages::ESystemMsg::kGreetings).c_str(), d); + iosystem::write_to_output(GetGreeting(d).c_str(), d); d->state = EConState::kGetName; return; } From 453c918e2373c5d7d140701913239aaa8cbca197 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:39:03 +0200 Subject: [PATCH 035/110] =?UTF-8?q?fix(utf8):=20=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D1=83=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Файлы справки лежат на диске в KOI8-R, а DataFile::get_one_line отдавал строки как есть -- под UTF-8-движком в память попадали байты чужой кодировки. Из-за этого разделы не находились (ключ раздела не совпадал с тем, что ввёл игрок, приведённым к нативной кодировке), а тело найденного раздела уезжало клиенту мусором. Строка теперь проходит через native_text::from_disk_line. Под UTF-8 она при этом может вырасти (байт KOI8-R разворачивается максимум в три байта), поэтому get_one_line принимает размер буфера, а буферы у вызывающего с тройным запасом. Заодно перевод строки снимается только если он там есть: прежний код рубил последний символ безусловно и терял его на строках без завершающего '\n'. --- src/engine/boot/boot_data_files.cpp | 37 ++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/engine/boot/boot_data_files.cpp b/src/engine/boot/boot_data_files.cpp index 15f4ee6d5..65f0dc22e 100644 --- a/src/engine/boot/boot_data_files.cpp +++ b/src/engine/boot/boot_data_files.cpp @@ -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; @@ -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 { @@ -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 != '#') { // если вдруг файл внезапно закончился и '#' так и не встретился // логаем ошибку и заканчиваем парсинг во избежание зацикливания @@ -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; @@ -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; From 4a6f238ad684e3be4910239680019aaba123e627 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:45:41 +0200 Subject: [PATCH 036/110] =?UTF-8?q?feat(utf8):=20=D0=BE=D0=B4=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=88=D0=B0=D0=BF=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?,=20=D0=B4=D0=B5=D0=B3=D1=80=D0=B0=D0=B4=D0=B8=D1=80=D1=83?= =?UTF-8?q?=D0=B5=D1=82=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80=D1=8C=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Было две шапки: нарядная для UTF-8-клиента и старая для всех остальных. Теперь одна -- lib/text/greeting.utf8, написанная полной палитрой. Клиенту в alt/win/ koi8 (и всей KOI8-R-сборке) она доезжает уже приведённой к KOI8-R: скруглённые углы рамки становятся обычными, тире -- дефисом, кавычки-лапки -- палочками. Проверка кодировки клиента из login.cpp ушла: приведением занимается граница вывода, которая и так через to_koi8 проходит. Чтобы деградация была осмысленной, а не сплошной заглушкой, словарь дополнен псевдографикой (140 записей): скруглённые и жирные рамки сводятся к обычным, дробные блоки -- к целым, звёздочки -- к '*'. Заодно закреплён инвариант "замена не длиннее исходного символа в байтах": вызывающие рассчитывают, что перевод в KOI8-R строку не удлиняет, а четыре записи его нарушали ('(tm)', '(R)', 'GBP', 'JPY'). Инвариант проверяется тестом по всей таблице. Сама перекодировка вынесена в codepages::Utf8ToKoi8 -- одна реализация на обе сборки вместо копии в каждой ветке #ifdef. --- src/engine/ui/login.cpp | 57 ++++++----- src/utils/native_text.cpp | 34 ++----- src/utils/native_text.h | 12 ++- src/utils/translit_koi8.cpp | 187 ++++++++++++++++++++++++++++++++++-- src/utils/translit_koi8.h | 8 ++ tests/translit_koi8.cpp | 37 ++++++- 6 files changed, 271 insertions(+), 64 deletions(-) diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index 2399e5a08..b05f94091 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -1542,47 +1542,46 @@ static void HandleInit(DescriptorData *d, char * /*argument*/) { return; } -// Экран приветствия в нарядном виде: рамка со скругленными углами, типографское тире, кавычки -// с "лапками". Всё это символы, которых в KOI8-R попросту нет, поэтому файл лежит отдельно от -// system_msg.xml (тот пока в KOI8-R) и уходит клиенту байт в байт (issue #3681). +// Экран приветствия. Файл lib/text/greeting.utf8 всегда в UTF-8 и написан полной палитрой: +// скруглённая рамка, типографское тире, кавычки-лапки. Он один на все кодировки -- клиенту в +// alt/win/koi8 (и всему KOI8-R-сборке целиком) то же самое доезжает уже приведённым к KOI8-R, +// где скруглённые углы становятся обычными, тире -- дефисом, лапки -- палочками (issue #3681). // -// Показываем его только когда движок работает нативно в UTF-8 И клиент выбрал UTF-8: лишь в -// этом сочетании текст не проходит через перекодировку и доезжает целым. Во всех остальных -// случаях (движок под KOI8-R, клиент в alt/win/koi8) отдаём прежнюю шапку из system_msg.xml. -static const std::string &GetGreeting(DescriptorData *d) { - static std::string fancy; - static bool fancy_loaded = false; - if (!fancy_loaded) { - fancy_loaded = true; - if (native_text::native_is_utf8()) { - std::ifstream in(GREETING_UTF8_FILE, std::ios::binary); - if (in) { - std::ostringstream body; - body << in.rdbuf(); +// Файл необязателен: нет его или в нём битый UTF-8 -- берётся прежняя шапка из system_msg.xml. +static const std::string &GetGreeting() { + static std::string greeting; + static bool loaded = false; + if (!loaded) { + loaded = true; + std::ifstream in(GREETING_UTF8_FILE, std::ios::binary); + if (in) { + std::ostringstream body; + body << in.rdbuf(); + const std::string raw = body.str(); + if (utf8::is_valid(raw)) { // В файле переводы строк обычные, а выводу нужен CRLF -- ровно та же // нормализация, что делает загрузчик system_msg.xml. - for (const char c : body.str()) { + std::string crlf; + crlf.reserve(raw.size() + raw.size() / 32); + for (const char c : raw) { if (c == '\r') { continue; } if (c == '\n') { - fancy += "\r\n"; + crlf += "\r\n"; } else { - fancy += c; + crlf += c; } } - // Файл читается сырым, без перекодировки, поэтому битый UTF-8 доехал бы до - // клиента как есть. Дешевле проверить один раз здесь, чем ловить это в игре. - if (!utf8::is_valid(fancy)) { - log("SYSERR: %s is not valid UTF-8, falling back to the plain greeting", - GREETING_UTF8_FILE); - fancy.clear(); - } + greeting = native_text::from_utf8(crlf); + } else { + log("SYSERR: %s is not valid UTF-8, falling back to the plain greeting", + GREETING_UTF8_FILE); } } } - if (!fancy.empty() && d->keytable == kCodePageUTF8) { - return fancy; + if (!greeting.empty()) { + return greeting; } return system_messages::GetText(system_messages::ESystemMsg::kGreetings); } @@ -1600,7 +1599,7 @@ static void HandleGetKeytable(DescriptorData *d, char *argument) { } d->keytable = (ubyte) *argument - (ubyte) '0'; ip_log(d->host); - iosystem::write_to_output(GetGreeting(d).c_str(), d); + iosystem::write_to_output(GetGreeting().c_str(), d); d->state = EConState::kGetName; return; } diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 113c6d6fd..08b01f2e8 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -424,31 +424,11 @@ std::string to_koi8(const std::string &text) { if (!has_high_byte) { return text; // pure ASCII is spelled identically in both encodings } - // Characters KOI8-R does not have are first reduced to the closest thing it does have - // (a typographic dash to "-", curly quotes to '"', an accented letter to its base one); - // only what has no equivalent reaches the converter's placeholder. Doing it here, before - // the conversion, is what allows a replacement to be longer than one character. - std::string reduced; - reduced.reserve(text.size()); - for (std::size_t pos = 0; pos < text.size();) { - char32_t cp = 0; - const std::size_t len = utf8::decode(text, pos, cp); - if (len == 0) { - break; - } - const char *const replacement = codepages::TranslitToKoi8(cp); - if (replacement != nullptr) { - reduced.append(replacement); - } else { - reduced.append(text, pos, len); - } - pos += len; - } + return codepages::Utf8ToKoi8(text); +} - // KOI8-R is never longer than the UTF-8 it came from, so the input size is a safe bound. - std::vector out(reduced.size() + 1, '\0'); - codepages::utf8_to_koi(const_cast(reduced.c_str()), out.data()); - return std::string(out.data()); +std::string from_utf8(const std::string &text) { + return text; // the native encoding already is UTF-8 } std::string translit_to_filename(std::string_view name) { @@ -658,6 +638,12 @@ std::string to_koi8(const std::string &text) { return text; // the native encoding already is KOI8-R } +std::string from_utf8(const std::string &text) { + // The native encoding is KOI8-R, so this is the real conversion: reduce what KOI8-R lacks + // (see translit_koi8.h), then transcode. + return codepages::Utf8ToKoi8(text); +} + std::string translit_to_filename(std::string_view name) { // Byte-wise, exactly as get_filename() did before this was extracted: transliterate through // the Latin table, then lowercase. Yo is not in that table and had its own special case. diff --git a/src/utils/native_text.h b/src/utils/native_text.h index fb15a72e3..9397e5a2a 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -152,10 +152,18 @@ std::string from_koi8(const std::string &text); // The inverse: take native text down to KOI8-R. Needed wherever something downstream is defined // in terms of KOI8-R bytes -- the legacy client code pages are KOI8-R -> target byte tables, and -// the on-disk formats are KOI8-R. Identity under KOI8-R. Characters absent from KOI8-R are -// replaced by the converter (it substitutes '+'), which is unavoidable for a narrower encoding. +// the on-disk formats are KOI8-R. Identity under KOI8-R. A character KOI8-R does not have is +// first reduced to the closest one it does (see translit_koi8.h): a typographic dash to "-", a +// rounded frame corner to a square one, an accented letter to its base. Only what has no +// equivalent at all becomes the converter's placeholder. std::string to_koi8(const std::string &text); +// Bring text that is UTF-8 on disk into the native encoding. The counterpart of from_koi8 for +// the files that are deliberately kept in UTF-8 rather than KOI8-R (the login screen). Identity +// under UTF-8; under KOI8-R it goes through the same reduction as to_koi8, so a file written +// with the full Unicode repertoire still renders sensibly on a KOI8-R build. +std::string from_utf8(const std::string &text); + // Read a data file (all of which are stored in KOI8-R) and hand back its contents in the // engine's native encoding. The counterpart of from_koi8 for whole files: parsers that take a // buffer should go through this instead of reading the path themselves, so the boundary stays diff --git a/src/utils/translit_koi8.cpp b/src/utils/translit_koi8.cpp index dcc9fc635..439ee717f 100644 --- a/src/utils/translit_koi8.cpp +++ b/src/utils/translit_koi8.cpp @@ -17,7 +17,11 @@ conversion, and are spelled as \xNN escapes so they do not depend on this file's #include "translit_koi8.h" +#include "utf8.h" +#include "utils_encoding.h" + #include +#include namespace codepages { @@ -31,11 +35,11 @@ struct TranslitEntry { // Sorted by code point: looked up with a binary search. constexpr TranslitEntry kTable[] = { {0x00A2, "\x63"}, // CENT SIGN - {0x00A3, "\x47\x42\x50"}, // POUND SIGN - {0x00A5, "\x4A\x50\x59"}, // YEN SIGN + {0x00A3, "\x4C"}, // POUND SIGN + {0x00A5, "\x59"}, // YEN SIGN {0x00AB, "\x22"}, // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK {0x00AD, ""}, // SOFT HYPHEN - {0x00AE, "\x28\x52\x29"}, // REGISTERED SIGN + {0x00AE, "\x52"}, // REGISTERED SIGN {0x00BB, "\x22"}, // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK {0x00C0, "\x41"}, // LATIN CAPITAL LETTER A WITH GRAVE {0x00C1, "\x41"}, // LATIN CAPITAL LETTER A WITH ACUTE @@ -334,26 +338,28 @@ constexpr TranslitEntry kTable[] = { {0x2013, "\x2D"}, // EN DASH {0x2014, "\x2D"}, // EM DASH {0x2015, "\x2D"}, // HORIZONTAL BAR - {0x2018, "\x27"}, // LEFT SINGLE QUOTATION MARK -> "'" - {0x2019, "\x27"}, // RIGHT SINGLE QUOTATION MARK -> "'" + {0x2018, "\x27"}, // LEFT SINGLE QUOTATION MARK + {0x2019, "\x27"}, // RIGHT SINGLE QUOTATION MARK {0x201A, "\x2C"}, // SINGLE LOW-9 QUOTATION MARK - {0x201B, "\x27"}, // SINGLE HIGH-REVERSED-9 QUOTATION MARK -> "'" + {0x201B, "\x27"}, // SINGLE HIGH-REVERSED-9 QUOTATION MARK {0x201C, "\x22"}, // LEFT DOUBLE QUOTATION MARK {0x201D, "\x22"}, // RIGHT DOUBLE QUOTATION MARK {0x201E, "\x22"}, // DOUBLE LOW-9 QUOTATION MARK {0x201F, "\x22"}, // DOUBLE HIGH-REVERSED-9 QUOTATION MARK {0x2022, "\x2A"}, // BULLET + {0x2023, "\x3E"}, // TRIANGULAR BULLET {0x2026, "\x2E\x2E\x2E"}, // HORIZONTAL ELLIPSIS {0x2027, "\x2A"}, // HYPHENATION POINT {0x2028, "\x20"}, // LINE SEPARATOR {0x2029, "\x20"}, // PARAGRAPH SEPARATOR {0x202F, "\x20"}, // NARROW NO-BREAK SPACE - {0x2032, "\x27"}, // PRIME -> "'" + {0x2032, "\x27"}, // PRIME {0x2033, "\x22"}, // DOUBLE PRIME {0x2039, "\x3C"}, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK {0x203A, "\x3E"}, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + {0x2043, "\x2D"}, // HYPHEN BULLET {0x20AC, "\x45\x55\x52"}, // EURO SIGN - {0x2122, "\x28\x74\x6D\x29"}, // TRADE MARK SIGN + {0x2122, "\x74\x6D"}, // TRADE MARK SIGN {0x2190, "\x3C\x2D"}, // LEFTWARDS ARROW {0x2191, "\x5E"}, // UPWARDS ARROW {0x2192, "\x2D\x3E"}, // RIGHTWARDS ARROW @@ -363,6 +369,144 @@ constexpr TranslitEntry kTable[] = { {0x2212, "\x2D"}, // MINUS SIGN {0x221E, "\x69\x6E\x66"}, // INFINITY {0x2260, "\x21\x3D"}, // NOT EQUAL TO + {0x2501, "\xE2\x94\x80"}, // BOX DRAWINGS HEAVY HORIZONTAL + {0x2503, "\xE2\x94\x82"}, // BOX DRAWINGS HEAVY VERTICAL + {0x2504, "\xE2\x94\x80"}, // BOX DRAWINGS LIGHT TRIPLE DASH HORIZONTAL + {0x2505, "\xE2\x94\x80"}, // BOX DRAWINGS HEAVY TRIPLE DASH HORIZONTAL + {0x2506, "\xE2\x94\x82"}, // BOX DRAWINGS LIGHT TRIPLE DASH VERTICAL + {0x2507, "\xE2\x94\x82"}, // BOX DRAWINGS HEAVY TRIPLE DASH VERTICAL + {0x2508, "\xE2\x94\x80"}, // BOX DRAWINGS LIGHT QUADRUPLE DASH HORIZONTAL + {0x2509, "\xE2\x94\x80"}, // BOX DRAWINGS HEAVY QUADRUPLE DASH HORIZONTAL + {0x250A, "\xE2\x94\x82"}, // BOX DRAWINGS LIGHT QUADRUPLE DASH VERTICAL + {0x250B, "\xE2\x94\x82"}, // BOX DRAWINGS HEAVY QUADRUPLE DASH VERTICAL + {0x250D, "\x2B"}, // BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY + {0x250E, "\x2B"}, // BOX DRAWINGS DOWN HEAVY AND RIGHT LIGHT + {0x250F, "\xE2\x94\x8C"}, // BOX DRAWINGS HEAVY DOWN AND RIGHT + {0x2511, "\x2B"}, // BOX DRAWINGS DOWN LIGHT AND LEFT HEAVY + {0x2512, "\x2B"}, // BOX DRAWINGS DOWN HEAVY AND LEFT LIGHT + {0x2513, "\xE2\x94\x90"}, // BOX DRAWINGS HEAVY DOWN AND LEFT + {0x2515, "\x2B"}, // BOX DRAWINGS UP LIGHT AND RIGHT HEAVY + {0x2516, "\x2B"}, // BOX DRAWINGS UP HEAVY AND RIGHT LIGHT + {0x2517, "\xE2\x94\x94"}, // BOX DRAWINGS HEAVY UP AND RIGHT + {0x2519, "\x2B"}, // BOX DRAWINGS UP LIGHT AND LEFT HEAVY + {0x251A, "\x2B"}, // BOX DRAWINGS UP HEAVY AND LEFT LIGHT + {0x251B, "\xE2\x94\x98"}, // BOX DRAWINGS HEAVY UP AND LEFT + {0x251D, "\x7C"}, // BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY + {0x251E, "\x2B"}, // BOX DRAWINGS UP HEAVY AND RIGHT DOWN LIGHT + {0x251F, "\x2B"}, // BOX DRAWINGS DOWN HEAVY AND RIGHT UP LIGHT + {0x2520, "\x7C"}, // BOX DRAWINGS VERTICAL HEAVY AND RIGHT LIGHT + {0x2521, "\x2B"}, // BOX DRAWINGS DOWN LIGHT AND RIGHT UP HEAVY + {0x2522, "\x2B"}, // BOX DRAWINGS UP LIGHT AND RIGHT DOWN HEAVY + {0x2523, "\xE2\x94\x9C"}, // BOX DRAWINGS HEAVY VERTICAL AND RIGHT + {0x2525, "\x7C"}, // BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY + {0x2526, "\x2B"}, // BOX DRAWINGS UP HEAVY AND LEFT DOWN LIGHT + {0x2527, "\x2B"}, // BOX DRAWINGS DOWN HEAVY AND LEFT UP LIGHT + {0x2528, "\x7C"}, // BOX DRAWINGS VERTICAL HEAVY AND LEFT LIGHT + {0x2529, "\x2B"}, // BOX DRAWINGS DOWN LIGHT AND LEFT UP HEAVY + {0x252A, "\x2B"}, // BOX DRAWINGS UP LIGHT AND LEFT DOWN HEAVY + {0x252B, "\xE2\x94\xA4"}, // BOX DRAWINGS HEAVY VERTICAL AND LEFT + {0x252D, "\x2B"}, // BOX DRAWINGS LEFT HEAVY AND RIGHT DOWN LIGHT + {0x252E, "\x2B"}, // BOX DRAWINGS RIGHT HEAVY AND LEFT DOWN LIGHT + {0x252F, "\x2D"}, // BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY + {0x2530, "\x2D"}, // BOX DRAWINGS DOWN HEAVY AND HORIZONTAL LIGHT + {0x2531, "\x2B"}, // BOX DRAWINGS RIGHT LIGHT AND LEFT DOWN HEAVY + {0x2532, "\x2B"}, // BOX DRAWINGS LEFT LIGHT AND RIGHT DOWN HEAVY + {0x2533, "\xE2\x94\xAC"}, // BOX DRAWINGS HEAVY DOWN AND HORIZONTAL + {0x2535, "\x2B"}, // BOX DRAWINGS LEFT HEAVY AND RIGHT UP LIGHT + {0x2536, "\x2B"}, // BOX DRAWINGS RIGHT HEAVY AND LEFT UP LIGHT + {0x2537, "\x2D"}, // BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY + {0x2538, "\x2D"}, // BOX DRAWINGS UP HEAVY AND HORIZONTAL LIGHT + {0x2539, "\x2B"}, // BOX DRAWINGS RIGHT LIGHT AND LEFT UP HEAVY + {0x253A, "\x2B"}, // BOX DRAWINGS LEFT LIGHT AND RIGHT UP HEAVY + {0x253B, "\xE2\x94\xB4"}, // BOX DRAWINGS HEAVY UP AND HORIZONTAL + {0x253D, "\x7C"}, // BOX DRAWINGS LEFT HEAVY AND RIGHT VERTICAL LIGHT + {0x253E, "\x7C"}, // BOX DRAWINGS RIGHT HEAVY AND LEFT VERTICAL LIGHT + {0x253F, "\x7C"}, // BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY + {0x2540, "\x2D"}, // BOX DRAWINGS UP HEAVY AND DOWN HORIZONTAL LIGHT + {0x2541, "\x2D"}, // BOX DRAWINGS DOWN HEAVY AND UP HORIZONTAL LIGHT + {0x2542, "\x7C"}, // BOX DRAWINGS VERTICAL HEAVY AND HORIZONTAL LIGHT + {0x2543, "\x2B"}, // BOX DRAWINGS LEFT UP HEAVY AND RIGHT DOWN LIGHT + {0x2544, "\x2B"}, // BOX DRAWINGS RIGHT UP HEAVY AND LEFT DOWN LIGHT + {0x2545, "\x2B"}, // BOX DRAWINGS LEFT DOWN HEAVY AND RIGHT UP LIGHT + {0x2546, "\x2B"}, // BOX DRAWINGS RIGHT DOWN HEAVY AND LEFT UP LIGHT + {0x2547, "\x2D"}, // BOX DRAWINGS DOWN LIGHT AND UP HORIZONTAL HEAVY + {0x2548, "\x2D"}, // BOX DRAWINGS UP LIGHT AND DOWN HORIZONTAL HEAVY + {0x2549, "\x7C"}, // BOX DRAWINGS RIGHT LIGHT AND LEFT VERTICAL HEAVY + {0x254A, "\x7C"}, // BOX DRAWINGS LEFT LIGHT AND RIGHT VERTICAL HEAVY + {0x254B, "\xE2\x94\xBC"}, // BOX DRAWINGS HEAVY VERTICAL AND HORIZONTAL + {0x254C, "\xE2\x94\x80"}, // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL + {0x254D, "\xE2\x94\x80"}, // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL + {0x254E, "\xE2\x94\x82"}, // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL + {0x254F, "\xE2\x94\x82"}, // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL + {0x256D, "\xE2\x94\x8C"}, // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT + {0x256E, "\xE2\x94\x90"}, // BOX DRAWINGS LIGHT ARC DOWN AND LEFT + {0x256F, "\xE2\x94\x98"}, // BOX DRAWINGS LIGHT ARC UP AND LEFT + {0x2570, "\xE2\x94\x94"}, // BOX DRAWINGS LIGHT ARC UP AND RIGHT + {0x2571, "\x2B"}, // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT + {0x2572, "\x2B"}, // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT + {0x2573, "\x2B"}, // BOX DRAWINGS LIGHT DIAGONAL CROSS + {0x2574, "\x2B"}, // BOX DRAWINGS LIGHT LEFT + {0x2575, "\x2B"}, // BOX DRAWINGS LIGHT UP + {0x2576, "\x2B"}, // BOX DRAWINGS LIGHT RIGHT + {0x2577, "\x2B"}, // BOX DRAWINGS LIGHT DOWN + {0x2578, "\x2B"}, // BOX DRAWINGS HEAVY LEFT + {0x2579, "\x2B"}, // BOX DRAWINGS HEAVY UP + {0x257A, "\x2B"}, // BOX DRAWINGS HEAVY RIGHT + {0x257B, "\x2B"}, // BOX DRAWINGS HEAVY DOWN + {0x257C, "\x2B"}, // BOX DRAWINGS LIGHT LEFT AND HEAVY RIGHT + {0x257D, "\x2B"}, // BOX DRAWINGS LIGHT UP AND HEAVY DOWN + {0x257E, "\x2B"}, // BOX DRAWINGS HEAVY LEFT AND LIGHT RIGHT + {0x257F, "\x2B"}, // BOX DRAWINGS HEAVY UP AND LIGHT DOWN + {0x2581, "\xE2\x96\x84"}, // LOWER ONE EIGHTH BLOCK + {0x2582, "\xE2\x96\x84"}, // LOWER ONE QUARTER BLOCK + {0x2583, "\xE2\x96\x84"}, // LOWER THREE EIGHTHS BLOCK + {0x2585, "\xE2\x96\x84"}, // LOWER FIVE EIGHTHS BLOCK + {0x2586, "\xE2\x96\x84"}, // LOWER THREE QUARTERS BLOCK + {0x2587, "\xE2\x96\x84"}, // LOWER SEVEN EIGHTHS BLOCK + {0x2589, "\xE2\x96\x88"}, // LEFT SEVEN EIGHTHS BLOCK + {0x258A, "\xE2\x96\x88"}, // LEFT THREE QUARTERS BLOCK + {0x258B, "\xE2\x96\x88"}, // LEFT FIVE EIGHTHS BLOCK + {0x258D, "\xE2\x96\x8C"}, // LEFT THREE EIGHTHS BLOCK + {0x258E, "\xE2\x96\x8C"}, // LEFT ONE QUARTER BLOCK + {0x258F, "\xE2\x96\x8C"}, // LEFT ONE EIGHTH BLOCK + {0x2594, "\xE2\x96\x80"}, // UPPER ONE EIGHTH BLOCK + {0x2595, "\xE2\x96\x90"}, // RIGHT ONE EIGHTH BLOCK + {0x2596, "\xE2\x96\x84"}, // QUADRANT LOWER LEFT + {0x2597, "\xE2\x96\x84"}, // QUADRANT LOWER RIGHT + {0x2598, "\xE2\x96\x80"}, // QUADRANT UPPER LEFT + {0x2599, "\xE2\x96\x88"}, // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT + {0x259A, "\xE2\x96\x92"}, // QUADRANT UPPER LEFT AND LOWER RIGHT + {0x259B, "\xE2\x96\x88"}, // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT + {0x259C, "\xE2\x96\x88"}, // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT + {0x259D, "\xE2\x96\x80"}, // QUADRANT UPPER RIGHT + {0x259E, "\xE2\x96\x92"}, // QUADRANT UPPER RIGHT AND LOWER LEFT + {0x259F, "\xE2\x96\x88"}, // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT + {0x25A1, "\x23"}, // WHITE SQUARE + {0x25AA, "\x2A"}, // BLACK SMALL SQUARE + {0x25AB, "\x2A"}, // WHITE SMALL SQUARE + {0x25B2, "\x5E"}, // BLACK UP-POINTING TRIANGLE + {0x25B6, "\x3E"}, // BLACK RIGHT-POINTING TRIANGLE + {0x25BC, "\x76"}, // BLACK DOWN-POINTING TRIANGLE + {0x25C0, "\x3C"}, // BLACK LEFT-POINTING TRIANGLE + {0x25CB, "\x6F"}, // WHITE CIRCLE + {0x25CF, "\x2A"}, // BLACK CIRCLE + {0x2605, "\x2A"}, // BLACK STAR + {0x2606, "\x2A"}, // WHITE STAR + {0x2620, "\x21"}, // SKULL AND CROSSBONES + {0x2660, "\x53"}, // BLACK SPADE SUIT + {0x2663, "\x43"}, // BLACK CLUB SUIT + {0x2665, "\x48"}, // BLACK HEART SUIT + {0x2666, "\x44"}, // BLACK DIAMOND SUIT + {0x2694, "\x78"}, // CROSSED SWORDS + {0x2713, "\x2B"}, // CHECK MARK + {0x2714, "\x2B"}, // HEAVY CHECK MARK + {0x2717, "\x78"}, // BALLOT X + {0x2718, "\x78"}, // HEAVY BALLOT X + {0x2726, "\x2A"}, // BLACK FOUR POINTED STAR + {0x2727, "\x2A"}, // WHITE FOUR POINTED STAR + {0x272A, "\x2A"}, // CIRCLED WHITE STAR + {0x2734, "\x2A"}, // EIGHT POINTED BLACK STAR + {0x2736, "\x2A"}, // SIX POINTED BLACK STAR }; } // namespace @@ -374,6 +518,33 @@ const char *TranslitToKoi8(char32_t code_point) { return (it != end && it->code_point == code_point) ? it->replacement : nullptr; } +std::string Utf8ToKoi8(const std::string &text) { + if (text.empty()) { + return text; + } + std::string reduced; + reduced.reserve(text.size()); + for (std::size_t pos = 0; pos < text.size();) { + char32_t cp = 0; + const std::size_t len = utf8::decode(text, pos, cp); + if (len == 0) { + break; + } + const char *const replacement = TranslitToKoi8(cp); + if (replacement != nullptr) { + reduced.append(replacement); + } else { + reduced.append(text, pos, len); + } + pos += len; + } + + // KOI8-R is never longer than the UTF-8 it came from, so the input size is a safe bound. + std::vector out(reduced.size() + 1, '\0'); + utf8_to_koi(const_cast(reduced.c_str()), out.data()); + return std::string(out.data()); +} + } // namespace codepages // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/translit_koi8.h b/src/utils/translit_koi8.h index c446dc88a..34cc77dd3 100644 --- a/src/utils/translit_koi8.h +++ b/src/utils/translit_koi8.h @@ -6,6 +6,8 @@ #ifndef BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ #define BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ +#include + namespace codepages { // UTF-8 replacement for a code point KOI8-R cannot represent, or nullptr when there is no @@ -13,6 +15,12 @@ namespace codepages { // than one character: "..." for an ellipsis, "(tm)" for a trademark sign. const char *TranslitToKoi8(char32_t code_point); +// UTF-8 text -> KOI8-R, running the table above as a pre-pass. Doing the substitution before the +// conversion is what allows a replacement to be longer than one character: the converter itself +// writes exactly one output byte per code point. No replacement ever grows the text (pinned by a +// test), so the result is never longer than the input. +std::string Utf8ToKoi8(const std::string &text); + } // namespace codepages #endif // BYLINS_SRC_UTILS_TRANSLIT_KOI8_H_ diff --git a/tests/translit_koi8.cpp b/tests/translit_koi8.cpp index 36f90ab28..43b9bce9d 100644 --- a/tests/translit_koi8.cpp +++ b/tests/translit_koi8.cpp @@ -7,9 +7,11 @@ #include "utils/translit_koi8.h" #include "utils/native_text.h" +#include "utils/utf8.h" #include +#include #include using codepages::TranslitToKoi8; @@ -32,7 +34,7 @@ TEST(TranslitKoi8, TypographyBecomesPlainAscii) { EXPECT_EQ(Tr(0x00BB), "\""); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK EXPECT_EQ(Tr(0x201C), "\""); // LEFT DOUBLE QUOTATION MARK EXPECT_EQ(Tr(0x2019), "'"); // RIGHT SINGLE QUOTATION MARK - EXPECT_EQ(Tr(0x2122), "(tm)"); // TRADE MARK SIGN + EXPECT_EQ(Tr(0x2122), "tm"); // TRADE MARK SIGN EXPECT_EQ(Tr(0x00AD), ""); // SOFT HYPHEN: drops out entirely // A no-break space needs no replacement: KOI8-R has one of its own (0x9A). EXPECT_EQ(TranslitToKoi8(0x00A0), nullptr); @@ -107,4 +109,37 @@ TEST(TranslitKoi8, ToKoi8FallsBackToThePlaceholder) { EXPECT_EQ(native_text::to_koi8("a\xF0\x9F\x98\x80" "b"), "a?b"); } +TEST(TranslitKoi8, GraphicsDegradeToWhatKoi8CanDraw) { + // Frames: the rounded and heavy corners reduce to the plain ones KOI8-R does have. + EXPECT_EQ(Tr(0x256D), "\xE2\x94\x8C"); // arc down and right -> U+250C + EXPECT_EQ(Tr(0x256E), "\xE2\x94\x90"); // arc down and left -> U+2510 + EXPECT_EQ(Tr(0x256F), "\xE2\x94\x98"); // arc up and left -> U+2518 + EXPECT_EQ(Tr(0x2570), "\xE2\x94\x94"); // arc up and right -> U+2514 + EXPECT_EQ(Tr(0x2501), "\xE2\x94\x80"); // heavy horizontal -> U+2500 + EXPECT_EQ(Tr(0x2503), "\xE2\x94\x82"); // heavy vertical -> U+2502 + // Partial blocks and stars reduce to something that still draws. + EXPECT_EQ(Tr(0x2589), "\xE2\x96\x88"); // 7/8 block -> full block + EXPECT_EQ(Tr(0x2726), "*"); // black four pointed star + EXPECT_EQ(Tr(0x2605), "*"); // black star +} + +TEST(TranslitKoi8, ReplacementNeverGrowsTheText) { + // The substitution is a pre-pass over the UTF-8, and callers rely on the conversion to + // KOI8-R never making the string longer. So a replacement must fit in the bytes the + // character itself occupied. + for (char32_t cp = 1; cp <= 0x10FFFF; ++cp) { + if (cp >= 0xD800 && cp <= 0xDFFF) { + continue; // surrogates are not encodable + } + const char *const r = TranslitToKoi8(cp); + if (r == nullptr) { + continue; + } + std::string source; + ASSERT_GT(utf8::encode(cp, source), 0u) << "U+" << static_cast(cp); + EXPECT_LE(std::strlen(r), source.size()) + << "replacement for U+" << static_cast(cp) << " is longer than the character"; + } +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From a3ac6642db93af843a434ac23ce393f1d7879aa1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 05:52:41 +0200 Subject: [PATCH 037/110] =?UTF-8?q?fix(utf8):=20=D0=BA=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D0=B8=20=D0=B2=20"=D0=B7=D0=B0=D0=BA=D0=BB=20?= =?UTF-8?q?=D0=B2=D1=81=D0=B5"=20=D1=81=D1=87=D0=B8=D1=82=D0=B0=D1=8E?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B2=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE?= =?UTF-8?q?=D0=BB=D0=B0=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раскладка списка заклинаний по колонкам решалась арифметикой по накопленной длине строки в байтах. Под UTF-8 байт на символ уже не один, поэтому перенос случался не там, где надо, и колонки разъезжались. Длина в буфере (смещение для strcpy) и длина в символах разведены: первая осталась байтовой, вторая считается через native_text::char_count и решает раскладку. Третья ветка переведена с sprintf на fmt -- printf меряет %-30s в байтах, а fmt для корректного UTF-8 меряет ширину поля в символах. --- src/engine/ui/cmd/do_spells.cpp | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/engine/ui/cmd/do_spells.cpp b/src/engine/ui/cmd/do_spells.cpp index 1ebb0dd5f..55175fd26 100644 --- a/src/engine/ui/cmd/do_spells.cpp +++ b/src/engine/ui/cmd/do_spells.cpp @@ -11,6 +11,7 @@ #include "engine/db/global_objects.h" #include "gameplay/mechanics/weather.h" #include "gameplay/core/remort.h" +#include "utils/native_text.h" #include @@ -48,11 +49,17 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { char names[kMaxMemoryCircle][kMaxStringLength]; std::string time_str; int slots[kMaxMemoryCircle], i, max_slot = 0, slot_num, gcount = 0; + // Разбивка на колонки считается в символах, а не в байтах: под UTF-8 байт на символ уже не + // один, и прежняя арифметика по длине строки разносила колонки (issue #3681). slots[] + // остаётся смещением в буфере -- оно по природе байтовое, -- а chars[] хранит, сколько + // символов в этот круг уже набрано. + int chars[kMaxMemoryCircle]; auto have_spells{false}; max_slot = 0; for (i = 0; i < kMaxMemoryCircle; i++) { *names[i] = '\0'; slots[i] = 0; + chars[i] = 0; } for (const auto &spl_info : MUD::Spells()) { @@ -89,18 +96,20 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { continue; if (CheckRecipeItems(ch, spell_id, ESpellType::kRunes, false)) { const auto line = fmt::format("{}|<...{:4}.> {}{:<38}&n|", - slots[slot_num] % 114 < 10 ? "\r\n" : " ", + chars[slot_num] % 114 < 10 ? "\r\n" : " ", CalcSpellManacost(ch, spell_id), GetSpellColor(spell_id), MUD::Spell(spell_id).GetCName()); strcpy(names[slot_num] + slots[slot_num], line.c_str()); slots[slot_num] += static_cast(line.size()); + chars[slot_num] += static_cast(native_text::char_count(line)); } else { if (all) { const auto line = fmt::format("{}|+--------+ {}{:<38}&n|", - slots[slot_num] % 114 < 10 ? "\r\n" : " ", GetSpellColor(spell_id), + chars[slot_num] % 114 < 10 ? "\r\n" : " ", GetSpellColor(spell_id), MUD::Spell(spell_id).GetCName()); strcpy(names[slot_num] + slots[slot_num], line.c_str()); slots[slot_num] += static_cast(line.size()); + chars[slot_num] += static_cast(native_text::char_count(line)); } } } else { @@ -118,9 +127,10 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { else { sprintf(buf1, "%s", "K"); } - slots[slot_num] += sprintf(names[slot_num] + slots[slot_num], - "%s|<%s%c%c%c%c%c%c%c>%s%s%-30s %-7s&n|", - slots[slot_num] % 116 < 10 ? "\r\n" : " ", + // fmt, а не sprintf: ширину поля с названием заклинания надо мерить в символах. + // printf считает %-30s в байтах, и под UTF-8 колонка разъезжалась (issue #3681). + const auto line = fmt::format("{}|<{}{}{}{}{}{}{}{}>{}{}{:<30} {:<7}&n|", + chars[slot_num] % 116 < 10 ? "\r\n" : " ", IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kKnow) ? buf1 : ".", IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kTemp) ? 'T' : '.', IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kPotionCast) ? 'P' : '.', @@ -128,11 +138,14 @@ void DisplaySpells(CharData *ch, CharData *vict, bool all) { IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kScrollCast) ? 'S' : '.', IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kItemCast) ? 'I' : '.', IS_SET(GET_SPELL_TYPE(ch, spell_id), ESpellType::kRunes) ? 'R' : '.', - '.', + '.', (CalcMinSpellLvl(ch, spell_id) - GetRealLevel(ch) < 10) ? " " : " ", GetSpellColor(spell_id), MUD::Spell(spell_id).GetCName(), - time_str.c_str()); + time_str); + strcpy(names[slot_num] + slots[slot_num], line.c_str()); + slots[slot_num] += static_cast(line.size()); + chars[slot_num] += static_cast(native_text::char_count(line)); } have_spells = true; }; From 8f352df9adb22ae83f60066918cfe621d69b008c Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 06:33:21 +0200 Subject: [PATCH 038/110] =?UTF-8?q?fix(utf8):=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BB=D0=B8=D0=B3=D0=B0=D1=82=D1=83=D1=80=D1=8B=20=D0=B2=20?= =?UTF-8?q?=D1=88=D0=B0=D0=BF=D0=BA=D0=B5=20--=20'ae'=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D0=B5=D0=B5=20=D0=B8=20=D1=80=D0=B0=D0=BC=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=B0=D1=88=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/greeting.utf8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 index 0d13f7a22..e4183ed6b 100644 --- a/lib/greeting.utf8 +++ b/lib/greeting.utf8 @@ -2,7 +2,7 @@ ╭──────────────────────────────────────────────────────────────────────────╮ │ Based on CircleMUD v3.0 — created by Jeremy Elson │ │ DikuMUD Gamma 0.0 — Sebastian Hammer, Michael Seifert, │ -│ Hans Henrik Stærfeldt, Tom Madsen, Katja Nyboe │ +│ Hans Henrik Staerfeldt, Tom Madsen, Katja Nyboe │ ╰──────────────────────────────────────────────────────────────────────────╯ ███████ From ac1a906142e87298626898a90d1d08e3004cd2a1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 07:14:07 +0200 Subject: [PATCH 039/110] =?UTF-8?q?fix(utf8):=20=D0=B4=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B0=D1=8F=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D0=BF=D0=BE=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=8C=20=D0=BD=D0=B0=20=D1=81=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B5=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utils::SortKoiString перегонял список в Windows-1251 прямо в строках (там русские буквы стоят по алфавиту, в отличие от KOI8-R), сортировал и перегонял обратно. Под UTF-8 это разрушало текст: побайтовая таблица KOI8-R -> Windows-1251 для многобайтовой строки не значит ничего, а обратное преобразование уже не восстанавливало исходник. Поэтому "справка умениябогатыря", "способностикупца" и прочие собираемые на лету разделы выезжали мусором, хотя обычная справка из файлов читалась нормально. Сортировка теперь идёт по ключу (native_text::sort_key), а сами строки не трогаются. Ключ даёт ровно тот же порядок, что и прежний трюк, и одинаков в обеих сборках -- закреплено тестом. Тот же ручной трюк, повторённый в help.cpp через SubstKtoW/SubstWtoK, убран. Заодно ширина колонок в этих разделах: std::setw меряет в байтах, поэтому под UTF-8 колонки не сходились. Переведено на fmt, который для корректного UTF-8 меряет ширину поля в символах. --- src/engine/db/help.cpp | 18 +++++++++--------- src/utils/native_text.cpp | 9 +++++++++ src/utils/native_text.h | 7 +++++++ src/utils/utils_string.cpp | 26 +++++++++++--------------- tests/native_text.cpp | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/engine/db/help.cpp b/src/engine/db/help.cpp index afd1b82c7..cc166766c 100644 --- a/src/engine/db/help.cpp +++ b/src/engine/db/help.cpp @@ -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"; @@ -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"; @@ -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"; diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 08b01f2e8..12862e451 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -767,6 +767,15 @@ std::string read_data_file(const std::string &path) { return from_koi8(raw); } + +std::string sort_key(const std::string &text) { + std::string key = to_koi8(text); + for (char &c : key) { + c = codepages::KtoW(c); + } + return key; +} + } // namespace native_text // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 9397e5a2a..3b42395a0 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -164,6 +164,13 @@ std::string to_koi8(const std::string &text); // with the full Unicode repertoire still renders sensibly on a KOI8-R build. std::string from_utf8(const std::string &text); +// Collation key for sorting Russian text. The Russian letters are not in alphabetical order in +// KOI8-R, so sorting has always gone through Windows-1251 bytes, where they are. The key +// reproduces exactly that order and does not depend on the native encoding: compare two keys +// instead of transcoding the strings themselves (which corrupted them under UTF-8, since the +// KOI8-R -> Windows-1251 byte table means nothing for a multibyte string). +std::string sort_key(const std::string &text); + // Read a data file (all of which are stored in KOI8-R) and hand back its contents in the // engine's native encoding. The counterpart of from_koi8 for whole files: parsers that take a // buffer should go through this instead of reading the path themselves, so the boundary stays diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index e4adbbe04..b40107c9b 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -365,25 +365,21 @@ std::string FixDot(std::string s) { return s; } +// Сортировка идёт по ключу, а не по перекодированной на месте строке. Прежний трюк -- перегнать +// список в Windows-1251 (там русские буквы стоят по алфавиту, в отличие от KOI8-R), отсортировать +// и перегнать обратно -- под UTF-8 портил текст: побайтовая таблица KOI8-R -> Windows-1251 для +// многобайтовой строки не значит ничего, и обратное преобразование уже не восстанавливало исходник +// (issue #3681). native_text::sort_key даёт ровно тот же порядок, ничего не меняя в самих строках. void SortKoiString(std::vector &str) { - for (auto &it : str) { - ConvertKtoW(it); - } - std::sort(str.begin(), str.end(), std::less()); - for (auto &it : str) { - ConvertWtoK(it); - } + std::sort(str.begin(), str.end(), [](const std::string &a, const std::string &b) { + return native_text::sort_key(a) < native_text::sort_key(b); + }); } - void SortKoiStringReverse(std::vector &str) { - for (auto &it : str) { - ConvertKtoW(it); - } - std::sort(str.begin(), str.end(), std::greater()); - for (auto &it : str) { - ConvertWtoK(it); - } + std::sort(str.begin(), str.end(), [](const std::string &a, const std::string &b) { + return native_text::sort_key(a) > native_text::sort_key(b); + }); } void ReplaceFirst(std::string &s, const std::string &toSearch, const std::string &replacer) { diff --git a/tests/native_text.cpp b/tests/native_text.cpp index f0574070f..ab7a153cb 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -473,4 +473,39 @@ TEST(NativeText, ToKoi8IsTheInverseBoundary) { } } +// KOI8-R spellings, so the fixture is built through from_koi8 and comes out native in either +// build. Words chosen to expose the KOI8-R byte order: there the Russian letters are NOT in +// alphabetical order, so a plain byte comparison would sort these wrong. +TEST(NativeText, SortKeyOrdersRussianAlphabetically) { + const auto native = [](const char *koi8) { return native_text::from_koi8(koi8); }; + // "arbuz", "banan", "vishnya", "yabloko" -- a, b, v, ya. + const std::string a = native("\xC1\xD2\xC2\xD5\xDA"); + const std::string b = native("\xC2\xC1\xCE\xC1\xCE"); + const std::string v = native("\xD7\xC9\xDB\xCE\xD1"); + const std::string ya = native("\xD1\xC2\xCC\xCF\xCB\xCF"); + + EXPECT_LT(native_text::sort_key(a), native_text::sort_key(b)); + EXPECT_LT(native_text::sort_key(b), native_text::sort_key(v)); + EXPECT_LT(native_text::sort_key(v), native_text::sort_key(ya)); + // "zhaba" and "ivan": alphabetically zh comes before i, but in KOI8-R its byte (0xD6) is + // above i's (0xC9) -- which is the whole reason the key exists. + const std::string zh = native("\xD6\xC1\xC2\xC1"); + const std::string i = native("\xC9\xD7\xC1\xCE"); + EXPECT_LT(native_text::sort_key(zh), native_text::sort_key(i)); + if (!native_text::native_is_utf8()) { + EXPECT_GT(zh, i); + } + // ASCII keeps its own order and stays below the Cyrillic. + EXPECT_LT(native_text::sort_key("abc"), native_text::sort_key("abd")); + EXPECT_LT(native_text::sort_key("zzz"), native_text::sort_key(a)); +} + +TEST(NativeText, SortKeyIsStableAcrossEncodings) { + // The key is a byte string in a fixed encoding, so the very same words must produce the very + // same key no matter which encoding the engine runs in. Pinned literally. + EXPECT_EQ(native_text::sort_key(native_text::from_koi8("\xC1\xD2\xC2\xD5\xDA")), + "\xE0\xF0\xE1\xF3\xE7"); // "arbuz" in Windows-1251 + EXPECT_EQ(native_text::sort_key("plain ascii"), "plain ascii"); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From eff885d44ff8cc5052cb789851320e55c28e17f1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 07:59:51 +0200 Subject: [PATCH 040/110] =?UTF-8?q?feat(utf8):=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B8=D1=81=D1=83=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=81=D0=B5=D0=B2=D0=B4=D0=BE=D0=B3=D1=80=D0=B0=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=B9=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Символы карты записаны в UTF-8 и приводятся к нативной кодировке один раз при первом обращении: под UTF-8 это тождество, под KOI8-R -- перекодировка со сведением через словарь. Клиент в UTF-8 видит тонкие, жирные и пунктирные линии и треугольники переходов вверх-вниз, клиент в koi8/alt -- обычную псевдографику KOI8-R. Ширина не меняется: каждая замена ровно в один символ, так что раскладка карты в обеих кодировках одна и та же. Язык рисунка прежний -- разрывы значат "пройти можно", сплошная "нельзя", -- но теперь он читается: открытый проход тонкий с разрывом, дверь с перекладиной, скрытый (видят только боги) пунктиром, стена жирная. Вода стала '≈', полёт '°' вместо запятой и апострофа. --- src/engine/ui/mapsystem.cpp | 79 ++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/engine/ui/mapsystem.cpp b/src/engine/ui/mapsystem.cpp index dc2b4744c..74147b124 100644 --- a/src/engine/ui/mapsystem.cpp +++ b/src/engine/ui/mapsystem.cpp @@ -4,7 +4,10 @@ #include "engine/ui/mapsystem.h" #include "administration/privilege.h" +#include "utils/native_text.h" + #include +#include #include #include @@ -145,43 +148,52 @@ enum { SCREEN_TOTAL }; -const char *signs[] = +// Символы карты записаны в UTF-8 (escape-последовательностями, чтобы не зависеть от кодировки +// самого файла) и приводятся к нативной кодировке один раз, при первом обращении. Под UTF-8 это +// тождество, под KOI8-R -- перекодировка со сведением через словарь: жирная и пунктирная линии +// становятся обычной, треугольники -- '^' и 'v'. Ширина при этом не меняется, каждая замена +// ровно в один символ, так что раскладка карты одинакова в обеих кодировках (issue #3681). +// +// Язык рисунка прежний: разрывы -- значит пройти можно, сплошная -- значит нельзя. Открытый +// проход тонкий с разрывом, дверь с перекладиной, скрытый (видят только боги) пунктиром, +// стена жирная. +const char *signs_utf8[] = { // SCREEN_Y - "&K - &n", - "&C-=-&n", - "&R---&n", - "&G---&n", + "&K \xE2\x94\x80 &n", + "&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n", + "&R\xE2\x94\x84\xE2\x94\x84\xE2\x94\x84&n", + "&G\xE2\x94\x81\xE2\x94\x81\xE2\x94\x81&n", // SCREEN_X - "&K:&n", + "&K\xC2\xB7&n", "&C/&n", - "&R|&n", - "&G|&n", + "&R\xE2\x94\x8A&n", + "&G\xE2\x94\x83&n", // SCREEN_UP - "&K^&n", - "&C^&n", - "&R^&n", + "&K\xE2\x96\xB2&n", + "&C\xE2\x96\xB2&n", + "&R\xE2\x96\xB2&n", "", // SCREEN_DOWN - "&Kv&n", - "&Cv&n", - "&Rv&n", + "&K\xE2\x96\xBC&n", + "&C\xE2\x96\xBC&n", + "&R\xE2\x96\xBC&n", "", // SCREEN_Y_UP - "&K -&n", - "&C-=&n", - "&R--&n", - "&G--&n", + "&K \xE2\x94\x80&n", + "&C\xE2\x94\x80\xE2\x95\x90&n", + "&R\xE2\x94\x84\xE2\x94\x84&n", + "&G\xE2\x94\x81\xE2\x94\x81&n", // SCREEN_Y_DOWN - "&K- &n", - "&C=-&n", - "&R--&n", - "&G--&n", + "&K\xE2\x94\x80 &n", + "&C\xE2\x95\x90\xE2\x94\x80&n", + "&R\xE2\x94\x84\xE2\x94\x84&n", + "&G\xE2\x94\x81\xE2\x94\x81&n", // OTHERS "&c@&n", "&C>&n", "&K~&n", - "&RЖ&n", + "&R\xD0\x96&n", "", "&K?&n", "&r1&n", @@ -213,13 +225,24 @@ const char *signs[] = "&WT&n", "&WE&n", "&WG&n", - "&C,&n", - "&C`&n", + "&C\xE2\x89\x88&n", + "&C\xC2\xB0&n", "&WO&n", - "&R,&n", - "&R`&n" + "&R\xE2\x89\x88&n", + "&R\xC2\xB0&n" }; +const std::string &Sign(int index) { + static std::vector native; + if (native.empty()) { + native.reserve(std::size(signs_utf8)); + for (const char *const sign : signs_utf8) { + native.push_back(native_text::from_utf8(sign)); + } + } + return native[index]; +} + inline bool GodBigMode(CharData *ch) { return ch->map_check_option(MAP_MODE_GOD_BIG) && privilege::IsImmortal(ch); @@ -688,7 +711,7 @@ void print_map(CharData *ch, CharData *imm) { if (screen[i][k] <= -1) { out += " "; } else if (screen[i][k] < SCREEN_TOTAL && screen[i][k] != SCREEN_EMPTY) { - out += signs[screen[i][k]]; + out += Sign(screen[i][k]); } } out += "\r\n"; From c3e687dd4a90a6b5fb0cae823b9ab09e4509193c Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 09:33:23 +0200 Subject: [PATCH 041/110] =?UTF-8?q?fix(map):=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=B1=D1=80=D1=8B=D0=B2=D0=B0=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обрезка пустых строк искала не первую и последнюю непустую строку, а первый разрыв: как только после начала карты попадалась строка без единой клетки, отрисовка на ней и заканчивалась. А разрыв посередине -- дело обычное, комнаты редко стоят плотным прямоугольником. Чем больше глубина, тем вероятнее разрыв, поэтому "карта богов" с глубиной 25 выглядела не крупнее обычной с глубиной 5. Ровно та же правка уже сделана для правой границы по столбцам ("иначе клетки дальше теряются") -- по строкам её забыли. --- src/engine/ui/mapsystem.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/engine/ui/mapsystem.cpp b/src/engine/ui/mapsystem.cpp index 74147b124..e6e083bf9 100644 --- a/src/engine/ui/mapsystem.cpp +++ b/src/engine/ui/mapsystem.cpp @@ -638,11 +638,16 @@ void print_map(CharData *ch, CharData *imm) { } } - if (found && start_line < 0) { - start_line = i; - } else if (!found && start_line > 0) { - end_line = i; - break; + // Ищем именно первую и последнюю непустую строку, а не первый разрыв: в строке + // посередине карты может не оказаться ни одной клетки (комнаты стоят буквой П), + // и прежний код обрубал карту прямо там. Чем больше глубина, тем вероятнее разрыв, + // поэтому "карта богов" с глубиной 25 выглядела не крупнее обычной. Ровно та же + // правка уже сделана для правой границы по столбцам (issue #3681). + if (found) { + if (start_line < 0) { + start_line = i; + } + end_line = static_cast(i) + 1; } } From ea03efe5390bca780e76a7aa55f8e7ccda40c2f2 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 09:47:55 +0200 Subject: [PATCH 042/110] =?UTF-8?q?feat(map):=20=D0=BF=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=BD=D0=B0=D1=87=D0=B5=D1=80=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D0=BA=D0=BB=D0=B5=D1=82=D0=BA?= =?UTF-8?q?=D1=83,=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D1=8B=20=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=B1=D1=83=D0=BA=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D1=81=D1=82=D0=BE=D1=8F=20?= =?UTF-8?q?=D0=B8=20=D1=83=D1=87=D0=B8=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Начертание выбирается по кодировке клиента, а не отдаётся словарю транслитерации. Словарь подбирает замену по похожести символа, а карте нужна замена по смыслу и строго той же ширины: домик постоя должен стать 'R', а не '#', стрелка перехода в зону -- '>', а не '->' в два знака (карта бы разъехалась ровно так же, как рамка на лигатуре). UTF-8-клиенту: '→' переход в другую зону, '○' мирная комната, '⌂' постой, '★' учитель, плюс прежние линии и треугольники. Всем остальным -- то же самое прежними знаками: '>', '~', 'R', 'T'. Буквы у лавки, банка, почты, конюшни, обмена и гривен оставлены: там буква несёт мнемонику, которую значок теряет, а конверт и монета вдобавок эмодзи-представимые и в части клиентов занимают две колонки. --- src/engine/ui/mapsystem.cpp | 178 ++++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 80 deletions(-) diff --git a/src/engine/ui/mapsystem.cpp b/src/engine/ui/mapsystem.cpp index e6e083bf9..94af40d4c 100644 --- a/src/engine/ui/mapsystem.cpp +++ b/src/engine/ui/mapsystem.cpp @@ -149,98 +149,116 @@ enum { }; // Символы карты записаны в UTF-8 (escape-последовательностями, чтобы не зависеть от кодировки -// самого файла) и приводятся к нативной кодировке один раз, при первом обращении. Под UTF-8 это -// тождество, под KOI8-R -- перекодировка со сведением через словарь: жирная и пунктирная линии -// становятся обычной, треугольники -- '^' и 'v'. Ширина при этом не меняется, каждая замена -// ровно в один символ, так что раскладка карты одинакова в обеих кодировках (issue #3681). +// самого файла) и приводятся к нативной кодировке один раз, при первом обращении. // -// Язык рисунка прежний: разрывы -- значит пройти можно, сплошная -- значит нельзя. Открытый -// проход тонкий с разрывом, дверь с перекладиной, скрытый (видят только боги) пунктиром, -// стена жирная. -const char *signs_utf8[] = +// На каждую клетку два начертания. Богатое -- UTF-8-клиенту: тонкие, жирные и пунктирные линии, +// треугольники переходов вверх-вниз, домик постоя, звёздочка учителя. Простое -- всем остальным, +// из того, что есть в KOI8-R. Начертание выбирается по кодировке клиента, а не отдаётся на откуп +// словарю транслитерации: словарь подбирает замену по похожести символа, а тут нужна замена по +// смыслу ('домик' должен стать 'R', а не '#') и строго той же ширины, иначе карта разъедется +// (issue #3681). +// +// Язык рисунка прежний -- разрывы значат "пройти можно", сплошная "нельзя": открытый проход +// тонкий с разрывом, дверь с перекладиной, скрытый (видят только боги) пунктиром, стена жирная. +struct MapSign { + const char *rich; + const char *plain; +}; + +const MapSign signs_utf8[] = { // SCREEN_Y - "&K \xE2\x94\x80 &n", - "&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n", - "&R\xE2\x94\x84\xE2\x94\x84\xE2\x94\x84&n", - "&G\xE2\x94\x81\xE2\x94\x81\xE2\x94\x81&n", + {"&K \xE2\x94\x80 &n", "&K \xE2\x94\x80 &n"}, + {"&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n", "&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n"}, + {"&R\xE2\x94\x84\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80\xE2\x94\x80&n"}, + {"&G\xE2\x94\x81\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80\xE2\x94\x80&n"}, // SCREEN_X - "&K\xC2\xB7&n", - "&C/&n", - "&R\xE2\x94\x8A&n", - "&G\xE2\x94\x83&n", + {"&K\xC2\xB7&n", "&K\xC2\xB7&n"}, + {"&C/&n", "&C/&n"}, + {"&R\xE2\x94\x8A&n", "&R\xE2\x94\x82&n"}, + {"&G\xE2\x94\x83&n", "&G\xE2\x94\x82&n"}, // SCREEN_UP - "&K\xE2\x96\xB2&n", - "&C\xE2\x96\xB2&n", - "&R\xE2\x96\xB2&n", - "", + {"&K\xE2\x96\xB2&n", "&K^&n"}, + {"&C\xE2\x96\xB2&n", "&C^&n"}, + {"&R\xE2\x96\xB2&n", "&R^&n"}, + {"", ""}, // SCREEN_DOWN - "&K\xE2\x96\xBC&n", - "&C\xE2\x96\xBC&n", - "&R\xE2\x96\xBC&n", - "", + {"&K\xE2\x96\xBC&n", "&Kv&n"}, + {"&C\xE2\x96\xBC&n", "&Cv&n"}, + {"&R\xE2\x96\xBC&n", "&Rv&n"}, + {"", ""}, // SCREEN_Y_UP - "&K \xE2\x94\x80&n", - "&C\xE2\x94\x80\xE2\x95\x90&n", - "&R\xE2\x94\x84\xE2\x94\x84&n", - "&G\xE2\x94\x81\xE2\x94\x81&n", + {"&K \xE2\x94\x80&n", "&K \xE2\x94\x80&n"}, + {"&C\xE2\x94\x80\xE2\x95\x90&n", "&C\xE2\x94\x80\xE2\x95\x90&n"}, + {"&R\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80&n"}, + {"&G\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80&n"}, // SCREEN_Y_DOWN - "&K\xE2\x94\x80 &n", - "&C\xE2\x95\x90\xE2\x94\x80&n", - "&R\xE2\x94\x84\xE2\x94\x84&n", - "&G\xE2\x94\x81\xE2\x94\x81&n", + {"&K\xE2\x94\x80 &n", "&K\xE2\x94\x80 &n"}, + {"&C\xE2\x95\x90\xE2\x94\x80&n", "&C\xE2\x95\x90\xE2\x94\x80&n"}, + {"&R\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80&n"}, + {"&G\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80&n"}, // OTHERS - "&c@&n", - "&C>&n", - "&K~&n", - "&R\xD0\x96&n", - "", - "&K?&n", - "&r1&n", - "&r2&n", - "&r3&n", - "&r4&n", - "&r5&n", - "&r6&n", - "&r7&n", - "&r8&n", - "&r9&n", - "&R!&n", - "&K?&n", - "&y1&n", - "&y2&n", - "&y3&n", - "&y4&n", - "&y5&n", - "&y6&n", - "&y7&n", - "&y8&n", - "&y9&n", - "&Y!&n", - "&W$&n", - "&WR&n", - "&WM&n", - "&WB&n", - "&WH&n", - "&WT&n", - "&WE&n", - "&WG&n", - "&C\xE2\x89\x88&n", - "&C\xC2\xB0&n", - "&WO&n", - "&R\xE2\x89\x88&n", - "&R\xC2\xB0&n" + {"&c@&n", "&c@&n"}, // SCREEN_CHAR + {"&C\xE2\x86\x92&n", "&C>&n"}, // SCREEN_NEW_ZONE + {"&K\xE2\x97\x8B&n", "&K~&n"}, // SCREEN_PEACE + {"&R\xD0\x96&n", "&R\xD0\x96&n"}, // SCREEN_DEATH_TRAP + {"", ""}, // SCREEN_EMPTY + {"&K?&n", "&K?&n"}, + {"&r1&n", "&r1&n"}, + {"&r2&n", "&r2&n"}, + {"&r3&n", "&r3&n"}, + {"&r4&n", "&r4&n"}, + {"&r5&n", "&r5&n"}, + {"&r6&n", "&r6&n"}, + {"&r7&n", "&r7&n"}, + {"&r8&n", "&r8&n"}, + {"&r9&n", "&r9&n"}, + {"&R!&n", "&R!&n"}, + {"&K?&n", "&K?&n"}, + {"&y1&n", "&y1&n"}, + {"&y2&n", "&y2&n"}, + {"&y3&n", "&y3&n"}, + {"&y4&n", "&y4&n"}, + {"&y5&n", "&y5&n"}, + {"&y6&n", "&y6&n"}, + {"&y7&n", "&y7&n"}, + {"&y8&n", "&y8&n"}, + {"&y9&n", "&y9&n"}, + {"&Y!&n", "&Y!&n"}, + {"&W$&n", "&W$&n"}, // SHOP + {"&W\xE2\x8C\x82&n", "&WR&n"}, // RENT + {"&WM&n", "&WM&n"}, // MAIL + {"&WB&n", "&WB&n"}, // BANK + {"&WH&n", "&WH&n"}, // HORSE + {"&W\xE2\x98\x85&n", "&WT&n"}, // TEACH + {"&WE&n", "&WE&n"}, // EXCH + {"&WG&n", "&WG&n"}, // TORC + {"&C\xE2\x89\x88&n", "&C\xE2\x89\x88&n"}, // WATER + {"&C\xC2\xB0&n", "&C\xC2\xB0&n"}, // FLYING + {"&WO&n", "&WO&n"}, // SPEC_OUTFIT + {"&R\xE2\x89\x88&n", "&R\xE2\x89\x88&n"}, // WATER_RED + {"&R\xC2\xB0&n", "&R\xC2\xB0&n"} // FLYING_RED }; -const std::string &Sign(int index) { - static std::vector native; - if (native.empty()) { - native.reserve(std::size(signs_utf8)); - for (const char *const sign : signs_utf8) { - native.push_back(native_text::from_utf8(sign)); +// true, если клиенту можно отдать богатое начертание: он сам в UTF-8 и движок в UTF-8. В любом +// другом сочетании текст по дороге проходит перекодировку в KOI8-R, и богатых символов там нет. +bool RichSigns(const CharData *viewer) { + return native_text::native_is_utf8() + && viewer->desc != nullptr + && viewer->desc->keytable == kCodePageUTF8; +} + +const std::string &Sign(const CharData *viewer, int index) { + static std::vector rich, plain; + if (rich.empty()) { + rich.reserve(std::size(signs_utf8)); + plain.reserve(std::size(signs_utf8)); + for (const auto &sign : signs_utf8) { + rich.push_back(native_text::from_utf8(sign.rich)); + plain.push_back(native_text::from_utf8(sign.plain)); } } - return native[index]; + return RichSigns(viewer) ? rich[index] : plain[index]; } @@ -716,7 +734,7 @@ void print_map(CharData *ch, CharData *imm) { if (screen[i][k] <= -1) { out += " "; } else if (screen[i][k] < SCREEN_TOTAL && screen[i][k] != SCREEN_EMPTY) { - out += Sign(screen[i][k]); + out += Sign(imm ? imm : ch, screen[i][k]); } } out += "\r\n"; From e42ce89088281009f13b29b27c554fb29c4ed0ea Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 15:02:11 +0200 Subject: [PATCH 043/110] =?UTF-8?q?revert(map):=20=D0=B2=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=8C=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D1=8B=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Псевдографика в клиенте пользователя выглядела съехавшей. Геометрию на сервере я померил: длина строк в символах в UTF-8 и KOI8-R совпадает знак в знак, а каждая трёхсимвольная связь стоит ровно на c-1..c+1 относительно колонки своей комнаты -- то есть на стороне движка ничего не смещается. Остаётся шрифт клиента: жирные и пунктирные линии (U+2501, U+2504 и родня) в моноширинных шрифтах редки и подтягиваются из шрифта-заменителя с другой шириной знакоместа. Красоту откатываем целиком: символы карты снова '-', '|', ':', '^', 'v'. Правка обрезки пустых строк остаётся -- она про обрыв карты на первом разрыве и к символам отношения не имеет. --- src/engine/ui/mapsystem.cpp | 181 ++++++++++++++---------------------- 1 file changed, 70 insertions(+), 111 deletions(-) diff --git a/src/engine/ui/mapsystem.cpp b/src/engine/ui/mapsystem.cpp index 94af40d4c..9852aa686 100644 --- a/src/engine/ui/mapsystem.cpp +++ b/src/engine/ui/mapsystem.cpp @@ -4,10 +4,7 @@ #include "engine/ui/mapsystem.h" #include "administration/privilege.h" -#include "utils/native_text.h" - #include -#include #include #include @@ -148,119 +145,81 @@ enum { SCREEN_TOTAL }; -// Символы карты записаны в UTF-8 (escape-последовательностями, чтобы не зависеть от кодировки -// самого файла) и приводятся к нативной кодировке один раз, при первом обращении. -// -// На каждую клетку два начертания. Богатое -- UTF-8-клиенту: тонкие, жирные и пунктирные линии, -// треугольники переходов вверх-вниз, домик постоя, звёздочка учителя. Простое -- всем остальным, -// из того, что есть в KOI8-R. Начертание выбирается по кодировке клиента, а не отдаётся на откуп -// словарю транслитерации: словарь подбирает замену по похожести символа, а тут нужна замена по -// смыслу ('домик' должен стать 'R', а не '#') и строго той же ширины, иначе карта разъедется -// (issue #3681). -// -// Язык рисунка прежний -- разрывы значат "пройти можно", сплошная "нельзя": открытый проход -// тонкий с разрывом, дверь с перекладиной, скрытый (видят только боги) пунктиром, стена жирная. -struct MapSign { - const char *rich; - const char *plain; -}; - -const MapSign signs_utf8[] = +const char *signs[] = { // SCREEN_Y - {"&K \xE2\x94\x80 &n", "&K \xE2\x94\x80 &n"}, - {"&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n", "&C\xE2\x94\x80\xE2\x95\x90\xE2\x94\x80&n"}, - {"&R\xE2\x94\x84\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80\xE2\x94\x80&n"}, - {"&G\xE2\x94\x81\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80\xE2\x94\x80&n"}, + "&K - &n", + "&C-=-&n", + "&R---&n", + "&G---&n", // SCREEN_X - {"&K\xC2\xB7&n", "&K\xC2\xB7&n"}, - {"&C/&n", "&C/&n"}, - {"&R\xE2\x94\x8A&n", "&R\xE2\x94\x82&n"}, - {"&G\xE2\x94\x83&n", "&G\xE2\x94\x82&n"}, + "&K:&n", + "&C/&n", + "&R|&n", + "&G|&n", // SCREEN_UP - {"&K\xE2\x96\xB2&n", "&K^&n"}, - {"&C\xE2\x96\xB2&n", "&C^&n"}, - {"&R\xE2\x96\xB2&n", "&R^&n"}, - {"", ""}, + "&K^&n", + "&C^&n", + "&R^&n", + "", // SCREEN_DOWN - {"&K\xE2\x96\xBC&n", "&Kv&n"}, - {"&C\xE2\x96\xBC&n", "&Cv&n"}, - {"&R\xE2\x96\xBC&n", "&Rv&n"}, - {"", ""}, + "&Kv&n", + "&Cv&n", + "&Rv&n", + "", // SCREEN_Y_UP - {"&K \xE2\x94\x80&n", "&K \xE2\x94\x80&n"}, - {"&C\xE2\x94\x80\xE2\x95\x90&n", "&C\xE2\x94\x80\xE2\x95\x90&n"}, - {"&R\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80&n"}, - {"&G\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80&n"}, + "&K -&n", + "&C-=&n", + "&R--&n", + "&G--&n", // SCREEN_Y_DOWN - {"&K\xE2\x94\x80 &n", "&K\xE2\x94\x80 &n"}, - {"&C\xE2\x95\x90\xE2\x94\x80&n", "&C\xE2\x95\x90\xE2\x94\x80&n"}, - {"&R\xE2\x94\x84\xE2\x94\x84&n", "&R\xE2\x94\x80\xE2\x94\x80&n"}, - {"&G\xE2\x94\x81\xE2\x94\x81&n", "&G\xE2\x94\x80\xE2\x94\x80&n"}, + "&K- &n", + "&C=-&n", + "&R--&n", + "&G--&n", // OTHERS - {"&c@&n", "&c@&n"}, // SCREEN_CHAR - {"&C\xE2\x86\x92&n", "&C>&n"}, // SCREEN_NEW_ZONE - {"&K\xE2\x97\x8B&n", "&K~&n"}, // SCREEN_PEACE - {"&R\xD0\x96&n", "&R\xD0\x96&n"}, // SCREEN_DEATH_TRAP - {"", ""}, // SCREEN_EMPTY - {"&K?&n", "&K?&n"}, - {"&r1&n", "&r1&n"}, - {"&r2&n", "&r2&n"}, - {"&r3&n", "&r3&n"}, - {"&r4&n", "&r4&n"}, - {"&r5&n", "&r5&n"}, - {"&r6&n", "&r6&n"}, - {"&r7&n", "&r7&n"}, - {"&r8&n", "&r8&n"}, - {"&r9&n", "&r9&n"}, - {"&R!&n", "&R!&n"}, - {"&K?&n", "&K?&n"}, - {"&y1&n", "&y1&n"}, - {"&y2&n", "&y2&n"}, - {"&y3&n", "&y3&n"}, - {"&y4&n", "&y4&n"}, - {"&y5&n", "&y5&n"}, - {"&y6&n", "&y6&n"}, - {"&y7&n", "&y7&n"}, - {"&y8&n", "&y8&n"}, - {"&y9&n", "&y9&n"}, - {"&Y!&n", "&Y!&n"}, - {"&W$&n", "&W$&n"}, // SHOP - {"&W\xE2\x8C\x82&n", "&WR&n"}, // RENT - {"&WM&n", "&WM&n"}, // MAIL - {"&WB&n", "&WB&n"}, // BANK - {"&WH&n", "&WH&n"}, // HORSE - {"&W\xE2\x98\x85&n", "&WT&n"}, // TEACH - {"&WE&n", "&WE&n"}, // EXCH - {"&WG&n", "&WG&n"}, // TORC - {"&C\xE2\x89\x88&n", "&C\xE2\x89\x88&n"}, // WATER - {"&C\xC2\xB0&n", "&C\xC2\xB0&n"}, // FLYING - {"&WO&n", "&WO&n"}, // SPEC_OUTFIT - {"&R\xE2\x89\x88&n", "&R\xE2\x89\x88&n"}, // WATER_RED - {"&R\xC2\xB0&n", "&R\xC2\xB0&n"} // FLYING_RED + "&c@&n", + "&C>&n", + "&K~&n", + "&RЖ&n", + "", + "&K?&n", + "&r1&n", + "&r2&n", + "&r3&n", + "&r4&n", + "&r5&n", + "&r6&n", + "&r7&n", + "&r8&n", + "&r9&n", + "&R!&n", + "&K?&n", + "&y1&n", + "&y2&n", + "&y3&n", + "&y4&n", + "&y5&n", + "&y6&n", + "&y7&n", + "&y8&n", + "&y9&n", + "&Y!&n", + "&W$&n", + "&WR&n", + "&WM&n", + "&WB&n", + "&WH&n", + "&WT&n", + "&WE&n", + "&WG&n", + "&C,&n", + "&C`&n", + "&WO&n", + "&R,&n", + "&R`&n" }; -// true, если клиенту можно отдать богатое начертание: он сам в UTF-8 и движок в UTF-8. В любом -// другом сочетании текст по дороге проходит перекодировку в KOI8-R, и богатых символов там нет. -bool RichSigns(const CharData *viewer) { - return native_text::native_is_utf8() - && viewer->desc != nullptr - && viewer->desc->keytable == kCodePageUTF8; -} - -const std::string &Sign(const CharData *viewer, int index) { - static std::vector rich, plain; - if (rich.empty()) { - rich.reserve(std::size(signs_utf8)); - plain.reserve(std::size(signs_utf8)); - for (const auto &sign : signs_utf8) { - rich.push_back(native_text::from_utf8(sign.rich)); - plain.push_back(native_text::from_utf8(sign.plain)); - } - } - return RichSigns(viewer) ? rich[index] : plain[index]; -} - inline bool GodBigMode(CharData *ch) { return ch->map_check_option(MAP_MODE_GOD_BIG) && privilege::IsImmortal(ch); @@ -657,10 +616,10 @@ void print_map(CharData *ch, CharData *imm) { } // Ищем именно первую и последнюю непустую строку, а не первый разрыв: в строке - // посередине карты может не оказаться ни одной клетки (комнаты стоят буквой П), - // и прежний код обрубал карту прямо там. Чем больше глубина, тем вероятнее разрыв, - // поэтому "карта богов" с глубиной 25 выглядела не крупнее обычной. Ровно та же - // правка уже сделана для правой границы по столбцам (issue #3681). + // посередине карты может не оказаться ни одной клетки (комнаты редко стоят плотным + // прямоугольником), и прежний код обрубал карту прямо там. Чем больше глубина, тем + // вероятнее разрыв, поэтому "карта богов" с глубиной 25 выглядела не крупнее обычной. + // Ровно та же правка уже сделана для правой границы по столбцам. if (found) { if (start_line < 0) { start_line = i; @@ -734,7 +693,7 @@ void print_map(CharData *ch, CharData *imm) { if (screen[i][k] <= -1) { out += " "; } else if (screen[i][k] < SCREEN_TOTAL && screen[i][k] != SCREEN_EMPTY) { - out += Sign(imm ? imm : ch, screen[i][k]); + out += signs[screen[i][k]]; } } out += "\r\n"; From 3f5781c4e13f799e6e92dcc51a399449c0a42c87 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 19:43:39 +0200 Subject: [PATCH 044/110] =?UTF-8?q?fix(utf8):=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=D0=B8=D1=81=D1=8C=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B0=D0=B6=D0=B4=D1=8B=20=D0=B8=20=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BD=D0=B0=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B5=20(#3?= =?UTF-8?q?681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Содержимое XML-конфига приводится к нативной кодировке один раз на документ (в DataNode), но перекодировка осталась ещё и на каждом поле, в parse::AttrStr. Под KOI8-R оба преобразования -- тождество, и это ничего не стоило. Под UTF-8 каждое поле переводилось дважды: байты уже готового UTF-8 разбирались как KOI8-R и разворачивались повторно. Само по себе это было бы только порчей текста, но конфиги движок ещё и пишет обратно (ObjSetsLoader::Load в конце нормализует файл через save()). Поэтому порча накапливалась: файл рос примерно вдвое на каждой загрузке. cfg/mechanics/obj_sets.xml дорос со 120 КБ до 6,7 ГБ, и очередной старт умирал, пытаясь прочитать его в память -- это и было то самое "распухание памяти на Check big sets in rent" (падало на следующем шаге, LoadCfg("obj_sets")). Перекодировка на уровне поля убрана. Заодно чтение файла целиком приведено к тому же правилу, что и построчное: from_disk_text берёт корректный UTF-8 как уже нативный и перекодирует только то, что им не является. Без этого цикл "прочитали - записали" остаётся неидемпотентным для любого файла, который движок и читает, и пишет. Проверено на боевом конфиге: первая загрузка 120960 -> 143363 байта (честная разовая перекодировка KOI8-R -> UTF-8), вторая и третья -- те же 143363 байта, байт в байт. До правки: 120960 -> 209301 -> 365820 и дальше вдвое. --- src/utils/native_text.cpp | 13 ++++++++++++- src/utils/native_text.h | 7 +++++++ src/utils/parser_wrapper.cpp | 19 ++++++++----------- src/utils/utils_parse.cpp | 9 ++++++--- tests/native_text.cpp | 26 ++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 12862e451..103e4ef7b 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -758,13 +758,24 @@ std::string from_disk_line(const char *line) { #endif } +std::string from_disk_text(const std::string &text) { +#ifdef INTERNAL_ENCODING_UTF8 + // Same discriminator as from_disk_line, applied to the whole file: well-formed UTF-8 is taken + // as already native, anything else as KOI8-R. Cyrillic in KOI8-R is almost never valid UTF-8, + // which makes validity a reliable test, and it is what keeps a load/save cycle idempotent. + return utf8::is_valid(text) ? text : from_koi8(text); +#else + return text; +#endif +} + std::string read_data_file(const std::string &path) { std::ifstream in(path, std::ios::binary); if (!in) { return {}; } std::string raw((std::istreambuf_iterator(in)), std::istreambuf_iterator()); - return from_koi8(raw); + return from_disk_text(raw); } diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 3b42395a0..7efb544fa 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -185,6 +185,13 @@ std::string read_data_file(const std::string &path); // player files coexist without a version field. std::string from_disk_line(const char *line); +// The same for a whole file's contents. Use this, not from_koi8, for anything the engine also +// WRITES back: from_koi8 transcodes unconditionally, so a file the engine already saved in the +// native encoding would be transcoded a second time -- and since the save then writes that back, +// every Cyrillic byte doubles on each load/save cycle and the file grows exponentially (that is +// exactly what happened to cfg/mechanics/obj_sets.xml, issue #3681). +std::string from_disk_text(const std::string &text); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a diff --git a/src/utils/parser_wrapper.cpp b/src/utils/parser_wrapper.cpp index d99aedd6e..6a216ea33 100644 --- a/src/utils/parser_wrapper.cpp +++ b/src/utils/parser_wrapper.cpp @@ -22,17 +22,14 @@ DataNode::DataNode() : DataNode::DataNode(const std::filesystem::path &file_name) : DataNode() { - // Файлы конфигов лежат на диске в KOI8-R. Переводим содержимое в нативную кодировку - // движка ОДИН раз на документ, а не на каждое поле: тогда всё, что читается через - // DataNode, приходит уже в нужной кодировке (issue #3681). Под KOI8-R это тождество. - std::string raw; - { - std::ifstream in(file_name, std::ios::binary); - if (in) { - raw.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); - } - } - const std::string converted = native_text::from_koi8(raw); + // Содержимое приводится к нативной кодировке движка ОДИН раз на документ, а не на каждое + // поле: тогда всё, что читается через DataNode, приходит уже в нужной кодировке. Под KOI8-R + // это тождество (issue #3681). + // + // read_data_file, а не самодельное чтение с from_koi8: конфиг движок не только читает, но и + // пишет обратно, а безусловная перекодировка уже сохранённого в UTF-8 файла удваивала бы + // каждую кириллическую букву на каждом цикле загрузки-сохранения. + const std::string converted = native_text::read_data_file(file_name.string()); if (auto result = impl_->xml_doc->load_buffer(converted.data(), converted.size()); !result) { std::ostringstream buffer; buffer << "..." << result.description() << "\r\n" << " (file: " << file_name << ")" << "\r\n"; diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 2fff35b6e..39a6fe34f 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -352,9 +352,12 @@ int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { std::string AttrStr(const parser_wrapper::DataNode &node, const char *key, const char *def) { const char *v = node.GetValue(key); - // XML-конфиги лежат на диске в KOI8-R; переводим в нативную кодировку движка - // (под KOI8-R это тождество, под UTF-8 - перекодировка). Issue #3681. - return native_text::from_koi8((v && *v) ? std::string(v) : std::string(def)); + // Перекодировки здесь НЕТ и быть не должно: DataNode приводит содержимое файла к нативной + // кодировке один раз на документ, поэтому значение уже нативное. Пока перекодировка стояла + // ещё и здесь, каждое поле переводилось дважды, а так как конфиги движок ещё и пишет + // обратно, файл рос на каждой загрузке -- cfg/mechanics/obj_sets.xml так дорос со 120 КБ + // до 6,7 ГБ и убивал загрузку по памяти (issue #3681). + return (v && *v) ? std::string(v) : std::string(def); } } // namespace parse diff --git a/tests/native_text.cpp b/tests/native_text.cpp index ab7a153cb..8c6fd90c8 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -508,4 +508,30 @@ TEST(NativeText, SortKeyIsStableAcrossEncodings) { EXPECT_EQ(native_text::sort_key("plain ascii"), "plain ascii"); } +TEST(NativeText, FromDiskTextIsIdempotent) { + // The bug this guards against: a config the engine both reads and writes was transcoded + // unconditionally on load, so text already saved in the native encoding got transcoded a + // second time -- and since the save wrote that back, every Cyrillic byte doubled on each + // load/save cycle. cfg/mechanics/obj_sets.xml grew from 120 KB to 6.7 GB that way. + const std::string koi8 = "\xD0\xD2\xC9\xD7\xC5\xD4"; // "privet" in KOI8-R + const std::string once = native_text::from_disk_text(koi8); + const std::string twice = native_text::from_disk_text(once); + EXPECT_EQ(once, twice) << "reading back what we wrote must not change it"; + // However many times it goes through, the size must not grow. + std::string acc = koi8; + for (int i = 0; i < 10; ++i) { + acc = native_text::from_disk_text(acc); + } + EXPECT_EQ(acc, once); + + if (native_text::native_is_utf8()) { + EXPECT_EQ(once, "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); + } else { + EXPECT_EQ(once, koi8); // no conversion at all under KOI8-R + } + // ASCII is spelled the same in both encodings and must pass through untouched. + EXPECT_EQ(native_text::from_disk_text(""), ""); + EXPECT_EQ(native_text::from_disk_text(""), ""); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 89cb871dcc5adfc2cdaa409f39401b7a3e4a17de Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 6 Aug 2026 22:59:28 +0200 Subject: [PATCH 045/110] =?UTF-8?q?test(utf8):=20=D1=86=D0=B8=D0=BA=D0=BB?= =?UTF-8?q?=20"=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8=D0=BB=D0=B8=20-?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=D0=B8"=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D1=91=D0=BD=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=BC=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тест повторяет ровно то, что делает загрузчик: собрать документ, записать, прочитать через DataNode + parse::AttrStr, записать снова -- и требует, чтобы второй файл совпал с первым байт в байт. Плюс отдельная проверка, что конфиг, лежащий на диске в KOI8-R, читается в нативную кодировку правильно. Проверено, что тест ловит ту самую ошибку: с возвращённой перекодировкой на уровне поля обе проверки падают, без неё проходят. --- tests/meson.build | 1 + tests/parser_wrapper.encoding.cpp | 105 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/parser_wrapper.encoding.cpp diff --git a/tests/meson.build b/tests/meson.build index b0e0d32e7..e2af290a9 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -45,6 +45,7 @@ test_sources = files( 'utils.encoding.cpp', 'accounts.storage.cpp', 'translit_koi8.cpp', + 'parser_wrapper.encoding.cpp', 'utf8.cpp', 'native_text.cpp', 'text_semantics.cpp', diff --git a/tests/parser_wrapper.encoding.cpp b/tests/parser_wrapper.encoding.cpp new file mode 100644 index 000000000..12a7c3033 --- /dev/null +++ b/tests/parser_wrapper.encoding.cpp @@ -0,0 +1,105 @@ +// The load/save cycle of an XML config must not change what it holds (issue #3681). +// +// This pins a bug that cost a world: the document was brought into the native encoding once in +// DataNode, and then AGAIN per field in parse::AttrStr. Under KOI8-R both conversions are the +// identity, so nothing showed. Under UTF-8 every field was converted twice -- and because the +// engine writes configs back (ObjSetsLoader::Load normalises the file through save()), the damage +// accumulated: cfg/mechanics/obj_sets.xml doubled on every boot until, at 6.7 GB, reading it into +// memory killed the process. +// +// Pure ASCII: the Cyrillic fixture is spelled as UTF-8 byte escapes and brought into the native +// encoding through native_text, so the same test is correct under either build. + +#include "utils/parser_wrapper.h" +#include "utils/utils_parse.h" +#include "utils/native_text.h" +#include "utils/translit_koi8.h" + +#include + +#include +#include +#include +#include +#include + +namespace { + +// "privet, mir" in UTF-8, plus an ampersand: colour codes in real configs are written "&W", and +// the ampersand is the character XML escaping is most likely to mangle on a re-save. +const char *const kCyrillicUtf8 = + "&W" "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" ", " "\xD0\xBC\xD0\xB8\xD1\x80" "&n"; + +std::string ReadWhole(const std::filesystem::path &path) { + std::ifstream in(path, std::ios::binary); + std::ostringstream buffer; + buffer << in.rdbuf(); + return buffer.str(); +} + +// A scratch file removed on destruction, so a failing assertion cannot leave litter behind. +class TempFile { + public: + explicit TempFile(const char *name) : path_(std::filesystem::temp_directory_path() / name) {} + ~TempFile() { std::error_code ec; std::filesystem::remove(path_, ec); } + const std::filesystem::path &path() const { return path_; } + + private: + std::filesystem::path path_; +}; + +} // namespace + +TEST(ParserWrapperEncoding, SaveLoadSaveIsByteIdentical) { + const std::string text = native_text::from_koi8(codepages::Utf8ToKoi8(kCyrillicUtf8)); + TempFile file("bylins_cfg_roundtrip.xml"); + + { + auto doc = parser_wrapper::DataNode::NewDocument(); + auto root = doc.AddChild("obj_sets"); + auto set = root.AddChild("set"); + set.SetValue("name", text); + ASSERT_TRUE(doc.Save(file.path())); + } + const std::string first = ReadWhole(file.path()); + ASSERT_FALSE(first.empty()); + + // Read it back the way the loaders do and write it out again from what we read. + { + parser_wrapper::DataNode loaded(file.path()); + ASSERT_TRUE(loaded.IsNotEmpty()); + ASSERT_TRUE(loaded.GoToChild("set")); + // The value must come back exactly as it went in -- this is where the double conversion + // showed up first. + EXPECT_EQ(parse::AttrStr(loaded, "name"), text); + + auto doc = parser_wrapper::DataNode::NewDocument(); + auto root = doc.AddChild("obj_sets"); + auto set = root.AddChild("set"); + set.SetValue("name", parse::AttrStr(loaded, "name")); + ASSERT_TRUE(doc.Save(file.path())); + } + const std::string second = ReadWhole(file.path()); + + EXPECT_EQ(first.size(), second.size()) << "the file grew on a load/save cycle"; + EXPECT_EQ(first, second) << "a load/save cycle must reproduce the file byte for byte"; +} + +TEST(ParserWrapperEncoding, ReadsAConfigStoredInKoi8) { + // Configs on disk are still KOI8-R, so reading one must transcode; and the result, once + // written back, must then stay put (covered by the test above). + TempFile file("bylins_cfg_koi8.xml"); + { + std::ofstream out(file.path(), std::ios::binary); + out << "\n\n"; + } + + parser_wrapper::DataNode loaded(file.path()); + ASSERT_TRUE(loaded.IsNotEmpty()); + ASSERT_TRUE(loaded.GoToChild("set")); + EXPECT_EQ(parse::AttrStr(loaded, "name"), + native_text::from_koi8(codepages::Utf8ToKoi8(kCyrillicUtf8))); +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 6dc62a637ced80401f8cf3e19b3e9ef7465421b7 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 7 Aug 2026 05:49:08 +0200 Subject: [PATCH 046/110] =?UTF-8?q?fix(utf8):=20=D0=B7=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D1=8C=20=D0=BD=D0=B0=20=D0=B4=D0=B8=D1=81=D0=BA=20=D0=B8?= =?UTF-8?q?=D0=B4=D1=91=D1=82=20=D0=B2=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B5=20=D0=BC=D0=B8=D1=80=D0=B0,=20=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=B2=20=D0=BD=D0=B0=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Граница чтения была, границы записи не было: движок писал файлы в нативной кодировке, и первое же сохранение переводило в UTF-8 всё, к чему он прикоснулся -- сейвы, ренту, доски, список игроков, конфиги, которые сам же нормализует при загрузке. Мир при этом объявлен неизменным, а откат на KOI8-R-сборку после такого старта уже невозможен: она прочитает эти файлы как байты KOI8-R. Добавлен native_text::to_disk -- зеркало from_disk_text. Под KOI8-R тождество, под UTF-8 приведение к KOI8-R с той же транслитерацией, что и для легаси-клиента. Проведён через все точки записи: конфиги (DataNode::Save), сейвы персонажей, рента, доски, players.lst, список неодобренных имён. Файлы, которые UTF-8 намеренно (lib/text/greeting.utf8), читаются сырыми и через эту границу не проходят. --- src/administration/names.cpp | 6 ++++ src/engine/db/obj_save.cpp | 7 +++-- src/engine/db/player_index.cpp | 4 ++- src/engine/entities/char_player.cpp | 5 +++- src/engine/ui/cmd/do_score.cpp | 5 ++-- src/gameplay/clans/house.cpp | 4 +-- .../communication/boards/boards_types.cpp | 30 +++++++++++-------- src/gameplay/mechanics/sight.cpp | 12 ++++++-- src/gameplay/mechanics/title.cpp | 5 ++-- src/utils/native_text.cpp | 8 +++++ src/utils/native_text.h | 12 ++++++++ src/utils/parser_wrapper.cpp | 13 +++++++- src/utils/utils_string.cpp | 5 +++- 13 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/administration/names.cpp b/src/administration/names.cpp index 442d7a7bb..8529c729b 100644 --- a/src/administration/names.cpp +++ b/src/administration/names.cpp @@ -9,6 +9,7 @@ * $Revision$ * ************************************************************************ */ +#include "utils/native_text.h" #include "names.h" #include "utils/grammar/gender.h" @@ -128,6 +129,11 @@ static void NewNames::save() { for (const auto &it : NewNameList) { lines.push_back(it.first); } + // Граница записи: имена уходят на диск в кодировке мира (сейчас KOI8-R), зеркально + // чтению (issue #3681). + for (auto &line : lines) { + line = native_text::to_disk(line); + } MUD::StateManager().SaveLines(state::EStateFile::kPendingNames, lines); } diff --git a/src/engine/db/obj_save.cpp b/src/engine/db/obj_save.cpp index 42bf4b576..b26105816 100644 --- a/src/engine/db/obj_save.cpp +++ b/src/engine/db/obj_save.cpp @@ -2127,7 +2127,10 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) { Crash_delete_files(iplayer); return false; } - file.write(obj_content.data(), static_cast(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(on_disk.size())); file.close(); /* оставил как пример #ifndef _WIN32 @@ -2140,7 +2143,7 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) { */ 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; } diff --git a/src/engine/db/player_index.cpp b/src/engine/db/player_index.cpp index 535b3da75..43c1b1d22 100644 --- a/src/engine/db/player_index.cpp +++ b/src/engine/db/player_index.cpp @@ -384,8 +384,10 @@ void FlushPlayerIndex() { } ++saved; + // Имя уходит на диск в кодировке мира (сейчас KOI8-R) -- зеркало from_disk_line, + // которым индекс читается обратно (issue #3681). sprintf(name, "%s %ld %d %d\n", - i.name().c_str(), + native_text::to_disk(i.name()).c_str(), i.uid(), i.level, i.last_logon); fputs(name, players); } diff --git a/src/engine/entities/char_player.cpp b/src/engine/entities/char_player.cpp index 4bc9462ae..03a0b4659 100644 --- a/src/engine/entities/char_player.cpp +++ b/src/engine/entities/char_player.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2008 Krodo // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "char_player.h" #include "gameplay/core/experience.h" #include "administration/privilege.h" @@ -830,7 +831,9 @@ void Player::save_char(bool update_save_time) { // Накопленный буфер пишем на диск в бинарном режиме (байты файла == байты // буфера на всех платформах) и из него же считаем CRC -- без перечитывания // только что записанного файла. - const std::string &pfile = saved.str(); + // Граница записи: сейв уходит на диск в той же кодировке, в какой лежит остальной мир + // (сейчас KOI8-R), а не в нативной -- зеркало от from_disk_line на чтении (issue #3681). + const std::string pfile = native_text::to_disk(saved.str()); utils::CExecutionTimer wt; FILE *pf = fopen(filename, "wb"); if (!pf) { diff --git a/src/engine/ui/cmd/do_score.cpp b/src/engine/ui/cmd/do_score.cpp index bd877500b..17426e262 100644 --- a/src/engine/ui/cmd/do_score.cpp +++ b/src/engine/ui/cmd/do_score.cpp @@ -6,6 +6,7 @@ слабовидящих. */ +#include "utils/native_text.h" #include "engine/ui/color.h" #include "gameplay/affects/affect_messages.h" #include "utils/utils_string.h" @@ -118,9 +119,9 @@ void PrintBonusStateInfo(CharData *ch, std::ostringstream &out); // \todo Переписать на вывод в поток с использованием общих со "счет все" функций void PrintScoreList(CharData *ch) { sprintf(buf, "%s", MUD::RaceMessages().GetMessage(GET_RACE(ch), ch->get_sex()).c_str()); - buf[0] = LOWER(buf[0]); + native_text::copy_lower_char(buf, buf); sprintf(buf1, "%s", religion_name[GET_RELIGION(ch)][static_cast(ch->get_sex())]); - buf1[0] = LOWER(buf1[0]); + native_text::copy_lower_char(buf1, buf1); SendMsgToChar(ch, "Вы %s, %s, %s, %s, уровень %d, перевоплощений %d.\r\n", ch->get_name().c_str(), buf, MUD::Class(ch->GetClass()).GetCName(), diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index c100a72bf..4bb50adb0 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -441,7 +441,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Owner %ld is no longer exist (%s).", unique, filename.c_str()); break; } - tempMember->name[0] = UPPER(tempMember->name[0]); + native_text::capitalize_first(tempMember->name); tempMember->rank_num = 0; tempMember->money = money; tempMember->exp = exp; @@ -477,7 +477,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Member %ld is no longer exist (%s).", unique, filename.c_str()); continue; } - tempMember->name[0] = UPPER(tempMember->name[0]); + native_text::capitalize_first(tempMember->name); tempMember->rank_num = rank; tempMember->money = money; tempMember->exp = exp; diff --git a/src/gameplay/communication/boards/boards_types.cpp b/src/gameplay/communication/boards/boards_types.cpp index e970474b6..98550c167 100644 --- a/src/gameplay/communication/boards/boards_types.cpp +++ b/src/gameplay/communication/boards/boards_types.cpp @@ -143,20 +143,26 @@ void Board::Save() { log("Error open file: %s! (%s %s %d)", file_.c_str(), __FILE__, __func__, __LINE__); return; } - file << "Type: " << type_ << " " - << "Clan: " << clan_rent_ << " " - << "PersUID: " << pers_unique_ << " " - << "PersName: " << (pers_name_.empty() ? "none" : pers_name_) << "\n"; + // Собираем всё в строку и приводим к кодировке диска одним куском: граница записи -- зеркало + // границы чтения (доски читаются через from_disk_line), иначе первое же сохранение доски + // переводит её файл в UTF-8, а мир вокруг остаётся в KOI8-R (issue #3681). + std::ostringstream out; + out << "Type: " << type_ << " " + << "Clan: " << clan_rent_ << " " + << "PersUID: " << pers_unique_ << " " + << "PersName: " << (pers_name_.empty() ? "none" : pers_name_) << "\n"; for (MessageListType::const_reverse_iterator message = messages.rbegin(); message != messages.rend(); ++message) { - file << "Message: " << (*message)->num << "\n" - << (*message)->author << " " - << (*message)->unique << " " - << (*message)->level << " " - << (*message)->date << " " - << (*message)->rank << "\n" - << (*message)->subject << "~\n" - << (*message)->text << "~\n"; + out << "Message: " << (*message)->num << "\n" + << (*message)->author << " " + << (*message)->unique << " " + << (*message)->level << " " + << (*message)->date << " " + << (*message)->rank << "\n" + << (*message)->subject << "~\n" + << (*message)->text << "~\n"; } + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index f010c4f97..533ebd88b 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -913,12 +913,18 @@ void do_auto_exits(CharData *ch) { // Наконец-то добавлена отрисовка в автовыходах закрытых дверей if (EXIT(ch, door) && EXIT(ch, door)->to_room() != kNowhere) { if (EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kClosed)) { - slen += sprintf(buf + slen, "(%c) ", LOWER(*dirs[door])); + // Печатаем первую БУКВУ направления: под UTF-8 у русской буквы два байта, и %c + // выводил половину (issue #3681). + slen += sprintf(buf + slen, "("); + slen += static_cast(native_text::copy_lower_char(dirs[door], buf + slen)); + slen += sprintf(buf + slen, ") "); } else if (!EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kHidden)) { if (world[EXIT(ch, door)->to_room()]->zone_rn == world[ch->in_room]->zone_rn) { - slen += sprintf(buf + slen, "%c ", LOWER(*dirs[door])); + slen += static_cast(native_text::copy_lower_char(dirs[door], buf + slen)); + slen += sprintf(buf + slen, " "); } else { - slen += sprintf(buf + slen, "%c ", UPPER(*dirs[door])); + slen += static_cast(native_text::copy_upper_char(dirs[door], buf + slen)); + slen += sprintf(buf + slen, " "); } } } diff --git a/src/gameplay/mechanics/title.cpp b/src/gameplay/mechanics/title.cpp index f539269f4..16704b143 100644 --- a/src/gameplay/mechanics/title.cpp +++ b/src/gameplay/mechanics/title.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2006 Krodo // Part of Bylins http://www.bylins.su +#include "utils/native_text.h" #include "title.h" #include "gameplay/economics/currencies.h" #include "engine/db/player_index.h" @@ -134,8 +135,8 @@ void TitleSystem::do_title(CharData *ch, char *argument, int/* cmd*/, int/* subc utils::Trim(title); utils::Trim(pre_title); if (!pre_title.empty()) { - sprintf(buf2, "%c%s", UPPER(pre_title.substr(0, 1).c_str()[0]), pre_title.substr(1).c_str()); - pre_title = buf2; + // Поднимаем первую букву целиком, а не первый байт (issue #3681). + native_text::capitalize_first(pre_title); } if (!pre_title.empty() && !check_pre_title(pre_title, ch)) return; if (!title.empty() && !check_title(title, ch)) return; diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 103e4ef7b..8a4538ca1 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -769,6 +769,14 @@ std::string from_disk_text(const std::string &text) { #endif } +std::string to_disk(const std::string &text) { +#ifdef INTERNAL_ENCODING_UTF8 + return to_koi8(text); +#else + return text; +#endif +} + std::string read_data_file(const std::string &path) { std::ifstream in(path, std::ios::binary); if (!in) { diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 7efb544fa..0d1f276c2 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -192,6 +192,18 @@ std::string from_disk_line(const char *line); // exactly what happened to cfg/mechanics/obj_sets.xml, issue #3681). std::string from_disk_text(const std::string &text); +// The write side of the same boundary, and the exact mirror of from_disk_text: whatever the +// engine puts on disk goes out in the encoding the disk format is in, which during the migration +// is still KOI8-R. Identity under KOI8-R; under UTF-8 the text is reduced and transcoded exactly +// as it is for a legacy client (see to_koi8). +// +// Read and write MUST stay symmetric. If the engine writes the native encoding while the rest of +// the world is KOI8-R, then the first save quietly converts every file it touches, rolling back +// to a KOI8-R build stops being possible, and the world is no longer the world we started with. +// The files that are deliberately UTF-8 (lib/text/greeting.utf8) are read raw and never go +// through here (issue #3681). +std::string to_disk(const std::string &text); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a diff --git a/src/utils/parser_wrapper.cpp b/src/utils/parser_wrapper.cpp index 6a216ea33..1aaf3661e 100644 --- a/src/utils/parser_wrapper.cpp +++ b/src/utils/parser_wrapper.cpp @@ -122,7 +122,18 @@ bool DataNode::Save(const std::filesystem::path &file) const { decl.append_attribute("encoding"); } decl.attribute("encoding").set_value("koi8-r"); - return doc.save_file(file.string().c_str()); + // Пишем в той же кодировке, в какой файл лежит на диске (сейчас KOI8-R), а не в нативной: + // граница записи обязана быть зеркалом границы чтения, иначе первое же сохранение молча + // переводит файл в UTF-8 и откат на KOI8-R-сборку становится невозможен (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + const std::string on_disk = native_text::to_disk(xml.str()); + std::ofstream out(file, std::ios::binary); + if (!out) { + return false; + } + out.write(on_disk.data(), static_cast(on_disk.size())); + return out.good(); } std::string DataNode::ToXmlString() const { diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index b40107c9b..467699c03 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -1186,7 +1186,10 @@ std::string ExpFormat(long long exp) { void name_convert(std::string &text) { if (!text.empty()) { utils::ConvertToLow(text); - *text.begin() = UPPER(*text.begin()); + // Первую букву поднимаем целиком, а не первый байт: под UTF-8 у русской буквы их два, + // и байтовый UPPER превращал имя в мусор -- "имя щщщщщ одобрить" не находило + // персонажа (issue #3681). + native_text::capitalize_first(text); } } From 9a926a8ce409b53943de379ba777136199a76254 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 7 Aug 2026 14:37:54 +0200 Subject: [PATCH 047/110] =?UTF-8?q?fix(utf8):=20=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D1=8F=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=20KOI8-R,=20=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=20=D0=BD=D0=B0=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateFileName и четыре его копии в house.cpp гоняли строку через байтовый AtoL. Под UTF-8 это резало русскую букву пополам и оставляло её в имени файла как есть, поэтому движок не находил уже существующие файлы и заводил рядом новые: в мире появилось 70 лишних досок с кириллицей в имени рядом со 105 старыми. У дружин последствия были бы хуже -- сундук, mod и pk-лог уехали бы в каталог с другим именем, то есть пропали бы. Строка приводится к KOI8-R и дальше идёт прежний байтовый код: под KOI8-R to_koi8 -- тождество, так что там не меняется ни байт. Копии в house.cpp и logger.cpp заменены вызовом CreateFileName. Заодно поправлены операции над ПЕРВЫМ СИМВОЛОМ, которые работали с первым байтом: name_convert (из-за неё "имя <персонаж> одобрить" не находило персонажа), первая буква направления в списке выходов, титул, имена в дружине, раса и вера в "счет". --- src/gameplay/clans/house.cpp | 19 ++++++++----------- src/utils/logger.cpp | 12 ++++++------ src/utils/utils_string.cpp | 8 ++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 4bb50adb0..643c4ec19 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -4,6 +4,7 @@ * (c) 2005 Krodo * ******************************************************************************/ +#include "utils/utils_string.h" #include "house.h" #include "utils/russian_keys.h" #include "utils/native_text.h" @@ -2436,9 +2437,11 @@ void Clan::save_chest() { log("Save obj: %s", this->abbrev.c_str()); ObjSaveSync::check(this->GetRent(), ObjSaveSync::CLAN_SAVE); + // Имя каталога дружины строится из байтов KOI8-R, как и до миграции: под UTF-8 байтовый + // AtoL резал русскую букву пополам, и добро дружины уезжало в каталог с другим именем + // (issue #3681). std::string buffer = this->abbrev; - for (unsigned i = 0; i != buffer.length(); ++i) - buffer[i] = LOWER(codepages::AtoL(buffer[i])); + CreateFileName(buffer); std::string filename = LIB_CLANS + buffer + "/" + buffer + ".obj"; for (auto chest : world[GetRoomRnum(this->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { @@ -2498,9 +2501,7 @@ void Clan::ChestLoad() { for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { buffer = (*clan)->abbrev; - for (unsigned i = 0; i != buffer.length(); ++i) { - buffer[i] = LOWER(codepages::AtoL(buffer[i])); - } + CreateFileName(buffer); std::string filename = LIB_CLANS + buffer + "/" + buffer + ".obj"; //лоадим сундук. в зонах его лоадить не нужно. @@ -2601,9 +2602,7 @@ void Clan::ChestUpdate() { // * Запись сообщения дружины в файл и поле клана. void Clan::write_mod(const std::string &arg) { std::string abbrev = this->get_abbrev(); - for (unsigned i = 0; i != abbrev.length(); ++i) { - abbrev[i] = LOWER(codepages::AtoL(abbrev[i])); - } + CreateFileName(abbrev); std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".mod"; std::ofstream file(filename.c_str()); @@ -4230,9 +4229,7 @@ std::string Clan::get_remember(unsigned int num, int flag) const { std::string Clan::get_file_abbrev() const { std::string text = this->get_abbrev(); - for (unsigned i = 0; i != text.length(); ++i) { - text[i] = LOWER(codepages::AtoL(text[i])); - } + CreateFileName(text); return text; } diff --git a/src/utils/logger.cpp b/src/utils/logger.cpp index ad3a0bd2c..7f5634fe4 100644 --- a/src/utils/logger.cpp +++ b/src/utils/logger.cpp @@ -41,12 +41,12 @@ void pers_log(CharData *ch, const char *format, ...) { } if (!ch->desc->pers_log) { - char filename[128], name[64], *ptr; - strcpy(name, GET_NAME(ch)); - for (ptr = name; *ptr; ptr++) { - *ptr = LOWER(codepages::AtoL(*ptr)); - } - sprintf(filename, "%s/perslog/%s.log", runtime_config.log_dir().c_str(), name); + char filename[128]; + // Имя файла персонального лога строится из байтов KOI8-R, как и до миграции + // (issue #3681). + std::string name = GET_NAME(ch); + CreateFileName(name); + sprintf(filename, "%s/perslog/%s.log", runtime_config.log_dir().c_str(), name.c_str()); ch->desc->pers_log = fopen(filename, "a"); if (!ch->desc->pers_log) { log("SYSERR: error open %s (%s %s %d)", filename, __FILE__, __func__, __LINE__); diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 467699c03..74015f393 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -1161,6 +1161,14 @@ std::string sprintGender(int gender_value) { // замена в name русских символов на англ в нижнем регистре (для файлов) void CreateFileName(std::string &name) { + // Имя файла собирается по байтам KOI8-R -- ровно так, как собиралось до миграции. Иначе у + // уже существующего персонажа доска получит другое имя файла, и старая просто потеряется: + // под UTF-8 байтовый AtoL резал русскую букву пополам и оставлял её в имени как есть, отчего + // рядом со 105 старыми досками завелось 70 новых, с кириллицей в имени (issue #3681). + // + // Приводим строку к KOI8-R и дальше работаем прежним байтовым кодом: под KOI8-R to_koi8 -- + // тождество, так что поведение там не меняется ни на байт. + name = native_text::to_koi8(name); for (unsigned i = 0; i != name.length(); ++i) name[i] = LOWER(codepages::AtoL(name[i])); } From 6436526487118879a4751f43eb39e138f7daae0e Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 04:24:46 +0200 Subject: [PATCH 048/110] =?UTF-8?q?fix(utf8):=20=D0=B0=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=BC?= =?UTF-8?q?,=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=201=20--=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=20=D0=B8=20=D1=81=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Разобраны все места, где регистр или сравнение делались по байту, а не по символу. Каждое просмотрено глазами: часть оказалась безопасной (сравнение внутри str_cmp уже под проверкой native_is_utf8; switch по имени файла -- там чистая латиница после транслитерации; CreateFileName -- намеренно байтовый, после приведения к KOI8-R). Починено то, что реально ломалось: - str_str -- регистронезависимый поиск подстроки шёл по байтам, под UTF-8 это и пропускало совпадения, и давало ложные на границе байтов; - CompareParam (обе перегрузки) -- то же самое, а через неё разбираются параметры команд и конфигов; - первое слово команды скрипта (три копии: загрузка мира, boot, OLC); - слово из аргумента скрипта в dg_comm; - название напитка в осмотре предмета; - имя при проверке на запрещённые сочетания; - аббревиатура дружины в переменных скриптов; - сортировка списка дружины по первой букве имени -- индекс буквы в алфавите искался strpbrk по одному байту. --- src/administration/names.cpp | 4 +--- src/engine/boot/boot_data_files.cpp | 4 +++- src/engine/db/world_data_source_base.cpp | 5 ++++- src/engine/scripting/dg_comm.cpp | 11 ++++++++--- src/engine/scripting/dg_olc.cpp | 6 +++--- src/engine/scripting/dg_scripts.cpp | 3 +-- src/gameplay/clans/house.cpp | 23 +++++++++++++++++------ src/gameplay/mechanics/sight.cpp | 2 +- src/utils/utils_parse.cpp | 15 +++++++++++---- src/utils/utils_string.cpp | 13 ++++++++----- 10 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/administration/names.cpp b/src/administration/names.cpp index 8529c729b..43887b5b0 100644 --- a/src/administration/names.cpp +++ b/src/administration/names.cpp @@ -403,9 +403,7 @@ int IsValidName(char *newname) { // change to lowercase char tempname[kMaxInputLength]; strcpy(tempname, newname); - for (std::size_t i = 0; tempname[i]; i++) { - tempname[i] = LOWER(tempname[i]); - } + native_text::to_lower(tempname); // Does the desired name contain a string in the invalid list? for (std::size_t i = 0; i < num_invalid; i++) { diff --git a/src/engine/boot/boot_data_files.cpp b/src/engine/boot/boot_data_files.cpp index 65f0dc22e..6b2df297b 100644 --- a/src/engine/boot/boot_data_files.cpp +++ b/src/engine/boot/boot_data_files.cpp @@ -343,7 +343,9 @@ void TriggersFile::LoadDgTriggerScript(Trigger *trig, const std::string &cmds, i // lowercase the command (first word) for faster comparison at runtime auto it = (*ptr)->cmd.begin(); while (it != (*ptr)->cmd.end() && (*it == ' ' || *it == '\t')) ++it; - while (it != (*ptr)->cmd.end() && *it != ' ') { *it = LOWER(*it); ++it; } + for (; it != (*ptr)->cmd.end() && *it != ' '; it += native_text::char_bytes(&*it)) { + native_text::copy_lower_char(&*it, &*it); + } ptr = &(*ptr)->next; } if (pos_end != std::string::npos) diff --git a/src/engine/db/world_data_source_base.cpp b/src/engine/db/world_data_source_base.cpp index 22bb0c16a..32dd51937 100644 --- a/src/engine/db/world_data_source_base.cpp +++ b/src/engine/db/world_data_source_base.cpp @@ -1,6 +1,7 @@ // Part of Bylins http://www.mud.ru // Base class for world data sources - implementation +#include "utils/native_text.h" #include "world_data_source_base.h" #include "db.h" #include "obj_prototypes.h" @@ -57,7 +58,9 @@ void WorldDataSourceBase::ParseTriggerScript(Trigger *trig, const std::string &s // lowercase the command (first word) for faster comparison at runtime auto it = cmd->cmd.begin(); while (it != cmd->cmd.end() && (*it == ' ' || *it == '\t')) ++it; - while (it != cmd->cmd.end() && *it != ' ') { *it = LOWER(*it); ++it; } + for (; it != cmd->cmd.end() && *it != ' '; it += native_text::char_bytes(&*it)) { + native_text::copy_lower_char(&*it, &*it); + } if (!head) { diff --git a/src/engine/scripting/dg_comm.cpp b/src/engine/scripting/dg_comm.cpp index 2e49f1ca9..01335702a 100644 --- a/src/engine/scripting/dg_comm.cpp +++ b/src/engine/scripting/dg_comm.cpp @@ -8,6 +8,7 @@ * $Revision$ * **************************************************************************/ +#include "utils/native_text.h" #include "engine/entities/obj_data.h" #include "utils/grammar/gender.h" #include "dg_scripts.h" @@ -31,10 +32,14 @@ char *any_one_name(char *argument, char *first_arg) { * Библиотечная функция ispunct() неправильно работает для русского языка * (по крайней мере у меня). Пока закоментировал. */ + // Шагаем по символу, а не по байту: под UTF-8 русская буква занимает два, и побайтовое + // приведение к нижнему регистру ломало её (issue #3681). for (arg = first_arg; *argument && !isspace(*argument) && - (!ispunct(*argument) || *argument == '#' || *argument == '-'); - arg++, argument++) - *arg = LOWER(*argument); + (!ispunct(*argument) || *argument == '#' || *argument == '-');) { + const std::size_t written = native_text::copy_lower_char(argument, arg); + arg += written; + argument += written; + } *arg = '\0'; return argument; diff --git a/src/engine/scripting/dg_olc.cpp b/src/engine/scripting/dg_olc.cpp index 39c2a8c91..0cdaf5767 100644 --- a/src/engine/scripting/dg_olc.cpp +++ b/src/engine/scripting/dg_olc.cpp @@ -14,6 +14,7 @@ * $Revision$ * **************************************************************************/ +#include "utils/native_text.h" #include "dg_olc.h" #include @@ -153,9 +154,8 @@ void TrigeditCompileDgCmdlist(Trigger *trig, const std::string &storage_str) while (it != str.end() && (*it == ' ' || *it == '\t')) { ++it; } - while (it != str.end() && *it != ' ') { - *it = LOWER(*it); - ++it; + for (; it != str.end() && *it != ' '; it += native_text::char_bytes(&*it)) { + native_text::copy_lower_char(&*it, &*it); } }; diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 69e261ae3..b0d0fa529 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -2759,8 +2759,7 @@ void find_replacement(void *go, } else if (!str_cmp(field, "clan")) { if (CLAN(mob)) { snprintf(str, str_size, "%s", CLAN(mob)->GetAbbrev()); - for (i = 0; str[i]; i++) - str[i] = LOWER(str[i]); + native_text::to_lower(str); } else snprintf(str, str_size, "0"); } else if (!str_cmp(field, "ClanRank")) { diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 643c4ec19..dad4263b7 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -3640,7 +3640,6 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { // т.к. в кои8-р русские буквы не попорядку const char *pSortAlph = "яюэьыъщшчцхфутсрпонмлкизжёедгвба"; // первая буква имени - char pcFirstChar[2]; // для избежания путаницы с именами фильтр начинается со знака "!" // формат команды: @@ -3752,11 +3751,23 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { case SORT_STAT_BY_LOGON: lSortParam = GetLastlogonByUnique(it.first); break; case SORT_STAT_BY_NAME: { - pcFirstChar[0] = LOWER(it.second->name[0]); - pcFirstChar[1] = '\0'; - char const *pTmp = strpbrk(pSortAlph, pcFirstChar); - if (pTmp) lSortParam = pTmp - pSortAlph; // индекс первой буквы в массиве - else lSortParam = pcFirstChar[0]; // или не русская буква или я хз + // Индекс первой БУКВЫ имени в алфавите. Под UTF-8 буква занимает два байта, и + // strpbrk по одному байту находил бы что попало, а то и вовсе ничего + // (issue #3681). Идём по алфавиту символ за символом. + char first_letter[8] = {0}; + native_text::copy_lower_char(it.second->name.c_str(), first_letter); + lSortParam = -1; + long letter_index = 0; + for (const char *p = pSortAlph; *p; p += native_text::char_bytes(p), ++letter_index) { + if (native_text::chars_equal_ci(p, first_letter)) { + lSortParam = letter_index; + break; + } + } + if (lSortParam < 0) { + // не русская буква -- пусть уедет в конец, но в стабильном порядке + lSortParam = static_cast(native_text::first_char_code(first_letter)); + } break; } // на всякий случай diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 533ebd88b..8140a442d 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -1344,7 +1344,7 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s if (object->get_type() == EObjType::kLiquidContainer) { char *tmp = drinkcon::daig_filling_drink(object, ch); char tmp2[128]; - *tmp = LOWER(*tmp); + native_text::copy_lower_char(tmp, tmp); sprintf(tmp2, "(%s)", tmp); strcat(buf2, tmp2); } diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 39a6fe34f..223b742c6 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -505,11 +505,16 @@ bool CompareParam(const std::string &buffer, const char *str, bool full) { return false; } + // Посимвольно, а не побайтово: под UTF-8 регистр русской буквы по одному байту не берётся + // (issue #3681). Шаг обеих строк -- на длину символа, они здесь всегда в одной кодировке. std::string::size_type i; - for (i = 0; i != buffer.length() && *str; ++i, ++str) { - if (LOWER(buffer[i]) != LOWER(*str)) { + for (i = 0; i != buffer.length() && *str;) { + if (!native_text::chars_equal_ci(buffer.c_str() + i, str)) { return false; } + const std::size_t step = native_text::char_bytes(str); + i += step; + str += step; } if (i == buffer.length()) { @@ -526,11 +531,13 @@ bool CompareParam(const std::string &buffer, const std::string &buffer2, bool fu return false; } + // Посимвольно, как и в перегрузке выше (issue #3681). std::string::size_type i; - for (i = 0; i != buffer.length() && i != buffer2.length(); ++i) { - if (LOWER(buffer[i]) != LOWER(buffer2[i])) { + for (i = 0; i != buffer.length() && i != buffer2.length();) { + if (!native_text::chars_equal_ci(buffer.c_str() + i, buffer2.c_str() + i)) { return false; } + i += native_text::char_bytes(buffer.c_str() + i); } if (i == buffer.length()) { diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 74015f393..6668359c6 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -830,15 +830,18 @@ char *str_str(const char *cs, const char *ct) { if (!cs || !ct) { return nullptr; } + // Сравниваем и шагаем ПО СИМВОЛАМ: под UTF-8 побайтовое приведение регистра для кириллицы + // неверно (таблица регистра -- байтовая, KOI8-R), и поиск подстроки то не находил совпадения, + // то находил ложные на границе байтов (issue #3681). while (*cs) { const char *t = ct; - while (*cs && (LOWER(*cs) != LOWER(*t))) { - cs++; + while (*cs && !native_text::chars_equal_ci(cs, t)) { + cs += native_text::char_bytes(cs); } char *s = (char*)cs; - while (*t && *cs && (LOWER(*cs) == LOWER(*t))) { - t++; - cs++; + while (*t && *cs && native_text::chars_equal_ci(cs, t)) { + t += native_text::char_bytes(t); + cs += native_text::char_bytes(cs); } if (!*t) { return s; From 6b6c66932a9791bf0bf9d19d296545126be2a5c5 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 04:35:40 +0200 Subject: [PATCH 049/110] =?UTF-8?q?fix(utf8):=20=D0=B0=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B9,=20?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D1=8C=202=20--=20=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA?= =?UTF-8?q?=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Просмотрены все 53 printf-выравнивания и 86 std::setw. Большинство оказалось безопасным: setw над счётчиками и числами (одни только счётчики дружин -- 39 штук), нумерация файлов при записи YAML, ASCII-значения ON/OFF и имена имм-команд в "покажи"/"установи", разряды опыта в "уровни". Починено то, где выравнивается русский текст: - настройки игрока ("переключатели") -- поля "нельзя/можно", "полный/краткий", "нет/MCCPv2", "Нет": колонки разъезжались на самом частом экране; - посылки -- имена отправителя и получателя в рамке; - статистика по классам, по заклинаниям, по мобам; - список материалов в "напечатать броню"; - список веток команды; - список сайтов дружин; - в "статистика" -- имя класса и русские заголовки строк; - в "стат" -- название аффекта и направление выхода. Добавлен native_text::pad_right: то же, что "%-Ns", но ширина в символах. Под KOI8-R -- байт в байт прежнее поведение. Есть тест. --- src/engine/ui/cmd/do_statistic.cpp | 15 ++++++++------- src/engine/ui/cmd/do_toggle.cpp | 23 ++++++++++++----------- src/engine/ui/cmd_god/do_print_armor.cpp | 3 ++- src/engine/ui/cmd_god/do_stat.cpp | 10 ++++++---- src/engine/ui/commands.cpp | 3 ++- src/gameplay/clans/house.cpp | 3 +-- src/gameplay/communication/parcel.cpp | 4 ++-- src/gameplay/statistics/mob_stat.cpp | 5 ++--- src/gameplay/statistics/spell_usage.cpp | 5 +++-- src/utils/native_text.cpp | 9 +++++++++ src/utils/native_text.h | 6 ++++++ tests/native_text.cpp | 17 +++++++++++++++++ 12 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/engine/ui/cmd/do_statistic.cpp b/src/engine/ui/cmd/do_statistic.cpp index fc87acded..4f7e817f2 100644 --- a/src/engine/ui/cmd/do_statistic.cpp +++ b/src/engine/ui/cmd/do_statistic.cpp @@ -6,6 +6,7 @@ \detail Detail description. */ +#include #include "engine/entities/char_data.h" #include "engine/db/global_objects.h" #include "engine/ui/color.h" @@ -63,7 +64,7 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ const int class_name_col_width{15}; const int number_col_width{3}; for (const auto &it : players) { - out << std::left << std::setw(class_name_col_width) << MUD::Class(it.first).GetPluralName() << " " + out << fmt::format("{:<{}}", MUD::Class(it.first).GetPluralName(), class_name_col_width) << " " << kColorBoldRed << "[" << kColorBoldCyn << std::setw(number_col_width) << std::right << it.second.first + it.second.second << kColorBoldRed << "|" << kColorBoldCyn @@ -82,22 +83,22 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ const int headline_width{33}; - out << std::left << std::setw(headline_width) << " Всего игроков:"; + out << fmt::format("{:<{}}", " Всего игроков:", headline_width); PrintValue(out, number_col_width, total); - out << std::left << std::setw(headline_width) << " Игроков выше|ниже 25 уровня:"; + out << fmt::format("{:<{}}", " Игроков выше|ниже 25 уровня:", headline_width); PrintPair(out, number_col_width, hilvl, lowlvl); - out << std::left << std::setw(headline_width) << " Игроков с перевоплощениями|без:"; + out << fmt::format("{:<{}}", " Игроков с перевоплощениями|без:", headline_width); PrintPair(out, number_col_width, rem, norem); - out << std::left << std::setw(headline_width) << " Клановых|внеклановых игроков:"; + out << fmt::format("{:<{}}", " Клановых|внеклановых игроков:", headline_width); PrintPair(out, number_col_width, clan, noclan); - out << std::left << std::setw(headline_width) << " Игроков с флагами ПК|без ПК:"; + out << fmt::format("{:<{}}", " Игроков с флагами ПК|без ПК:", headline_width); PrintPair(out, number_col_width, pk, nopk); - out << std::left << std::setw(headline_width) << " Героев (без ПК) | Тварей убито:"; + out << fmt::format("{:<{}}", " Героев (без ПК) | Тварей убито:", headline_width); const int kills_col_width{5}; PrintPair(out, kills_col_width, char_stat::players_killed, char_stat::mobs_killed); out << "\r\n"; diff --git a/src/engine/ui/cmd/do_toggle.cpp b/src/engine/ui/cmd/do_toggle.cpp index b3dcc7000..2bfaed51d 100644 --- a/src/engine/ui/cmd/do_toggle.cpp +++ b/src/engine/ui/cmd/do_toggle.cpp @@ -2,6 +2,7 @@ // Created by Sventovit on 07.09.2024. // +#include "utils/native_text.h" #include "engine/entities/char_data.h" #include "gameplay/clans/house.h" @@ -41,7 +42,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { " Сжатый режим : %-3s \r\n" " Повтор команд : %-3s " " Обращения : %-3s " - " Кто-то : %-6s \r\n" + " Кто-то : %s \r\n" " Болтать : %-3s " " Орать : %-3s \r\n" " Аукцион : %-3s " @@ -49,7 +50,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { " Автозаучивание: %-3s \r\n" " Призыв : %-3s " " Автозавершение: %-3s " - " Группа (вид) : %-7s \r\n" + " Группа (вид) : %s \r\n" " Без двойников : %-3s " " Автопомощь : %-3s " " Автодележ : %-3s \r\n" @@ -59,13 +60,13 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { " Трусость : %-3s " " Ширина экрана : %-3d " " Высота экрана : %-3d \r\n" - " Сжатие : %-6s " + " Сжатие : %s " " Новости (вид) : %-5s " " Доски : %-3s \r\n" " Хранилище : %-8s" " Пклист : %-3s " " Политика : %-3s \r\n" - " Пкформат : %-6s " + " Пкформат : %s " " Соклановцы : %-8s" " Оффтоп : %-3s \r\n" " Потеря связи : %-3s " @@ -76,7 +77,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { BoolToOnOffStr(ch->IsFlagged(EPrf::kCompact)), (ch->IsFlagged(EPrf::kNoRepeat) ? "NO" : "YES"), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoTell)), - ch->IsFlagged(EPrf::kNoInvistell) ? "нельзя" : "можно", + native_text::pad_right(ch->IsFlagged(EPrf::kNoInvistell) ? "нельзя" : "можно", 6).c_str(), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoGossip)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoHoller)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoAuction)), @@ -84,7 +85,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { BoolToOnOffStr(ch->IsFlagged(EPrf::kAutomem)), BoolToOnOffStr(ch->IsFlagged(EPrf::KSummonable)), BoolToOnOffStr(ch->IsFlagged(EPrf::kGoAhead)), - ch->IsFlagged(EPrf::kShowGroup) ? "полный" : "краткий", + native_text::pad_right(ch->IsFlagged(EPrf::kShowGroup) ? "полный" : "краткий", 7).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kNoClones)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutoassist)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutosplit)), @@ -95,7 +96,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { (ch)->player_specials->saved.stringLength, (ch)->player_specials->saved.stringWidth, #if defined(HAVE_ZLIB) - ch->desc->deflate == nullptr ? "нет" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), + native_text::pad_right(ch->desc->deflate == nullptr ? "нет" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), 6).c_str(), #else "N/A", #endif @@ -104,7 +105,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { GetChestMode(ch).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kPklMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kPolitMode)), - ch->IsFlagged(EPrf::kPkFormatMode) ? "краткий" : "полный", + native_text::pad_right(ch->IsFlagged(EPrf::kPkFormatMode) ? "краткий" : "полный", 6).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kClanmembersMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kOfftopMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAntiDcMode)), @@ -114,19 +115,19 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { if ((ch)->player_specials->saved.ntfyExchangePrice > 0) { sprintf(buf, " Уведомления : %-7ld ", (ch)->player_specials->saved.ntfyExchangePrice); } else { - sprintf(buf, " Уведомления : %-7s ", "Нет"); + sprintf(buf, " Уведомления : %s ", native_text::pad_right("Нет", 7).c_str()); } SendMsgToChar(buf, ch); snprintf(buf, kMaxStringLength, " Карта : %-3s " " Вход в зону : %-3s \r\n" - " Магщиты (вид) : %-8s" + " Магщиты (вид) : %s" " Автопризыв : %-5s " " Маппер : %-3s \r\n" " Контроль IP : %-6s ", BoolToOnOffStr(ch->IsFlagged(EPrf::kDrawMap)), BoolToOnOffStr(ch->IsFlagged(EPrf::kShowZoneNameOnEnter)), - (ch->IsFlagged(EPrf::kBriefShields) ? "краткий" : "полный"), + native_text::pad_right(ch->IsFlagged(EPrf::kBriefShields) ? "краткий" : "полный", 8).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutonosummon)), BoolToOnOffStr(ch->IsFlagged(EPrf::kMapper)), BoolToOnOffStr(ch->IsFlagged(EPrf::kIpControl))); diff --git a/src/engine/ui/cmd_god/do_print_armor.cpp b/src/engine/ui/cmd_god/do_print_armor.cpp index d0797169f..9fa366274 100644 --- a/src/engine/ui/cmd_god/do_print_armor.cpp +++ b/src/engine/ui/cmd_god/do_print_armor.cpp @@ -6,6 +6,7 @@ \detail Detail description. */ +#include #include "engine/entities/char_data.h" #include "utils/russian_keys.h" #include "utils/native_text.h" @@ -314,7 +315,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { out << " " << std::setw(2) << it->first << " | " << std::setw(7) << obj->get_vnum() << " | " - << std::setw(14) << material_name[obj->get_material()] << " | " + << fmt::format("{:>14}", material_name[obj->get_material()]) << " | " << obj->get_PName(grammar::ECase::kNom) << "\r\n"; for (int i = 0; i < kMaxObjAffect; i++) { diff --git a/src/engine/ui/cmd_god/do_stat.cpp b/src/engine/ui/cmd_god/do_stat.cpp index 6f058ea5e..da239904f 100644 --- a/src/engine/ui/cmd_god/do_stat.cpp +++ b/src/engine/ui/cmd_god/do_stat.cpp @@ -616,13 +616,15 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { // Routine to show what spells a char is affected by if (!k->affected.empty()) { for (const auto &aff : k->affected) { - std::string sline = fmt::sprintf("Заклинания: (%3d%s|%s) %s%-21s%s ", + std::string sline = fmt::sprintf("Заклинания: (%3d%s|%s) %s%s%s ", aff->duration + 1, (aff->battleflag.get(kAfPulsedec)) || (aff->battleflag.get(kAfSameTime)) ? "плс" : "мин", (aff->battleflag.get(kAfBattledec)) || (aff->battleflag.get(kAfSameTime)) ? "рнд" : "мин", kColorCyn, // issue.affect-migration: affect name by its own identity (affect_type), spell fallback. - affects::AffectMsg(aff->affect_type, affects::EAffectMsgType::kShortDesc).c_str(), + // Ширина поля -- в символах: printf меряет %-21s в байтах (issue #3681). + native_text::pad_right( + affects::AffectMsg(aff->affect_type, affects::EAffectMsgType::kShortDesc), 21).c_str(), kColorNrm); bool has_modifier = aff->modifier != 0; if (has_modifier) { @@ -1279,8 +1281,8 @@ void do_stat_room(CharData *ch, const int rnum = 0) { GET_ROOM_VNUM(rm->dir_option[i]->to_room()), kColorNrm); sprintbit(rm->dir_option[i]->exit_info.get_plane(0), exit_bits, tmpBuf, sizeof(tmpBuf)); snprintf(buf, sizeof(buf), - "Выход %s%-5s%s: Ведет в : [%s], Ключ: [%5d], Название: %s (%s), Тип: %s\r\n", - kColorCyn, dirs[i], kColorNrm, smallBuf, + "Выход %s%s%s: Ведет в : [%s], Ключ: [%5d], Название: %s (%s), Тип: %s\r\n", + kColorCyn, native_text::pad_right(dirs[i], 5).c_str(), kColorNrm, smallBuf, rm->dir_option[i]->key, rm->dir_option[i]->keyword ? rm->dir_option[i]->keyword : "Нет(дверь)", rm->dir_option[i]->vkeyword ? rm->dir_option[i]->vkeyword : "Нет(дверь)", tmpBuf); diff --git a/src/engine/ui/commands.cpp b/src/engine/ui/commands.cpp index 02e64349e..80210898c 100644 --- a/src/engine/ui/commands.cpp +++ b/src/engine/ui/commands.cpp @@ -1,3 +1,4 @@ +#include #include "commands.h" #include "utils/utils.h" #include "engine/core/comm.h" @@ -233,7 +234,7 @@ void CommandEmbranchmentImplementation::print_branches_list(std::stringstream &s ss << "\r\n"; for (const auto &branch : m_branches.trie()) { const std::string &prefix = branch.prefix(); - ss << " " << std::setw(m_branches.max_length()) << prefix + ss << " " << fmt::format("{:>{}}", prefix, m_branches.max_length()) << " - " << m_branches.handlers().at(prefix)->get_help_line() << "\r\n"; } ss << "\r\n"; diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index dad4263b7..f97b2ff2c 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -4556,8 +4556,7 @@ void init_xhelp() { " Список сайтов дружин:\r\n\r\n"; for (const auto &i : Clan::ClanList) { - out << " $COLORW" << std::setw(7) << std::left - << i->GetAbbrev() << "$COLORn -- $COLORC" + out << " $COLORW" << fmt::format("{:<7}", i->GetAbbrev()) << "$COLORn -- $COLORC" << (i->get_web_url().empty() ? "$COLORW[ НЕТ ИНФОРМАЦИИ ]" : i->get_web_url()) << "$COLORn\r\n"; } diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index f7300e389..a1834d206 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -457,8 +457,8 @@ void fill_ex_desc(CharData *ch, ObjData *obj, std::string sender) { "неуклюжего взлома - царская служба безопасности бдит...\r\n" "На табличке сбоку видны надписи:\r\n\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; - out << "| Отправитель: " << std::setw(size) << sender - << " |\r\n| Получатель: " << std::setw(size) << GET_NAME(ch) << " |\r\n"; + out << "| Отправитель: " << fmt::format("{:>{}}", sender, size) + << " |\r\n| Получатель: " << fmt::format("{:>{}}", GET_NAME(ch), size) << " |\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; obj->set_ex_description("посылка бандероль пакет ящик parcel box case chest", out.str().c_str()); diff --git a/src/gameplay/statistics/mob_stat.cpp b/src/gameplay/statistics/mob_stat.cpp index 99aff072d..5c51b8c4d 100644 --- a/src/gameplay/statistics/mob_stat.cpp +++ b/src/gameplay/statistics/mob_stat.cpp @@ -58,7 +58,7 @@ void AddClassExp(ECharClass class_id, int exp) { std::string PrintClassExpStat(const ECharClass id, unsigned long long top_exp) { std::ostringstream out; - out << std::left << std::setw(15) << MUD::Class(id).GetPluralName() << " " << std::left << kColorBoldCyn; + out << fmt::format("{:<15}", MUD::Class(id).GetPluralName()) << " " << std::left << kColorBoldCyn; const int points_amount{10}; int stars{0}; if (top_exp > 0) { @@ -524,8 +524,7 @@ void ShowZoneMobKillsStat(CharData *ch, ZoneVnum zone_vnum, int months) { " vnum : имя : pk : группа = убийств (n3=100 моба убили 100 раз втроем)\r\n\r\n"; for (auto & i : sort_list) { - out << i.first << " : " << std::setw(20) - << PrintMobName(i.first, 20) << " : " + out << i.first << " : " << fmt::format("{:>20}", PrintMobName(i.first, 20)) << " : " << i.second.kills.at(0) << " :"; for (int g = 1; g <= kMaxGroupSize; ++g) { if (i.second.kills.at(g) > 0) { diff --git a/src/gameplay/statistics/spell_usage.cpp b/src/gameplay/statistics/spell_usage.cpp index 7838eaf43..cbdbadb8d 100644 --- a/src/gameplay/statistics/spell_usage.cpp +++ b/src/gameplay/statistics/spell_usage.cpp @@ -6,6 +6,7 @@ \detail Detail description. */ +#include #include "spell_usage.h" #include "engine/boot/boot_data_files.h" @@ -31,9 +32,9 @@ std::string SpellUsage::StatToPrint() { char *end_time = str_dup(rustime(localtime(&now))); out << rustime(localtime(&SpellUsage::start)) << " - " << end_time << "\n"; for (auto & it : SpellUsage::usage) { - out << std::setw(35) << MUD::Class(it.first).GetName() << "\r\n"; + out << fmt::format("{:>35}", MUD::Class(it.first).GetName()) << "\r\n"; for (auto & itt : it.second) { - out << std::setw(25) << MUD::Spell(itt.first).GetName() << " : " << itt.second << "\r\n"; + out << fmt::format("{:>25}", MUD::Spell(itt.first).GetName()) << " : " << itt.second << "\r\n"; } } return out.str(); diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 8a4538ca1..ec6af50ec 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -769,6 +769,15 @@ std::string from_disk_text(const std::string &text) { #endif } +std::string pad_right(std::string_view s, std::size_t width) { + const std::size_t len = char_count(s); + std::string out(s); + if (len < width) { + out.append(width - len, ' '); + } + return out; +} + std::string to_disk(const std::string &text) { #ifdef INTERNAL_ENCODING_UTF8 return to_koi8(text); diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 0d1f276c2..23ccbe2a9 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -204,6 +204,12 @@ std::string from_disk_text(const std::string &text); // through here (issue #3681). std::string to_disk(const std::string &text); +// Pad `s` on the right with spaces to `width` CHARACTERS. The replacement for printf's "%-Ns" +// wherever the value can hold Russian: printf counts the field width in bytes, so under UTF-8 a +// Cyrillic word ate twice its share and the column drifted. Under KOI8-R this is byte-for-byte +// what "%-Ns" did. Longer input is returned untouched, exactly like printf (issue #3681). +std::string pad_right(std::string_view s, std::size_t width); + // Transliterate `name` into the ASCII form used for save-file names: Russian letters become // Latin ones, ASCII is lowercased. The mapping is fixed by what the byte-wise implementation // produced before the migration and MUST NOT drift -- the result is the on-disk file name of a diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 8c6fd90c8..57cadaf61 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -534,4 +534,21 @@ TEST(NativeText, FromDiskTextIsIdempotent) { EXPECT_EQ(native_text::from_disk_text(""), ""); } +TEST(NativeText, PadRightCountsCharactersNotBytes) { + // The replacement for printf's "%-Ns" wherever the value can be Russian: printf counts the + // field in bytes, so a Cyrillic word ate twice its share and the column drifted (issue #3681). + const std::string privet = native_text::from_koi8("\xD0\xD2\xC9\xD7\xC5\xD4"); // 6 letters + const std::string padded = native_text::pad_right(privet, 10); + EXPECT_EQ(native_text::char_count(padded), 10u) << "padding must be measured in characters"; + EXPECT_EQ(padded.substr(0, privet.size()), privet); + EXPECT_EQ(padded.substr(privet.size()), " "); + + // ASCII behaves exactly like "%-Ns" did. + EXPECT_EQ(native_text::pad_right("ab", 5), "ab "); + EXPECT_EQ(native_text::pad_right("", 3), " "); + // Longer than the field: returned untouched, again like printf. + EXPECT_EQ(native_text::pad_right("abcdef", 3), "abcdef"); + EXPECT_EQ(native_text::pad_right(privet, 2), privet); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From ccb60fa500d92f68e4306bd553207f99b4ef8d6e Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 04:42:18 +0200 Subject: [PATCH 050/110] =?UTF-8?q?fix(utf8):=20=D0=B0=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B9,=20?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D1=8C=203=20--=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B5=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Текст, который режется под ширину колонки, резался по байтам. Под UTF-8 это и показывает вдвое меньше, чем задумано, и способно разрубить букву пополам -- клиенту уедет битый байт. Добавлен native_text::char_offset (смещение N-го символа) и через него поправлены: названия предметов в ремесле, комментарий сета в OLC, имена в списке группы, тема сообщения в changelog, имена в счётчике урона. Под KOI8-R -- прежнее поведение байт в байт. Есть тест. Оставшиеся 511 вызовов strlen просмотрены отдельным проходом: арифметики "ширина минус длина" в них нет, все найденные -- про размер буфера или про предельную длину аргумента, к отображению отношения не имеют. --- .../boards/boards_changelog_loaders.cpp | 2 +- src/gameplay/crafting/item_creation.cpp | 5 +++-- src/gameplay/mechanics/groups.cpp | 3 ++- src/gameplay/mechanics/obj_sets_olc.cpp | 2 +- src/gameplay/statistics/dps.cpp | 5 +++-- src/utils/native_text.cpp | 10 ++++++++++ src/utils/native_text.h | 6 ++++++ tests/native_text.cpp | 13 +++++++++++++ 8 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/gameplay/communication/boards/boards_changelog_loaders.cpp b/src/gameplay/communication/boards/boards_changelog_loaders.cpp index 2a3b4cdfc..fccd26846 100644 --- a/src/gameplay/communication/boards/boards_changelog_loaders.cpp +++ b/src/gameplay/communication/boards/boards_changelog_loaders.cpp @@ -43,7 +43,7 @@ void ChangeLogLoaderImplementation::add_message(const std::string &author, std::string subj(message->text.begin(), std::find(message->text.begin(), message->text.end(), '\n')); /* if (subj.size() > 40) { - subj = subj.substr(0, 40); + subj = subj.substr(0, native_text::char_offset(subj, 40)); } */ utils::Trim(subj); message->subject = subj; diff --git a/src/gameplay/crafting/item_creation.cpp b/src/gameplay/crafting/item_creation.cpp index f6ae07813..6c8bfe51c 100644 --- a/src/gameplay/crafting/item_creation.cpp +++ b/src/gameplay/crafting/item_creation.cpp @@ -6,6 +6,7 @@ * $Date$ * * $Revision$ * ************************************************************************ */ +#include "utils/native_text.h" #include "item_creation.h" #include #include "utils/utils_parse.h" @@ -325,7 +326,7 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ trec = make_recepts[i]; auto obj = GetObjectPrototype(trec->obj_proto); if (obj) { - obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, 39)); + obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, native_text::char_offset(obj->get_PName(grammar::ECase::kNom), 39))); } while (make_skills[j].num != ESkill::kUndefined) { if (make_skills[j].num == trec->skill) { @@ -341,7 +342,7 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ if (trec->parts[j].proto != 0) { obj = GetObjectPrototype(trec->parts[j].proto); if (obj) { - obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, 34)); + obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, native_text::char_offset(obj->get_PName(grammar::ECase::kNom), 34))); } else { obj_name = "Нет"; } diff --git a/src/gameplay/mechanics/groups.cpp b/src/gameplay/mechanics/groups.cpp index c79c1e710..eac31300c 100644 --- a/src/gameplay/mechanics/groups.cpp +++ b/src/gameplay/mechanics/groups.cpp @@ -6,6 +6,7 @@ \detail Группы, вступление, покидание, дележка опыта - должно быть тут. */ +#include "utils/native_text.h" #include "gameplay/mechanics/groups.h" #include "utils/grammar/gender.h" #include "utils/grammar/declensions.h" @@ -272,7 +273,7 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { if (!header) buffer << "Персонаж | Здоровье | Рядом | Аффект | Дебаф | Положение\r\n"; - buffer << fmt::format("&B{:<20}&n|", k->get_name().substr(0, 20)); + buffer << fmt::format("&B{:<20}&n|", k->get_name().substr(0, native_text::char_offset(k->get_name(), 20))); buffer << fmt::format("{}", GetWarmValueColor(k->get_hit(), k->get_real_max_hit())); buffer << fmt::format("{:<10}&n|", WORD_STATE[posi_value(k->get_hit(), k->get_real_max_hit()) + 1]); diff --git a/src/gameplay/mechanics/obj_sets_olc.cpp b/src/gameplay/mechanics/obj_sets_olc.cpp index a9860f1ec..1e09f69ff 100644 --- a/src/gameplay/mechanics/obj_sets_olc.cpp +++ b/src/gameplay/mechanics/obj_sets_olc.cpp @@ -897,7 +897,7 @@ void sedit::parse_setcomment(CharData *ch, const char *arg) { olc_set.comment.clear(); } else { olc_set.comment = arg; - olc_set.comment = olc_set.comment.substr(0, 40); + olc_set.comment = olc_set.comment.substr(0, native_text::char_offset(olc_set.comment, 40)); } show_main(ch); } diff --git a/src/gameplay/statistics/dps.cpp b/src/gameplay/statistics/dps.cpp index 4ef9cc8eb..fd5c59f27 100644 --- a/src/gameplay/statistics/dps.cpp +++ b/src/gameplay/statistics/dps.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2009 Krodo // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "dps.h" #include "gameplay/core/remort.h" #include "gameplay/mechanics/minions.h" @@ -32,7 +33,7 @@ void DpsNode::set_name(const char *name) { if (name && *name) { name_ = name; if (name_.size() > 25) { - name_ = name_.substr(0, 25); + name_ = name_.substr(0, native_text::char_offset(name_, 25)); } } } @@ -134,7 +135,7 @@ void Dps::Clear(int type) { struct sort_node { sort_node(const std::string &in_name, int in_dps, unsigned in_round_dmg, unsigned in_over_dmg) : dps(in_dps), round_dmg(in_round_dmg), over_dmg(in_over_dmg) { - name = in_name.substr(0, 25); + name = in_name.substr(0, native_text::char_offset(in_name, 25)); }; std::string name; diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index ec6af50ec..535e53517 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -8,6 +8,8 @@ through them changes nothing until the encoding flip. */ #include "native_text.h" + +#include #include "utf8.h" #include "utils_encoding.h" #include "translit_koi8.h" @@ -769,6 +771,14 @@ std::string from_disk_text(const std::string &text) { #endif } +std::size_t char_offset(std::string_view s, std::size_t chars) { +#ifdef INTERNAL_ENCODING_UTF8 + return utf8::byte_offset(s, chars); +#else + return std::min(chars, s.size()); +#endif +} + std::string pad_right(std::string_view s, std::size_t width) { const std::size_t len = char_count(s); std::string out(s); diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 23ccbe2a9..962b41a1d 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -45,6 +45,12 @@ void capitalize_first(std::string &s); // there never splits a multibyte character. KOI8-R: min(max_bytes, s.size()). std::size_t truncate_offset(std::string_view s, std::size_t max_bytes); +// Byte offset of the `chars`-th character (clamped to the end), so `s.substr(0, char_offset(s, n))` +// keeps exactly n characters. KOI8-R: min(chars, s.size()). Use this, not substr(0, n), wherever +// text is cut to fit a column: cutting by bytes both halves the visible width under UTF-8 and can +// split a character in two (issue #3681). +std::size_t char_offset(std::string_view s, std::size_t chars); + // Byte length of the character that starts at `s` (KOI8-R: 1), for stepping over a whole // character byte-by-byte. Always >= 1; on a malformed/truncated UTF-8 lead it returns only the // bytes actually present (never counts past a terminator or a non-continuation byte). diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 57cadaf61..203a437c6 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -551,4 +551,17 @@ TEST(NativeText, PadRightCountsCharactersNotBytes) { EXPECT_EQ(native_text::pad_right(privet, 2), privet); } +TEST(NativeText, CharOffsetCutsOnCharacterBoundaries) { + // Cutting text to fit a column by BYTES both halves the visible width under UTF-8 and can + // split a character in two, sending a broken byte to the client (issue #3681). + const std::string privet = native_text::from_koi8("\xD0\xD2\xC9\xD7\xC5\xD4"); // 6 letters + const std::string cut = privet.substr(0, native_text::char_offset(privet, 3)); + EXPECT_EQ(native_text::char_count(cut), 3u); + EXPECT_EQ(cut, privet.substr(0, native_text::native_is_utf8() ? 6 : 3)); + // Asking for more than there is yields the whole string, never past the end. + EXPECT_EQ(native_text::char_offset(privet, 100), privet.size()); + EXPECT_EQ(native_text::char_offset("", 5), 0u); + EXPECT_EQ(native_text::char_offset("abcdef", 4), 4u); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From d558be131da9d36624ccb1d687c65ec3e3440c87 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 04:57:13 +0200 Subject: [PATCH 051/110] =?UTF-8?q?feat(utf8):=20=D1=84=D0=BB=D0=B8=D0=BF?= =?UTF-8?q?=20--=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D1=80=D0=B0=D0=BD=D1=82=D0=B0=D0=B9=D0=BC=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=20UTF-8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Исходники теперь UTF-8. Содержимое файлов при этом не изменилось ни на байт: в git блобы всегда лежали в UTF-8, а KOI8-R давало лишь working-tree-encoding, то есть перекодировку при выкладке в рабочее дерево. Снятие атрибута и повторный checkout -- вот и весь флип, поэтому диффа по коду здесь нет. Промежуточные костыли, которые держали UTF-8-сборку на плаву до этого момента, выкинуты: - из Dockerfile ушла перекодировка дерева iconv'ом перед сборкой; - из CI ушла работа "UTF-8 flip rehearsal", которая делала то же самое: теперь это обычная сборка; - опции internal_encoding больше нет. Собрать байтовый вариант из UTF-8-исходников всё равно нельзя -- это одно состояние, а не два, -- поэтому INTERNAL_ENCODING_UTF8 определяется всегда. Байтовые ветки под #else в native_text пока остались и будут вычищены отдельно. Прочее по мелочи: MSVC получает /source-charset:utf-8 вместо koi8-r, из clang убран -Wno-invalid-source-encoding (исходники теперь валидный UTF-8), generate_version.py читает и пишет version.cpp в UTF-8. Хук pre-commit теперь проверяет обе стороны: исходники обязаны разбираться как UTF-8, файлы мира -- оставаться в кодировке, объявленной в .gitattributes. CLAUDE.md переписан: пляска с iconv для src/ больше не нужна, для lib/ -- нужна. --- .gitattributes | 7 ++++--- .githooks/pre-commit | 17 +++++++++++++++-- .github/workflows/_linux.yml | 33 --------------------------------- CLAUDE.md | 33 ++++++++++++++------------------- Dockerfile | 16 +--------------- meson.build | 19 ++++++------------- meson_options.txt | 1 - tools/meson/generate_version.py | 4 ++-- 8 files changed, 42 insertions(+), 88 deletions(-) diff --git a/.gitattributes b/.gitattributes index ab544fed0..843cec6ec 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ -/src/** working-tree-encoding=KOI8-R eol=lf -/tests/** working-tree-encoding=KOI8-R eol=lf -/tests/**/*.py working-tree-encoding=UTF-8 eol=lf +# Исходники в UTF-8 (issue #3681). До флипа тут стояло working-tree-encoding=KOI8-R: +# в git блобы всегда лежали в UTF-8, а в рабочее дерево выкладывались в KOI8-R. +/src/** eol=lf +/tests/** eol=lf *.md working-tree-encoding=UTF-8 eol=lf /.githooks/** working-tree-encoding=UTF-8 eol=lf /lib.template/** working-tree-encoding=KOI8-R eol=lf diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5be69d9a1..048267921 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,7 +5,7 @@ # ============================================================================ # Блокирует коммит если: # 1. Найден символ замены U+FFFD (О©╫) -# 2. Файл должен быть KOI8-R, но находится в UTF-8 +# 2. Файл должен быть KOI8-R, но находится в UTF-8 (данные мира; исходники наоборот -- UTF-8) # 3. Найдены типичные паттерны битой кодировки # ============================================================================ @@ -97,8 +97,21 @@ for FILE in $FILES; do fi echo " ✅ Кодировка правильная" + elif [[ "$FILE" == src/* || "$FILE" == tests/* ]]; then + # Исходники после флипа (issue #3681) обязаны быть валидным UTF-8. Файл, случайно + # сохранённый редактором в KOI8-R, внешне выглядит нормально, но ломает литералы. + if ! iconv -f utf-8 -t utf-8 "$FILE" >/dev/null 2>&1; then + echo "📄 $FILE" + echo " ❌ ОШИБКА: не UTF-8. Исходники должны быть в UTF-8." + ERROR_FOUND=1 + continue + fi + if grep -q "$BAD_CHAR" "$FILE" 2>/dev/null; then + echo "❌ [BLOCKER] Символ замены (О©╫) найден в: $FILE" + ERROR_FOUND=1 + fi else - # Для не-KOI8-R файлов проверяем только символ замены + # Для остальных файлов проверяем только символ замены if grep -q "$BAD_CHAR" "$FILE" 2>/dev/null; then echo "❌ [BLOCKER] Символ замены (О©╫) найден в: $FILE" ERROR_FOUND=1 diff --git a/.github/workflows/_linux.yml b/.github/workflows/_linux.yml index 06db4c4cc..8dc3ca035 100644 --- a/.github/workflows/_linux.yml +++ b/.github/workflows/_linux.yml @@ -90,39 +90,6 @@ 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 diff --git a/CLAUDE.md b/CLAUDE.md index f9555ec06..0591f03a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -367,12 +367,12 @@ The heartbeat system includes built-in profiling: ## Critical Notes -- **File Encoding**: All project source files MUST be in KOI8-R encoding (not UTF-8). This is critical for proper Russian text handling in the codebase. Use `iconv` to convert if needed: `iconv -f utf8 -t koi8-r input.cpp > output.cpp` +- **File Encoding**: All project source files are UTF-8 (issue #3681). Edit them directly — no iconv dance. The world data on disk (`lib/`, `lib.template/`) is still KOI8-R; see `.gitattributes` for exactly which trees. - **Thread Safety**: Main game loop is single-threaded; use `BlockingQueue` for cross-thread communication - **Shared Pointers**: Always use `CharData::shared_ptr` and `ObjData::shared_ptr` to prevent use-after-free - **Pulse Timing**: Never use wall-clock delays; register actions with heartbeat system - **Script Depth**: DG Scripts limited to 512 recursion depth to prevent stack overflow -- **Runtime Encoding**: While source files are KOI8-R, runtime text can be multiple encodings (Alt, Win, UTF-8, KOI8-R) based on client settings +- **Runtime Encoding**: The engine holds text in UTF-8 and works on it per character (`utils/native_text.*`). Client codepages (Alt, Win, KOI8-R, UTF-8) are converted at the network boundary only; the disk boundary (`from_disk_text` / `to_disk`) keeps world files in KOI8-R. ## Claude Code Workflow Rules @@ -401,33 +401,28 @@ meson setup build_otel \ ninja -C build_otel -j$(($(nproc)/2)) ``` -### File Encoding - CRITICAL -**Proper workflow for editing KOI8-R files:** +### File Encoding +Sources are UTF-8. Edit `.cpp`/`.h` directly with the Edit tool — the KOI8-R conversion dance is +gone (issue #3681). Git always stored these blobs as UTF-8; what changed is that the working tree +no longer converts them on checkout. -For files marked as `working-tree-encoding=KOI8-R` in .gitattributes (all files in /src/**, /tests/**): +Still KOI8-R on disk, and still needing care: the world and configs (`lib/`, `lib.template/`, +`/lib/cfg/**`, `/lib/misc/**`, `/lib/text/help/**`, `/lib/etc/board/**`). For those the old rule +holds — convert, edit, convert back: ```bash -# 1. Convert to UTF-8 for editing -iconv -f koi8-r -t utf-8 src/file.cpp > /tmp/file_utf8.cpp - -# 2. Edit the UTF-8 version with Edit tool or text editor -# (make your changes here) - -# 3. Convert back to KOI8-R -iconv -f utf-8 -t koi8-r /tmp/file_utf8.cpp > src/file.cpp +iconv -f koi8-r -t utf-8 lib/cfg/some.xml > /tmp/some_utf8.xml +# edit /tmp/some_utf8.xml +iconv -f utf-8 -t koi8-r /tmp/some_utf8.xml > lib/cfg/some.xml ``` -**NEVER use the Edit tool directly on existing .cpp/.h files that contain Russian text.** -Only use Edit for: -- Newly created files that will be pure ASCII/English -- Temporary UTF-8 converted files (in /tmp) -- Files in .gitattributes marked as UTF-8 (e.g., Python files) +The pre-commit hook enforces both directions: sources must parse as UTF-8, world files must stay +in the encoding `.gitattributes` declares. **NEVER use sed for editing source files.** Sed has tendency to: - Modify files in unexpected places (matching wrong lines) - Lead to file corruption detection and accidental `git checkout` (losing all uncommitted work) - Cause cumulative errors from multiple sed operations -- Corrupt KOI8-R encoding **Alternative: unified diff patches** - for small targeted changes: ```bash diff --git a/Dockerfile b/Dockerfile index 2030fda14..3fe98c75c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,9 +34,6 @@ 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) \ @@ -70,16 +67,6 @@ 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} @@ -91,8 +78,7 @@ 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} \ - -Dinternal_encoding=${INTERNAL_ENCODING} && \ + -Dyaml=${YAML_OPT} -Dsqlite=${SQLITE_OPT} && \ meson compile -C build circle:executable # ───────────────────────── Этап 2: рантайм ───────────────────────── diff --git a/meson.build b/meson.build index 741eef286..1e12107ea 100644 --- a/meson.build +++ b/meson.build @@ -88,9 +88,6 @@ if host_system == 'windows' endif endif -if cpp.get_id() == 'clang' - project_args += '-Wno-invalid-source-encoding' -endif cpu = host_machine.cpu_family() # GNU-driver compilers only. clang-cl (get_id()=='clang-cl') takes its arch from the MSVC @@ -108,22 +105,18 @@ if host_system == 'windows' if cpp.get_argument_syntax() == 'msvc' # MSVC and clang-cl project_args += ['/MP', '/bigobj', '-DFMT_UNICODE=0', '/external:anglebrackets', '/external:W0'] if cpp.get_id() == 'msvc' - # MSVC accepts the KOI8-R codepage (20866) for source/exec charset. - project_args += ['/source-charset:koi8-r', '/execution-charset:koi8-r'] - else - # clang-cl rejects non-UTF-8 /source-charset ('invalid value koi8-r'); it keeps narrow-literal - # bytes as-is, so just silence the KOI8-R source-encoding warnings (as for the GNU clang driver). - project_args += ['-Wno-invalid-source-encoding'] + project_args += ['/source-charset:utf-8', '/execution-charset:utf-8'] endif elif cpp.get_id() == 'gcc' or cpp.get_id() == 'clang' project_args += ['-Wa,-mbig-obj', '-D__USE_MINGW_ANSI_STDIO=1'] 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 +# issue #3681: движок работает с текстом в UTF-8 и посимвольно. Исходники тоже UTF-8 (см. +# .gitattributes), поэтому строковые литералы и рантайм совпадают по кодировке -- это одно +# состояние, а не два: собрать байтовый вариант из UTF-8-исходников нельзя, и опции для этого +# больше нет. Байтовые ветки под #else в native_text ещё остались и будут вычищены отдельно. +project_args += '-DINTERNAL_ENCODING_UTF8' linker_choice = get_option('linker') if linker_choice != '' diff --git a/meson_options.txt b/meson_options.txt index f870dc113..91a4d29cc 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -12,7 +12,6 @@ option('use_pch', type: 'boolean', value: true, description: 'Use precompiled he # 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') diff --git a/tools/meson/generate_version.py b/tools/meson/generate_version.py index 852eaa690..5cb2ce0c8 100755 --- a/tools/meson/generate_version.py +++ b/tools/meson/generate_version.py @@ -53,7 +53,7 @@ engine_name = "BRusMUD" engine_version = f'{mmp}.{commit_count}' -with open(input_file, encoding='koi8-r') as f: +with open(input_file, encoding='utf-8') as f: content = f.read() content = content.replace('${REVISION}', f'{git_rev} ({buildtype})') @@ -63,5 +63,5 @@ content = content.replace('${ENGINE_NAME}', engine_name) content = content.replace('${ENGINE_VERSION}', engine_version) -with open(output_file, 'w', encoding='koi8-r') as f: +with open(output_file, 'w', encoding='utf-8') as f: f.write(content) From 83a7f0efe9a7c6e760423f0202673ef734e0a7e5 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 05:44:41 +0200 Subject: [PATCH 052/110] =?UTF-8?q?fix(utf8):=20=D0=B2=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=8C=20=D0=B2=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BE=D0=B1=D1=8B=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=B0=D0=B2=D1=8B=D1=87=D0=BA=D0=B8?= =?UTF-8?q?=20--=20=D0=BD=D0=B0=20=D1=91=D0=BB=D0=BE=D1=87=D0=BA=D0=B0?= =?UTF-8?q?=D1=85=20=D0=BB=D0=BE=D0=BC=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5=D1=80=D1=8B=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/greeting.utf8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 index e4183ed6b..ff848cc89 100644 --- a/lib/greeting.utf8 +++ b/lib/greeting.utf8 @@ -19,4 +19,4 @@ Сайт: www.bylins.su Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики -Введите имя персонажа (или «новый», чтобы создать нового): \ No newline at end of file +Введите имя персонажа (или "новый" для создания нового): \ No newline at end of file From df2be73a8e7ca19b3d24bf0c28c36d6a87c870ef Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 05:58:37 +0200 Subject: [PATCH 053/110] =?UTF-8?q?Revert=20"fix(utf8):=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=83=D1=82=D1=8C=20=D0=B2=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=87=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=B0=D0=B2=D1=8B=D1=87?= =?UTF-8?q?=D0=BA=D0=B8"=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кавычки-ёлочки в приглашении возвращены по просьбе: триггеры ломались не из-за них. --- lib/greeting.utf8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 index ff848cc89..e4183ed6b 100644 --- a/lib/greeting.utf8 +++ b/lib/greeting.utf8 @@ -19,4 +19,4 @@ Сайт: www.bylins.su Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики -Введите имя персонажа (или "новый" для создания нового): \ No newline at end of file +Введите имя персонажа (или «новый», чтобы создать нового): \ No newline at end of file From ec5bc6ca38723babf2f487b0b501f2afefadc953 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 20:43:27 +0200 Subject: [PATCH 054/110] =?UTF-8?q?fix(utf8):=20=D1=88=D0=B0=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=86=D0=B5=D0=BD=D1=82=D1=80=D0=B8=D1=80=D1=83=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D1=86=D0=B5=D0=BB=D0=B8=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=B8=20=D1=80=D0=B8=D1=81=D1=83=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D0=BA=D0=B0=D0=BC=D0=B8,=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=B5=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D1=83=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20(#3?= =?UTF-8?q?681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Блоки шапки центрировались каждый по-своему: рамка титров прижималась к левому краю, логотип стоял на фиксированном отступе, а подписи считались от другой ширины. Теперь все блоки центрируются по одной ширине экрана (79), как в оригинальной шапке. Скруглённые углы рамки заменены на прямые, а '✦' на '•': в Lucida Console -- шрифте, на котором это смотрят, -- ни U+256D..U+2570, ни U+2726 нет, клиент подставляет их из другого шрифта, и рамка разъезжается. Прямые углы и точка есть практически везде. --- lib/greeting.utf8 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 index e4183ed6b..77de9b982 100644 --- a/lib/greeting.utf8 +++ b/lib/greeting.utf8 @@ -1,9 +1,9 @@ -╭──────────────────────────────────────────────────────────────────────────╮ -│ Based on CircleMUD v3.0 — created by Jeremy Elson │ -│ DikuMUD Gamma 0.0 — Sebastian Hammer, Michael Seifert, │ -│ Hans Henrik Staerfeldt, Tom Madsen, Katja Nyboe │ -╰──────────────────────────────────────────────────────────────────────────╯ + ┌────────────────────────────────────────────────────────┐ + │ Based on CircleMUD v3.0 — created by Jeremy Elson │ + │ DikuMUD Gamma 0.0 — Sebastian Hammer, Michael Seifert, │ + │ Hans Henrik Staerfeldt, Tom Madsen, Katja Nyboe │ + └────────────────────────────────────────────────────────┘ ███████ ██ @@ -13,10 +13,10 @@ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ███████ ███████ ████ ███ ███ ████ ████ ████ ████ ███████ ████ - ✦ ПО ПРЕДАНИЯМ РУССКИХ НАРОДНЫХ СКАЗОК И БЫЛИН ✦ + • ПО ПРЕДАНИЯМ РУССКИХ НАРОДНЫХ СКАЗОК И БЫЛИН • - По административным вопросам — Стрибог ‹stribog@bylins.su› - Сайт: www.bylins.su - Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики + По административным вопросам — Стрибог ‹stribog@bylins.su› + Сайт: www.bylins.su + Вики: https://bylinsmud.fandom.com/ru/wiki/Bylinsmud_вики Введите имя персонажа (или «новый», чтобы создать нового): \ No newline at end of file From 83d1106f14e8c4a7d44c51737cd274a1ee6ce5c6 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 21:36:58 +0200 Subject: [PATCH 055/110] =?UTF-8?q?refactor(utf8):=20=D0=B2=D1=8B=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD=D1=83=D1=82=D1=8C=20=D0=BC=D1=91=D1=80=D1=82=D0=B2?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После флипа INTERNAL_ENCODING_UTF8 определён всегда, значит вторая половина каждой пары реализаций -- мёртвый код. Байтовый вариант скомпилировать уже нельзя: собрать его из UTF-8-исходников не получится, а держать в дереве код, который никто не собирает и не проверяет, -- худший вид долга. Убраны все #ifdef INTERNAL_ENCODING_UTF8 вместе с #else-ветками: native_text похудел на 200 строк, russian_keys на 46 (осталась таблица кодовых точек, без байтов KOI8-R), table_wrapper -- на выбор между char_table и utf8_table. Заодно свёрнуты рантайм-развилки по native_is_utf8(): восемь сравнений строк в utils_string (за проверкой шёл недостижимый побайтовый цикл), приведение ввода к нативной кодировке, отдача UTF-8-клиенту и подъём буфера в diskio. native_is_utf8() пока остался: на нём ещё ветвятся ожидания в тестах, это следующий заход. --- src/administration/names.cpp | 90 +- src/engine/boot/boot_data_files.cpp | 84 +- src/engine/db/world_data_source_base.cpp | 4 +- src/engine/scripting/dg_comm.cpp | 36 +- src/engine/scripting/dg_olc.cpp | 138 +- src/engine/scripting/dg_scripts.cpp | 636 +++---- src/engine/ui/cmd/do_statistic.cpp | 18 +- src/engine/ui/cmd/do_toggle.cpp | 116 +- src/engine/ui/cmd_god/do_print_armor.cpp | 114 +- src/engine/ui/cmd_god/do_stat.cpp | 474 ++--- src/engine/ui/commands.cpp | 4 +- src/gameplay/clans/house.cpp | 1690 ++++++++--------- .../boards/boards_changelog_loaders.cpp | 8 +- src/gameplay/communication/parcel.cpp | 252 +-- src/gameplay/crafting/item_creation.cpp | 850 ++++----- src/gameplay/mechanics/groups.cpp | 246 +-- src/gameplay/mechanics/obj_sets_olc.cpp | 418 ++-- src/gameplay/mechanics/sight.cpp | 662 +++---- src/gameplay/statistics/dps.cpp | 104 +- src/gameplay/statistics/mob_stat.cpp | 92 +- src/utils/utils_parse.cpp | 106 +- src/utils/utils_string.cpp | 94 +- 22 files changed, 3118 insertions(+), 3118 deletions(-) diff --git a/src/administration/names.cpp b/src/administration/names.cpp index 43887b5b0..63396fafc 100644 --- a/src/administration/names.cpp +++ b/src/administration/names.cpp @@ -51,7 +51,7 @@ int was_agree_name(DescriptorData *d) { d->character->set_sex(static_cast(sex)); (d->character)->player_specials->saved.NameGod = immlev + 1000; (d->character)->player_specials->saved.NameIDGod = GetPlayerIdByName(immname); - sprintf(buf, "\r\nВаше имя одобрено!\r\n"); + sprintf(buf, "\r\n !\r\n"); iosystem::write_to_output(buf, d); sprintf(buf, "AUTOAGREE: %s was agreed by %s", GET_PC_NAME(d->character), immname); log(buf, d); @@ -75,7 +75,7 @@ int was_disagree_name(DescriptorData *d) { } sscanf(line.c_str(), "%s %s %d", mortname, immname, &immlev); if (!strcmp(mortname, GET_NAME(d->character))) { - sprintf(buf, "\r\nВаше имя запрещено!\r\n"); + sprintf(buf, "\r\n !\r\n"); iosystem::write_to_output(buf, d); sprintf(buf, "AUTOAGREE: %s was disagreed by %s", GET_PC_NAME(d->character), immname); log(buf, d); @@ -102,18 +102,18 @@ void rm_agree_name(CharData *d) { }); } -// список неодобренных имен, дубль2 -// на этот раз ничего перебирать не будем, держа все в памяти и обновляя по необходимости +// , 2 +// , struct NewName { - std::string name0; // падежи + std::string name0; // std::string name1; // --//-- std::string name2; // --//-- std::string name3; // --//-- std::string name4; // --//-- std::string name5; // --//-- - std::string email; // мыло - EGender sex{EGender::kNeutral}; // часто не ясно, для какоо пола падежи вообще + std::string email; // + EGender sex{EGender::kNeutral}; // , }; typedef std::shared_ptr NewNamePtr; @@ -121,7 +121,7 @@ typedef std::map NewNameListType; static NewNameListType NewNameList; -// сохранение списка в файл +// static void NewNames::save() { // issue.misc-migrate: atomic whole-file replace via StateManager. std::vector lines; @@ -129,15 +129,15 @@ static void NewNames::save() { for (const auto &it : NewNameList) { lines.push_back(it.first); } - // Граница записи: имена уходят на диск в кодировке мира (сейчас KOI8-R), зеркально - // чтению (issue #3681). + // : ( KOI8-R), + // (issue #3681). for (auto &line : lines) { line = native_text::to_disk(line); } MUD::StateManager().SaveLines(state::EStateFile::kPendingNames, lines); } -// добавить в список без сохранения на диск +// static void NewNames::cache_add(CharData *ch) { NewNamePtr name(new NewName); @@ -153,13 +153,13 @@ static void NewNames::cache_add(CharData *ch) { NewNameList[GET_NAME(ch)] = name; } -// добавление имени в список неодобренных для показа иммам +// void NewNames::add(CharData *ch) { cache_add(ch); save(); } -// поиск/удаление персонажа из списка неодобренных имен +// / void NewNames::remove(CharData *ch) { NewNameListType::iterator it; it = NewNameList.find(GET_NAME(ch)); @@ -168,19 +168,19 @@ void NewNames::remove(CharData *ch) { save(); } -// для удаления через команду имма +// void NewNames::remove(const std::string &name, CharData *actor) { auto it = NewNameList.find(name); if (it != NewNameList.end()) { NewNameList.erase(it); - SendMsgToChar("Запись удалена.\r\n", actor); + SendMsgToChar(" .\r\n", actor); } else - SendMsgToChar("В списке нет такого имени.\r\n", actor); + SendMsgToChar(" .\r\n", actor); save(); } -// лоад списка неодобренных имен +// void NewNames::load() { // issue.misc-migrate: read names via StateManager (keeping the previous `>> buffer` tokenising), // then re-save to normalise. A missing file just yields an empty list. @@ -188,32 +188,32 @@ void NewNames::load() { std::istringstream iss(line); std::string buffer; while (iss >> buffer) { - // сразу проверяем не сделетился ли уже персонаж + // Player t_tch; Player *tch = &t_tch; if (LoadPlayerCharacter(buffer.c_str(), tch, ELoadCharFlags::kFindId) < 0) continue; - // не сделетился... + // ... cache_add(tch); } } save(); } -// вывод списка неодобренных имму +// bool NewNames::show(CharData *actor) { if (NewNameList.empty()) return false; std::ostringstream buffer; - buffer << "\r\nИгроки, ждущие одобрения имени (имя <игрок> одобрить/запретить/удалить):\r\n" << kColorWht; + buffer << "\r\n, ( <> //):\r\n" << kColorWht; for (NewNameListType::const_iterator it = NewNameList.begin(); it != NewNameList.end(); ++it) { const auto sex = static_cast(to_underlying(it->second->sex)); - buffer << "Имя: " << it->first << " " << it->second->name0 << "/" << it->second->name1 + buffer << ": " << it->first << " " << it->second->name0 << "/" << it->second->name1 << "/" << it->second->name2 << "/" << it->second->name3 << "/" << it->second->name4 << "/" << it->second->name5 << " Email: &S" - << (GET_GOD_FLAG(actor, EGf::kDemigod) ? "неопределен" : it->second->email) - << "&s Пол: " << genders[sex] << "\r\n"; + << (GET_GOD_FLAG(actor, EGf::kDemigod) ? "" : it->second->email) + << "&s : " << genders[sex] << "\r\n"; } buffer << kColorNrm; SendMsgToChar(buffer.str(), actor); @@ -286,41 +286,41 @@ static void go_name(CharData *ch, CharData *vict, int action) { int god_level = ch->IsFlagged(EPrf::kCoderinfo) ? kLvlImplementator : GetRealLevel(ch); if (GetRealLevel(vict) > god_level) { - SendMsgToChar("А он ведь старше вас...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); return; } - // одобряем или нет + // int lev = (vict)->player_specials->saved.NameGod; if (lev > 1000) lev = lev - 1000; if (lev > god_level) { - SendMsgToChar("Об этом имени уже позаботился бог старше вас.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (lev == god_level) if ((vict)->player_specials->saved.NameIDGod != ch->get_uid()) - SendMsgToChar("Об этом имени уже позаботился другой бог вашего уровня.\r\n", ch); + SendMsgToChar(" .\r\n", ch); if (action == NAME_AGREE) { (vict)->player_specials->saved.NameGod = god_level + 1000; (vict)->player_specials->saved.NameIDGod = ch->get_uid(); - //SendMsgToChar("Имя одобрено!\r\n", ch); - SendMsgToChar(vict, "&GВаше имя одобрено!&n\r\n"); + //SendMsgToChar(" !\r\n", ch); + SendMsgToChar(vict, "&G !&n\r\n"); agree_name(vict, GET_NAME(ch), god_level); - sprintf(buf, "&c%s одобрил%s имя игрока %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); + sprintf(buf, "&c%s %s %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); SendMsgToGods(buf, true); - // В этом теперь нет смысла + // //mudlog(buf, CMP, kLevelGod, SYSLOG, true); } else { (vict)->player_specials->saved.NameGod = god_level; (vict)->player_specials->saved.NameIDGod = ch->get_uid(); - //SendMsgToChar("Имя запрещено!\r\n", ch); - SendMsgToChar(vict, "&RВаше имя запрещено!&n\r\n"); + //SendMsgToChar(" !\r\n", ch); + SendMsgToChar(vict, "&R !&n\r\n"); disagree_name(vict, GET_NAME(ch), god_level); - sprintf(buf, "&c%s запретил%s имя игрока %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); + sprintf(buf, "&c%s %s %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); SendMsgToGods(buf, true); //mudlog(buf, CMP, kLevelGod, SYSLOG, true); @@ -329,10 +329,10 @@ static void go_name(CharData *ch, CharData *vict, int action) { } const char *MORTAL_DO_TITLE_FORMAT = "\r\n" - "имя - вывод списка имен, ждущих одобрения, если они есть\r\n" - "имя <игрок> одобрить - одобрить имя данного игрока\r\n" - "имя <игрок> запретить - запретить имя данного игрока\r\n" - "имя <игрок> удалить - удалить данное имя из списка без запрета или одобрения\r\n"; + " - , , \r\n" + " <> - \r\n" + " <> - \r\n" + " <> - \r\n"; void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { std::string name, command = argument; @@ -348,11 +348,11 @@ void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { utils::Trim(command); int action = -1; - if (CompareParam(command, "одобрить")) + if (CompareParam(command, "")) action = NAME_AGREE; - else if (CompareParam(command, "запретить")) + else if (CompareParam(command, "")) action = NAME_DISAGREE; - else if (CompareParam(command, "удалить")) + else if (CompareParam(command, "")) action = NAME_DELETE; if (action < 0) { @@ -369,14 +369,14 @@ void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { CharData *vict; if ((vict = target_resolver::FindPlayerVis(ch, name)) != nullptr) { if (!(vict = target_resolver::FindPlayer(ch, name))) { - SendMsgToChar("Нет такого игрока.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } go_name(ch, vict, action); } else { - vict = new Player; // TODO: переделать на стек + vict = new Player; // TODO: if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) < 0) { - SendMsgToChar("Такого персонажа не существует.\r\n", ch); + SendMsgToChar(" .\r\n", ch); delete vict; return; } diff --git a/src/engine/boot/boot_data_files.cpp b/src/engine/boot/boot_data_files.cpp index 6b2df297b..1daff92b8 100644 --- a/src/engine/boot/boot_data_files.cpp +++ b/src/engine/boot/boot_data_files.cpp @@ -58,10 +58,10 @@ void DataFile::get_one_line(char *buf, std::size_t buf_size) { raw[--len] = '\0'; // take off the trailing \r\n } - // Граница кодировки (issue #3681): файлы справки лежат на диске в KOI8-R, а движок под - // UTF-8 ждёт нативный текст -- без этого ключи разделов не совпадали бы с тем, что ввёл - // игрок, а тело раздела уезжало бы клиенту байтами чужой кодировки. Строка может при этом - // вырасти (KOI8-R байт -> до трёх байт UTF-8), поэтому размер буфера передаётся явно. + // (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'; @@ -407,7 +407,7 @@ void WorldFile::parse_room(int virtual_nr) { fatal_log("SYSERR: Room %d is outside of any zone.", virtual_nr); } } - // Создаем новую комнату + // world.push_back(new RoomData); world[room_realnum]->zone_rn = zone; world[room_realnum]->vnum = virtual_nr; @@ -482,7 +482,7 @@ void WorldFile::parse_room(int virtual_nr) { switch (letter) { case 'I': get_line(file(), - line); //оставлено для совместимости, удалить после пересохранения комнат + line); // , break; case 'T': dg_read_trigger(world[room_realnum], WLD_TRIGGER, virtual_nr); @@ -512,7 +512,7 @@ void WorldFile::setup_dir(int room, unsigned dir) { world[room]->dir_option_proto[dir] = std::make_shared(); world[room]->dir_option_proto[dir]->general_description = fread_string(); - // парс строки алиаса двери на имя; вининельный падеж, если он есть + // ; , world[room]->dir_option_proto[dir]->set_keywords(fread_string()); if (!get_line(file(), line)) { @@ -540,9 +540,9 @@ void WorldFile::setup_dir(int room, unsigned dir) { world[room]->dir_option_proto[dir]->key = t[1]; world[room]->dir_option_proto[dir]->to_room(t[2]); - // Полностью пустые D-блоки - мусор из старых редакторов, на рантайм не - // влияют и без причины засоряют show errors. Дропаем их сразу при загрузке - // (симметрично в YAML- и SQLite-загрузчиках), см. issue #3272. + // D- - , + // show errors. + // ( YAML- SQLite-), . issue #3272. if (world[room]->dir_option_proto[dir]->is_empty()) { world[room]->dir_option_proto[dir].reset(); } @@ -615,7 +615,7 @@ void ObjectFile::parse_object(const int nr) { tobj->set_short_description(utils::colorLOW(fread_string())); snprintf(buf, sizeof(buf), "%s", tobj->get_short_description().c_str()); - tobj->set_PName(grammar::ECase::kNom, utils::colorLOW(buf)); //именительный падеж равен короткому описанию + tobj->set_PName(grammar::ECase::kNom, utils::colorLOW(buf)); // for (j = grammar::ECase::kGen; j <= grammar::ECase::kLastCase; j++) { tobj->set_PName(static_cast(j), utils::colorLOW(fread_string())); @@ -659,8 +659,8 @@ void ObjectFile::parse_object(const int nr) { timer = 99999; } tobj->set_timer(timer); - // issue #3581: obj->spell и его уровень -- мёртвая пара, ни одна механика их не читает. - // t[2]/t[3] всё ещё разбираются (формат легаси-строки = 4 поля), но объекту не присваиваются. + // issue #3581: obj->spell -- ͣ , . + // t[2]/t[3] ӣ ݣ ( - = 4 ), . if (!get_line(file(), m_line)) { fatal_log("SYSERR: Expecting *3th* numeric line of %s, but file ended!", m_buffer); @@ -731,7 +731,7 @@ void ObjectFile::parse_object(const int nr) { tobj->set_weight(tobj->get_val(1) + 5); } } - tobj->unset_extraflag(EObjFlag::kTransformed); //от шаловливых ручек + tobj->unset_extraflag(EObjFlag::kTransformed); // tobj->unset_extraflag(EObjFlag::kTicktimer); // *** extra descriptions and affect fields *** @@ -829,7 +829,7 @@ bool ObjectFile::check_object(ObjData *obj) { log("SYSERR: Object #%d (%s) has negative weight (%d).", GET_OBJ_VNUM(obj), obj->get_short_description().c_str(), obj->get_weight()); } -/* спам от антуражных шмоток +/* if (obj->get_rent_off() <= 0) { error = true; log("SYSERR: Object #%d (%s) has negative cost/day (%d).", @@ -1026,14 +1026,14 @@ void MobileFile::parse_mobile(const int nr) { // DG triggers -- script info follows mob S/E section // DG triggers -- script is defined after the end of the room 'T' - // Ингредиентная магия -- 'I' - // Объекты загружаемые по-смертно -- 'L' + // -- 'I' + // - -- 'L' do { letter = fread_letter(file()); ungetc(letter, file()); switch (letter) { - case 'I': // Оставлено для совместимости со старым форматиом (с ингредиентами в файлах зон). + case 'I': // ( ). get_line(file(), line); break; @@ -1074,12 +1074,12 @@ void MobileFile::parse_mobile(const int nr) { } } /* -//сконвертированно в пфайлы - // Авто-выставление "магии жизни" мобам с заклинанием - // "исцеление"/"групповое исцеление" -- билдеры часто забывают - // прописать Skill: 189, и кастер заваливает каст из-за нулевого - // процента. На загрузке выставляем kLifeMagic = level * 10 для - // любого моба с одним из этих заклинаний в памяти (#3267). +// + // - " " + // ""/" " -- + // Skill: 189, - + // . kLifeMagic = level * 10 + // (#3267). if (GET_SPELL_MEM(mob_proto + i, ESpell::kHeal) > 0 || GET_SPELL_MEM(mob_proto + i, ESpell::kGroupHeal) > 0) { mob_proto[i].set_skill(ESkill::kLifeMagic, mob_proto[i].GetLevel() * 10); @@ -1099,7 +1099,7 @@ void MobileFile::parse_simple_mob(int i, int nr) { mob_proto[i].set_con(11); mob_proto[i].set_cha(11); - mob_proto[i].player_specials->saved.NameGod = 1001; // у мобов имя всегда одобрено + mob_proto[i].player_specials->saved.NameGod = 1001; // if (!get_line(file(), line)) { fatal_log("SYSERR: Format error in mob #%d, file ended after S flag!", nr); } @@ -1236,10 +1236,10 @@ void MobileFile::interpret_espec(const char *keyword, const char *value, int i, } -/* заготовка парса резистов у моба при загрузке мада, чтоб в след раз не придумывать - if (GET_RESIST(mob_proto + i, 4) > 49 && !mob_proto[i].get_role(kBoss)) // жизнь и не боссы +/* , + if (GET_RESIST(mob_proto + i, 4) > 49 && !mob_proto[i].get_role(kBoss)) // { - if (zone_table[world[&mob_proto[i]->in_room]->zone].group < 3) // в зонах 0-2 группы + if (zone_table[world[&mob_proto[i]->in_room]->zone].group < 3) // 0-2 log("RESIST LIVE num: %d Vnum: %d Level: %d Name: %s", GET_RESIST(mob_proto + i, 4), mob_index[i].vnum, GetRealLevel(&mob_proto[i]), GET_PAD(&mob_proto[i], 0)); } */ @@ -1254,7 +1254,7 @@ void MobileFile::interpret_espec(const char *keyword, const char *value, int i, SetSave(mob_proto + i, save, std::clamp(t[to_underlying(save)], kMinSaving, kMaxSaving)); } } -// Svent: и что тут за коллекция магик намберов бесконечная? Вынести в настройки. +// Svent: ? . CASE("HPReg") { mob_proto[i].add_abils.hitreg = std::clamp(num_arg, -200, 200); } @@ -1489,7 +1489,7 @@ bool ZoneFile::load_zone() { size_t digits = full_file_name().find_first_of("1234567890"); if (digits <= full_file_name().size()) { if (zone.vnum != atoi(full_file_name().c_str() + digits)) { - fatal_log("SYSERR: файл %s содержит неверный номер зоны %d", full_file_name().c_str(), zone.vnum); + fatal_log("SYSERR: %s %d", full_file_name().c_str(), zone.vnum); } } if (2 == count) { @@ -1538,7 +1538,7 @@ bool ZoneFile::load_regular_zone() { if (zone.typeB_count) { CREATE(zone.typeB_list, zone.typeB_count); CREATE(zone.typeB_flag, zone.typeB_count); - // сбрасываем все флаги + // for (b_number = zone.typeB_count; b_number > 0; b_number--) { zone.typeB_flag[b_number - 1] = false; } @@ -1559,7 +1559,7 @@ bool ZoneFile::load_regular_zone() { } zone.name = buf; - log("Читаем zon файл: %s", full_file_name().c_str()); + log(" zon : %s", full_file_name().c_str()); while (*buf != 'S' && !feof(file())) { line_num += get_line(file(), buf); @@ -1598,9 +1598,9 @@ bool ZoneFile::load_regular_zone() { auto group = 0; const auto count = sscanf(buf, "#%d %d %d %d", &zone.level, &zone.type, &group, &zone.entrance); if (count < 2) { - fatal_log("SYSERR: ошибка чтения z.level, z.type, z.group, z.entrance: %s", buf); + fatal_log("SYSERR: z.level, z.type, z.group, z.entrance: %s", buf); } - zone.group = (group == 0) ? 1 : group; //группы в 0 рыл не бывает + zone.group = (group == 0) ? 1 : group; // 0 line_num += get_line(file(), buf); char t1[80]; @@ -1609,7 +1609,7 @@ bool ZoneFile::load_regular_zone() { *t2 = 0; auto tmp_reset_idle = 0; if (sscanf(buf, " %d %d %d %d %s %s", &zone.top, &zone.lifespan, &zone.reset_mode, &tmp_reset_idle, t1, t2) < 4) { - // если нет четырех констант, то, возможно, это старый формат -- попробуем прочитать три + // , , , -- const auto count = sscanf(buf, " %d %d %d %s %s", &zone.top, &zone.lifespan, @@ -1643,9 +1643,9 @@ bool ZoneFile::load_regular_zone() { } ptr++; - // Новые параметры формата файла: - // A номер_зоны -- зона типа A из списка - // B номер_зоны -- зона типа B из списка + // : + // A _ -- A + // B _ -- B if (zone.cmd[cmd_no].command == 'A') { sscanf(ptr, " %d", &zone.typeA_list[a_number]); a_number++; @@ -1744,8 +1744,8 @@ class HelpFile : public DataFile { }; bool HelpFile::load_help() { - // В нативной кодировке строка может быть втрое длиннее прочитанной с диска (KOI8-R байт - // разворачивается в UTF-8 максимум в три), поэтому буферы с запасом (issue #3681). + // (KOI8-R + // UTF-8 ), (issue #3681). constexpr size_t kLineBufSize = 3 * READ_SIZE + 1; #if defined(CIRCLE_MACINTOSH) static char key[kLineBufSize], next_key[kLineBufSize], entry[32384]; // ? @@ -1762,8 +1762,8 @@ bool HelpFile::load_help() { snprintf(entry, sizeof(entry), "%s\r\n", key); get_one_line(line, sizeof(line)); while (*line != '#') { - // если вдруг файл внезапно закончился и '#' так и не встретился - // логаем ошибку и заканчиваем парсинг во избежание зацикливания + // '#' + // if ((*line == '$') && (*(line + 1) == 0)) { std::stringstream str_log; str_log << "SYSERR: unexpected EOF in help file: \"" << file_name() << "\""; diff --git a/src/engine/db/world_data_source_base.cpp b/src/engine/db/world_data_source_base.cpp index 32dd51937..6fb4bb2e6 100644 --- a/src/engine/db/world_data_source_base.cpp +++ b/src/engine/db/world_data_source_base.cpp @@ -119,8 +119,8 @@ IndexData* WorldDataSourceBase::CreateTriggerIndex(int vnum, Trigger *trig) trig_index[top_of_trigt++] = index; - // Индекс для 'vnum trig': какие триггеры грузят данный предмет. Строим его - // здесь -- через эту функцию проходит каждый триггер любого формата мира. + // 'vnum trig': . + // -- . IndexTriggerObjLoads(vnum, trig); return index; diff --git a/src/engine/scripting/dg_comm.cpp b/src/engine/scripting/dg_comm.cpp index 01335702a..1e2825997 100644 --- a/src/engine/scripting/dg_comm.cpp +++ b/src/engine/scripting/dg_comm.cpp @@ -28,12 +28,12 @@ char *any_one_name(char *argument, char *first_arg) { // Find length of first word /* - * Тут нужно что-то делать!!! - * Библиотечная функция ispunct() неправильно работает для русского языка - * (по крайней мере у меня). Пока закоментировал. + * - !!! + * ispunct() + * ( ). . */ - // Шагаем по символу, а не по байту: под UTF-8 русская буква занимает два, и побайтовое - // приведение к нижнему регистру ломало её (issue #3681). + // , : UTF-8 , + // ţ (issue #3681). for (arg = first_arg; *argument && !isspace(*argument) && (!ispunct(*argument) || *argument == '#' || *argument == '-');) { const std::size_t written = native_text::copy_lower_char(argument, arg); @@ -56,18 +56,18 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ switch (type[i]) { case '~': if (!otokens[i]) - strcat(sb, "Кто-то"); + strcat(sb, "-"); else if ((CharData *) otokens[i] == ch) - strcat(sb, "Вы"); + strcat(sb, ""); else strcat(sb, sight::PersonName((CharData *) otokens[i], ch, 0)); break; case '@': if (!otokens[i]) - strcat(sb, "чей-то"); + strcat(sb, "-"); else if ((CharData *) otokens[i] == ch) - strcat(sb, "ваш"); + strcat(sb, ""); else { strcat(sb, sight::PersonName((CharData *) otokens[i], ch, 1)); } @@ -75,34 +75,34 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ case '^': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, "чей-то"); + strcat(sb, "-"); else if (otokens[i] == ch) - strcat(sb, "ваш"); + strcat(sb, ""); else strcat(sb, grammar::PossessivePronoun(((CharData *) otokens[i])->get_sex())); break; case '}': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, "Он"); + strcat(sb, ""); else if (otokens[i] == ch) - strcat(sb, "Вы"); + strcat(sb, ""); else strcat(sb, grammar::PersonalPronoun(((CharData *) otokens[i])->get_sex())); break; case '*': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, "ему"); + strcat(sb, ""); else if (otokens[i] == ch) - strcat(sb, "вам"); + strcat(sb, ""); else strcat(sb, grammar::DativePronoun(((CharData *) otokens[i])->get_sex())); break; case '`': if (!otokens[i]) - strcat(sb, "что-то"); + strcat(sb, "-"); else strcat(sb, OBJS(((ObjData *) otokens[i]), ch)); break; @@ -111,8 +111,8 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ strcat(sb, tokens[i]); strcat(sb, "\n\r"); -// Ибо нефиг, достали прочерки ставить чтоб не аперкейсило в многостроковых сказаниях, пусть билдеры руками. -// sb[0] = UPPER(sb[0]); // Библиотечный вызов toupper() глючит для русского +// , , . +// sb[0] = UPPER(sb[0]); // toupper() SendMsgToChar(sb, ch); } diff --git a/src/engine/scripting/dg_olc.cpp b/src/engine/scripting/dg_olc.cpp index 0cdaf5767..8e4650841 100644 --- a/src/engine/scripting/dg_olc.cpp +++ b/src/engine/scripting/dg_olc.cpp @@ -80,14 +80,14 @@ bool TrigeditQueueLuaFormat(DescriptorData *d, bool save_on_completion) const std::string source = OLC_STORAGE(d) ? OLC_STORAGE(d) : ""; const auto request_id = lua_scripting::QueueLuaFormat(source); if (request_id == 0) { - SendMsgToChar("Очередь форматтера Lua переполнена. Повторите позже.\r\n", d->character.get()); + SendMsgToChar(" Lua . .\r\n", d->character.get()); return false; } d->olc->lua_format_request_id = request_id; d->olc->lua_format_save_on_completion = save_on_completion; SendMsgToChar(save_on_completion - ? "Проверка и форматирование Lua-скрипта перед сохранением запущены. Дождитесь результата.\r\n" - : "Форматирование Lua-скрипта запущено. Дождитесь результата.\r\n", + ? " Lua- . .\r\n" + : " Lua- . .\r\n", d->character.get()); return true; } @@ -105,17 +105,17 @@ bool TrigeditFormatLuaBeforeSave(DescriptorData *d) bool TrigeditApplyFormattedLua(DescriptorData *d, const lua_scripting::LuaFormatResult& result) { if (!result.success) { - SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: %s\r\n", result.error.c_str()); + SendMsgToChar(d->character.get(), " / Lua: %s\r\n", result.error.c_str()); return false; } if (result.formatted.size() >= MAX_CMD_LENGTH) { - SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: результат слишком большой.\r\n"); + SendMsgToChar(d->character.get(), " / Lua: .\r\n"); return false; } RECREATE(OLC_STORAGE(d), MAX_CMD_LENGTH); memcpy(OLC_STORAGE(d), result.formatted.c_str(), result.formatted.size() + 1); OLC_VAL(d)++; - SendMsgToChar(d->character.get(), "Lua-скрипт отформатирован.\r\n"); + SendMsgToChar(d->character.get(), "Lua- .\r\n"); return true; } @@ -268,7 +268,7 @@ void ProcessLuaFormatterResults() d->olc->lua_format_save_on_completion = false; if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { if (save_on_completion) { - SendMsgToChar("Форматирование не применено. Исходный код триггера сохраняется.\r\n", + SendMsgToChar(" . .\r\n", d->character.get()); TrigeditSaveAndExit(d); } else { @@ -279,7 +279,7 @@ void ProcessLuaFormatterResults() const bool formatted = TrigeditApplyFormattedLua(d, result); if (save_on_completion) { if (!formatted) { - SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); + SendMsgToChar(" Lua- .\r\n", d->character.get()); } TrigeditSaveAndExit(d); continue; @@ -310,7 +310,7 @@ void script_save_to_disk(FILE *fp, const void *item, int type) { } /************************************************************************** - * Редактирование ТРИГГЕРОВ + * * trigedit **************************************************************************/ @@ -360,24 +360,24 @@ void trigedit_disp_menu(DescriptorData *d) { trgtypes[0] = '\0'; } std::stringstream out; - out << "Редактирование триггера " << "[&y" << OLC_NUM(d) << "&n]\r\n\r\n" - << "&g1)&n Название : " << "&y" <narg << "\r\n" - << "&g5)&n Аргументы : " << "&y" << trig->arglist.c_str() << "\r\n" - << "&g6)&n " << (TrigeditIsLuaTrigger(trig) ? "Lua-скрипт" : "Команды") << ":\r\n" + out << " " << "[&y" << OLC_NUM(d) << "&n]\r\n\r\n" + << "&g1)&n : " << "&y" <narg << "\r\n" + << "&g5)&n : " << "&y" << trig->arglist.c_str() << "\r\n" + << "&g6)&n " << (TrigeditIsLuaTrigger(trig) ? "Lua-" : "") << ":\r\n" << "&c" << OLC_STORAGE(d); if (trig->get_attach_type() == MOB_TRIGGER) { - out << "&g7)&n Обрабатывать команды моба в стане? : &y" << (trig->add_flag ? "ДА" : "НЕТ") << "&n\r\n"; + out << "&g7)&n ? : &y" << (trig->add_flag ? "" : "") << "&n\r\n"; } #if defined(WITH_LUAJIT_PROTOTYPE) - out << "&g8)&n Язык скрипта : &y" << (TrigeditIsLuaTrigger(trig) ? "Lua" : "DG") << "&n\r\n"; + out << "&g8)&n : &y" << (TrigeditIsLuaTrigger(trig) ? "Lua" : "DG") << "&n\r\n"; #endif if (TrigeditIsLuaTrigger(trig) && lua_scripting::LuaFormatterAvailable()) { - out << "&g9)&n Проверить синтаксис и отформатировать Lua\r\n"; + out << "&g9)&n Lua\r\n"; } - out << "&gQ)&n Завершить редактирование\r\n" "Введите Выбранное :"; + out << "&gQ)&n \r\n" " :"; SendMsgToChar(out.str(), d->character.get()); OLC_MODE(d) = TRIGEDIT_MAIN_MENU; } @@ -417,12 +417,12 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (tolower(*arg) == 'q') { d->olc->lua_format_request_id = 0; d->olc->lua_format_save_on_completion = false; - SendMsgToChar("Форматирование отменено. Исходный Lua-код сохраняется без форматирования.\r\n", + SendMsgToChar(" . Lua- .\r\n", d->character.get()); TrigeditSaveAndExit(d); } else { - SendMsgToChar("Проверка и форматирование Lua-скрипта перед сохранением еще выполняются. " - "Для отмены форматирования и сохранения исходного кода используйте Q.\r\n", + SendMsgToChar(" Lua- . " + " Q.\r\n", d->character.get()); } return; @@ -435,7 +435,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { d->olc->lua_format_request_id = 0; d->olc->lua_format_save_on_completion = false; } else { - SendMsgToChar("Форматирование Lua-скрипта еще выполняется. Для выхода используйте Q.\r\n", + SendMsgToChar(" Lua- . Q.\r\n", d->character.get()); trigedit_disp_menu(d); return; @@ -448,7 +448,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (!GET_TRIG_TYPE(OLC_TRIG(d))) { SendMsgToChar("Invalid Trigger Type! Answer a to abort quit!\r\n", d->character.get()); } - SendMsgToChar("Желаете ли вы сохранить изменения в триггере? (y/n): ", d->character.get()); + SendMsgToChar(" ? (y/n): ", d->character.get()); OLC_MODE(d) = TRIGEDIT_CONFIRM_SAVESTRING; } else { cleanup_olc(d, CLEANUP_ALL); @@ -456,35 +456,35 @@ void trigedit_parse(DescriptorData *d, char *arg) { return; case '1': OLC_MODE(d) = TRIGEDIT_NAME; - SendMsgToChar("Название: ", d->character.get()); + SendMsgToChar(": ", d->character.get()); break; case '2': OLC_MODE(d) = TRIGEDIT_INTENDED; - SendMsgToChar("0: Мобы, 1: Объекты, 2: Комнаты: ", d->character.get()); + SendMsgToChar("0: , 1: , 2: : ", d->character.get()); break; case '3': OLC_MODE(d) = TRIGEDIT_TYPES; trigedit_disp_types(d); break; case '4': OLC_MODE(d) = TRIGEDIT_NARG; - SendMsgToChar("Числовой аргумент: ", d->character.get()); + SendMsgToChar(" : ", d->character.get()); break; case '5': OLC_MODE(d) = TRIGEDIT_ARGUMENT; - SendMsgToChar("Аргументы: ", d->character.get()); + SendMsgToChar(": ", d->character.get()); break; case '6': if (TrigeditIsLuaTrigger(OLC_TRIG(d)) && !TrigeditLuaEditingEnabled()) { - SendMsgToChar("Редактирование Lua-скриптов недоступно в этой сборке сервера.\r\n", + SendMsgToChar(" Lua- .\r\n", d->character.get()); trigedit_disp_menu(d); return; } OLC_MODE(d) = TRIGEDIT_COMMANDS; SendMsgToChar(TrigeditIsLuaTrigger(OLC_TRIG(d)) - ? "Введите Lua-скрипт триггера: (/s saves /h for help)\r\n\r\n" - : "Введите команды триггера: (/s saves /h for help)\r\n\r\n", + ? " Lua- : (/s saves /h for help)\r\n\r\n" + : " : (/s saves /h for help)\r\n\r\n", d->character.get()); d->backstr = nullptr; if (OLC_STORAGE(d)) { @@ -499,7 +499,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { case '7': if (OLC_TRIG(d)->get_attach_type() == MOB_TRIGGER) { OLC_MODE(d) = TRIGEDIT_ADDFLAG; - SendMsgToChar("Флаг (1-ДА): ", d->character.get()); + SendMsgToChar(" (1-): ", d->character.get()); } else { trigedit_disp_menu(d); } @@ -514,9 +514,9 @@ void trigedit_parse(DescriptorData *d, char *arg) { case '9': if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { - SendMsgToChar("Автоформат доступен только для Lua-триггеров.\r\n", d->character.get()); + SendMsgToChar(" Lua-.\r\n", d->character.get()); } else if (!lua_scripting::LuaFormatterAvailable()) { - SendMsgToChar("Форматтер Lua недоступен в этой сборке.\r\n", d->character.get()); + SendMsgToChar(" Lua .\r\n", d->character.get()); } else if (TrigeditFormatLua(d)) { return; } @@ -535,7 +535,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (TrigeditFormatLuaBeforeSave(d)) { return; } - SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); + SendMsgToChar(" Lua- .\r\n", d->character.get()); } TrigeditSaveAndExit(d); return; @@ -546,8 +546,8 @@ void trigedit_parse(DescriptorData *d, char *arg) { case 'a': // abort quitting break; - default: SendMsgToChar("Неверный выбор!\r\n", d->character.get()); - SendMsgToChar("Желаете ли вы сохранить изменения в триггере? : ", d->character.get()); + default: SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar(" ? : ", d->character.get()); return; } break; @@ -581,17 +581,17 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { OLC_TRIG(d)->set_script_language(TriggerScriptLanguage::Lua); strcpy(OLC_STORAGE(d), kDefaultLuaTriggerTemplate); - SendMsgToChar("Тело триггера заменено на шаблон Lua.\r\n", d->character.get()); + SendMsgToChar(" Lua.\r\n", d->character.get()); OLC_VAL(d)++; } } else if (TrigeditIsLuaTrigger(OLC_TRIG(d))) { OLC_TRIG(d)->set_script_language(TriggerScriptLanguage::Dg); strcpy(OLC_STORAGE(d), kDefaultDgTriggerTemplate); - SendMsgToChar("Тело триггера заменено на шаблон DG.\r\n", d->character.get()); + SendMsgToChar(" DG.\r\n", d->character.get()); OLC_VAL(d)++; } #else - SendMsgToChar("Lua-триггеры недоступны в этой сборке сервера.\r\n", d->character.get()); + SendMsgToChar("Lua- .\r\n", d->character.get()); #endif break; @@ -629,7 +629,7 @@ void sprintbyts(int data, char *dest) { } void TriggerDistribution(DescriptorData *d) { switch (OLC_TRIG(d)->get_attach_type()) { - case WLD_TRIGGER: // в комнатах ссылка на прототип + case WLD_TRIGGER: // for (RoomRnum nr = kFirstRoom; nr <= top_of_world; nr++) { if (!SCRIPT(world[nr])->has_triggers()) continue; @@ -689,7 +689,7 @@ void trigedit_save(DescriptorData *d) { DescriptorData *dsc; if (TrigeditIsLuaTrigger(trig) && !TrigeditLuaEditingEnabled()) { - SendMsgToChar("ОШИБКА: Lua-триггеры недоступны в этой сборке сервера.\r\n", d->character.get()); + SendMsgToChar(": Lua- .\r\n", d->character.get()); return; } @@ -698,9 +698,9 @@ void trigedit_save(DescriptorData *d) { TrigeditApplyStorageToTrigger(trig, storage_str); if ((trig_rnum = GetTriggerRnum(OLC_NUM(d))) != -1) { - // Этот триггер уже есть. + // . - // Очистка прототипа + // proto = trig_index[trig_rnum]->proto; proto->cmdlist.reset(); @@ -718,14 +718,14 @@ void trigedit_save(DescriptorData *d) { trigger->arglist.clear(); trigger->set_name(""); if (GET_TRIG_WAIT(trigger).time_remaining > 0) { - free(GET_TRIG_WAIT(trigger).info); // Причина уже обсуждалась + free(GET_TRIG_WAIT(trigger).info); // remove_event(GET_TRIG_WAIT(trigger)); } #if defined(WITH_LUAJIT_PROTOTYPE) - // issue #3568: у живого инстанса может висеть приостановленная Lua-корутина - // (LuaWaitState хранит Trigger*). Перезапись тела ниже (*trigger = *proto), - // особенно при смене Lua->DG, оставила бы вейт-реестр ссылаться на изменённый - // триггер -> UAF/порча кучи. Отменяем Lua-вейты, как это делает ExtractTrigger. + // issue #3568: Lua- + // (LuaWaitState Trigger*). (*trigger = *proto), + // Lua->DG, - Σ + // -> UAF/ . Lua-, ExtractTrigger. lua_scripting::LuaScriptEngine::CancelWaitsForTrigger(trigger); #endif @@ -752,12 +752,12 @@ void trigedit_save(DescriptorData *d) { new_index[trig_rnum]->func = nullptr; new_index[trig_rnum]->proto = new Trigger(*trig); --i; - continue; // повторить копирование еще раз, но уже по-другому + continue; // , - } else { new_index[i] = trig_index[i]; } } else { - // докопировать + // new_index[i + 1] = trig_index[i]; proto = trig_index[i]->proto; proto->set_rnum(i + 1); @@ -794,9 +794,9 @@ void trigedit_save(DescriptorData *d) { } } - // Индекс "предмет -> грузящие его триггеры" строится при загрузке мира, - // так что для правленого скрипта его надо пересчитать -- иначе 'vnum trig' - // будет показывать старую картину до ближайшей перезагрузки. + // " -> " , + // -- 'vnum trig' + // . ReindexTriggerObjLoads(OLC_NUM(d), trig_index[trig_rnum]->proto); // Save trigger to disk using data source abstraction (YAML/SQLite/Legacy) @@ -805,11 +805,11 @@ void trigedit_save(DescriptorData *d) { auto* data_source = world_loader::WorldDataSourceManager::Instance().GetDataSource(); if (!data_source->SaveTriggers(OLC_ZNUM(d), OLC_NUM(d), notify_level)) { - SendMsgToChar("ОШИБКА: Не удалось сохранить триггер!\r\n", d->character.get()); + SendMsgToChar(": !\r\n", d->character.get()); return; } - SendMsgToChar("Триггер сохранен.\r\n", d->character.get()); + SendMsgToChar(" .\r\n", d->character.get()); } // Save all triggers for a zone to disk (without requiring DescriptorData) @@ -832,7 +832,7 @@ bool trigedit_save_to_disk(int zone_rnum, int notify_level) { top = zone_table[zone_rnum].top; if (zone >= dungeons::kZoneStartDungeons) { - snprintf(buf, sizeof(buf), "Отказ сохранения зоны %d на диск.", zone); + snprintf(buf, sizeof(buf), " %d .", zone); mudlog(buf, CMP, kLvlGreatGod, SYSLOG, true); return false; } @@ -948,19 +948,19 @@ void trigedit_create_index(int znum, const char *type) { /************************************************************************** - * Редактирование ПРОТОТИПОВ СКРИПТОВ + * * trigedit **************************************************************************/ void dg_olc_script_free(DescriptorData *d) -// Удаление прототипа в OLC_SCRIPT +// OLC_SCRIPT { OLC_SCRIPT(d).clear(); } void dg_olc_script_copy(DescriptorData *d) -// Создание копии прототипа скрипта для текщего редактируемого моба/объекта/комнаты +// // { switch (OLC_ITEM_TYPE(d)) { case MOB_TRIGGER: OLC_SCRIPT(d) = *OLC_MOB(d)->proto_script; @@ -1006,11 +1006,11 @@ void dg_script_menu(DescriptorData *d) { } snprintf(buf, sizeof(buf), "\r\n" - " %sN%s) Новый триггер для этого скрипта\r\n" - " %sD%s) Удалить триггер в этом скрипте\r\n" - " %sX%s) Выйти из редактора скрипта\r\n" - " %sQ%s) Выйти из редактора скрипта (без сохранения) \r\n\r\n" - " Введите выбранное :", grn, nrm, grn, nrm, grn, nrm, grn, nrm); + " %sN%s) \r\n" + " %sD%s) \r\n" + " %sX%s) \r\n" + " %sQ%s) ( ) \r\n\r\n" + " :", grn, nrm, grn, nrm, grn, nrm, grn, nrm); SendMsgToChar(buf, d->character.get()); } @@ -1028,7 +1028,7 @@ int dg_script_edit_parse(DescriptorData *d, char *arg) { } else { OLC_SCRIPT(d).swap(*OLC_ROOM(d)->proto_script); } - // тут break не нужен + // break // fall through case 'q': dg_olc_script_free(d); @@ -1063,8 +1063,8 @@ int dg_script_edit_parse(DescriptorData *d, char *arg) { } if (GetTriggerRnum(vnum) < 0) { - SendMsgToChar("Неверный VNUM триггера!\r\n" - "Пожалуйста, выберите позицию, vnum (ex: 1, 200):", d->character.get()); + SendMsgToChar(" VNUM !\r\n" + ", , vnum (ex: 1, 200):", d->character.get()); return 1; } diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index b0d0fa529..07adb6ae2 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -72,7 +72,7 @@ constexpr long long kPulsesPerMudHour = kSecsPerMudHour*kPassesPerSec; inline bool IS_CHARMED(CharData* ch) {return (mount::IsHorse(ch) || AFF_FLAGGED(ch, EAffect::kCharmed));} -// Вывод сообщений о неверных управляющих конструкциях DGScript +// DGScript #define DG_CODE_ANALYZE // external vars from triggers.cpp @@ -83,7 +83,7 @@ int last_trig_vnum = 0; int curr_trig_vnum = 0; int last_trig_line_num = 0; -// списки сущностей с триггерами смены времени +// std::unordered_set timechange_mobs; std::unordered_set timechange_objs; std::unordered_set timechange_rooms; @@ -132,7 +132,7 @@ int trgvar_in_room(int vnum); void do_worldecho(char *msg); /* -Костыль, но всеж. +, . */ bool CharacterLinkDrop = false; @@ -193,7 +193,7 @@ void script_log(const char *msg, LogMode type) { */ void trig_log(Trigger *trig, std::string msg, LogMode type) { char tmpbuf[kMaxStringLength]; - snprintf(tmpbuf, kMaxStringLength, "(Trigger: %s, VNum: %d) : %s [строка: %d]", GET_TRIG_NAME(trig), + snprintf(tmpbuf, kMaxStringLength, "(Trigger: %s, VNum: %d) : %s [: %d]", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig), msg.c_str(), last_trig_line_num); script_log(tmpbuf, type); } @@ -206,7 +206,7 @@ cmdlist_element::shared_ptr find_continue_done(cmdlist_element::shared_ptr cl); std::list worlds_vars; -// Для отслеживания работы команд "wat" и "mat" +// "wat" "mat" int reloc_target = -1; Trigger *cur_trig = nullptr; @@ -411,8 +411,8 @@ RoomData *find_room(long n) { ************************************************************/ /** - * Возвращает id моба указанного внума. - * \param num - если есть и больше 0 - возвращает id не первого моба, а указанного порядкового номера. + * id . + * \param num - 0 - id , . */ int find_id_by_char_vnum(int vnum, int num = 0) { int count = 0; @@ -430,8 +430,8 @@ int find_id_by_char_vnum(int vnum, int num = 0) { } /** - * Возвращает id объекта указанного внума. - * \param num - если есть и больше 0 - возвращает id не первого моба, а указанного порядкового номера. + * id . + * \param num - 0 - id , . */ int find_id_by_obj_vnum(int vnum, int num = 0) { int count = 0; @@ -449,8 +449,8 @@ int find_id_by_obj_vnum(int vnum, int num = 0) { } // return room with VNUM n -// Внимание! Для комнаты UID = ROOM_ID_BASE+VNUM, т.к. -// RNUM может быть независимо изменен с помощью OLC +// ! UID = ROOM_ID_BASE+VNUM, .. +// RNUM OLC int find_vnumum(long n) { // return (GetRoomRnum (n) != kNowhere) ? ROOM_ID_BASE + n : -1; return (GetRoomRnum(n) != kNowhere) ? n : -1; @@ -464,11 +464,11 @@ int find_room_uid(long n) { * generic searches based only on name ************************************************************/ -// Чисто-цифровая строка (без UID-префикса) -- это vnum, а не имя/UID. -// Используется в process_attach/detach/run для предупреждения билдеров -// о паттернах вида "detach %vnum%" -- они проваливаются в O(N) -// сканы по character_list / object_list, прежде чем дойти до get_room. -// См. issue #3232 (488мс на одну зону вместо 4мс). +// - ( UID-) -- vnum, /UID. +// process_attach/detach/run +// "detach %vnum%" -- O(N) +// character_list / object_list, get_room. +// . issue #3232 (488 4). bool is_plain_vnum_string(const char *name) { if (!name || !*name) { return false; @@ -485,7 +485,7 @@ bool is_plain_vnum_string(const char *name) { CharData *get_char(const char *name) { CharData *i; - // Отсекаем поиск левых UID-ов. + // UID-. if ((*name == UID_OBJ) || (*name == UID_ROOM)) return nullptr; @@ -794,8 +794,8 @@ void script_trigger_check(int mode) { } } -// проверка каждый час на триги изменении времени -// итерация по спискам зарегистрированных сущностей вместо перебора всего мира +// +// void script_timechange_trigger_check(const int time, const int time_day) { utils::CExecutionTimer timercheck; @@ -831,16 +831,16 @@ EVENT(trig_wait_event) { go = wait_event_obj->go; type = wait_event_obj->type; GET_TRIG_WAIT(trig).time_remaining = 0; - // issue #3523: from_current выставляется только при паузе триггера из-за стана - // моба. Логируем возобновление лишь когда триггер РЕАЛЬНО продолжится, иначе - // лог о "продолжил работу" был бы ложным. Проверять надо ДО script_driver: - // после него trig может быть уже удалён (remove_trigger -> ExtractTrigger). - // - цикл script_driver зайдёт только если есть строка (curr_line), триггер - // не свёрнут (GET_TRIG_DEPTH) и не остановлен (is_halted); - // - моб не выведен из мира (in_room != kNowhere: extract_char уже вынул бы - // его из комнаты, а script_driver ничего не выполнит); - // - моб вышел из стана: иначе интерпретатор снова повесит wait и команда - // опять не выполнится. + // issue #3523: from_current - + // . , + // " " . script_driver: + // trig ̣ (remove_trigger -> ExtractTrigger). + // - script_driver ģ (curr_line), + // ף (GET_TRIG_DEPTH) (is_halted); + // - (in_room != kNowhere: extract_char + // , script_driver ); + // - : wait + // . if (wait_event_obj->from_current && type == MOB_TRIGGER && go && trig && trig->curr_line && GET_TRIG_DEPTH(trig) && !trig->is_halted()) { auto *mob = static_cast(go); @@ -848,7 +848,7 @@ EVENT(trig_wait_event) { && !AFF_FLAGGED(mob, EAffect::kHold) && !AFF_FLAGGED(mob, EAffect::kStopFight) && !AFF_FLAGGED(mob, EAffect::kMagicStopFight)) { - mudlog(fmt::format("DG: триггер {} #{} продолжил работу после лага моба {} #{}", + mudlog(fmt::format("DG: {} #{} {} #{}", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig), GET_SHORT(mob), GET_MOB_VNUM(mob)), CMP, kLvlGod, SYSLOG, true); } @@ -888,7 +888,7 @@ void do_stat_trigger(CharData *ch, Trigger *trig, bool need_num) { if (trig->get_attach_type() == MOB_TRIGGER) { snprintf(sb, sizeof(sb), "Trigger Type: %s, Numeric Arg: %d, Execute mob command: %s, Arg list: %s\r\n", - buf, GET_TRIG_NARG(trig), trig->add_flag ? "ДА" : "НЕТ", !trig->arglist.empty() ? trig->arglist.c_str() : "None"); + buf, GET_TRIG_NARG(trig), trig->add_flag ? "" : "", !trig->arglist.empty() ? trig->arglist.c_str() : "None"); } else { snprintf(sb, sizeof(sb), "Trigger Type: %s, Numeric Arg: %d, Arg list: %s\r\n", buf, GET_TRIG_NARG(trig), !trig->arglist.empty() ? trig->arglist.c_str() : "None"); @@ -944,15 +944,15 @@ void find_uid_name(const char *uid, char *name, size_t name_size) { const auto FOREACH_LIST_GUID = "{18B3D8D1-240E-4D60-AEAB-6748580CA460}"; const auto FOREACH_LIST_POS_GUID = "{4CC4E031-7376-4EED-AD4F-2FD0DC8D4E2D}"; -// некоторые dg-функции (например foreach) создают внутренние переменные для работы -// выводим их в удобочитаемой форме, чтоб никого не смущали непонятные переменные в выводе +// dg- ( foreach) +// , static std::string print_variable_name(const std::string &name) { static std::map text_mapping_list = { {FOREACH_LIST_GUID, "[list] "}, {FOREACH_LIST_POS_GUID, "[position] "} }; - // включение/отключение вывода внутреннего состояния foreach + // / foreach const bool display_state_vars = true; std::string result = name; @@ -1015,7 +1015,7 @@ void script_stat(CharData *ch, Script *sc) { std::stringstream buffer; if (t->get_attach_type() == MOB_TRIGGER) { buffer << " Trigger Type: " << buf1 << ", Numeric Arg:" << GET_TRIG_NARG(t) - << " , Execute mob command: " << (t->add_flag ? "ДА" : "НЕТ") + << " , Execute mob command: " << (t->add_flag ? "" : "") << " , Arg list:" << (!t->arglist.empty() ? t->arglist.c_str() : "None"); } else { buffer << " Trigger Type: " << buf1 << ", Numeric Arg:" << GET_TRIG_NARG(t) @@ -1253,7 +1253,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { room = world[ch->in_room]; if (!SCRIPT(room)->has_triggers()) { SendMsgToChar("This room does not have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { room->cleanup_script(); SendMsgToChar("All triggers removed from room.\r\n", ch); @@ -1298,7 +1298,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar("Players don't have triggers.\r\n", ch); } else if (!SCRIPT(victim)->has_triggers()) { SendMsgToChar("That mob doesn't have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { victim->cleanup_script(); snprintf(buf, sizeof(buf), "All triggers removed from %s.\r\n", GET_SHORT(victim)); timechange_unregister_mob(victim); @@ -1314,7 +1314,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else if (object) { if (!object->get_script()->has_triggers()) { SendMsgToChar("That object doesn't have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { object->cleanup_script(); snprintf(buf, sizeof(buf), "All triggers removed from %s.\r\n", !object->get_short_description().empty() ? object->get_short_description().c_str() @@ -1334,13 +1334,13 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { void add_var_cntx(std::list &var_list, std::string name, std::string value, long id) { /*++ - Добавление переменной в список с учетом контекста (СТРОГИЙ поиск). - При добавлении в список локальных переменных контекст должен быть 0. + ( ). + 0. - var_list - список переменных - name - имя переменной - value - значение переменной - id - контекст переменной + var_list - + name - + value - + id - --*/ TriggerVar vd; @@ -1358,17 +1358,17 @@ void add_var_cntx(std::list &var_list, std::string name, std::string } TriggerVar find_var_cntx(std::list var_list, std::string name, long id) { -/* Поиск переменной с учетом контекста (НЕСТРОГИЙ поиск). +/* ( ). - Поиск осуществляется по паре ИМЯ:КОНТЕКСТ. - 1. Имя переменной должно совпадать с параметром name - 2. Контекст переменной должен совпадать с параметром id, если - такой переменной нет, производится попытка найти переменную - с контекстом 0. + :. + 1. name + 2. id, + , + 0. - var_list - указатель на первый элемент списка переменных - name - имя переменной - id - контекст переменной + var_list - + name - + id - --*/ utils::ConvertToLow(name); auto it = std::find_if(var_list.begin(), var_list.end(), [&name, id](TriggerVar vd) { return (vd.name == name) && (vd.context == id); }); @@ -1380,17 +1380,17 @@ TriggerVar find_var_cntx(std::list var_list, std::string name, long int remove_var_cntx(std::list &var_list, std::string name, long id) { /*++ - Удаление переменной из списка с учетом контекста (СТРОГИЙ поиск). + ( ). - Поиск строгий. + . - var_list - список переменных - name - имя переменной - id - контекст переменной + var_list - + name - + id - - Возвращает: - 1 - переменная найдена и удалена - 0 - переменная не найдена + : + 1 - + 0 - --*/ utils::ConvertToLow(name); @@ -1414,7 +1414,7 @@ bool CheckSript(const RoomData *go, const long type) { return !SCRIPT(go)->is_purged() && SCRIPT(go)->has_triggers() && IS_SET(SCRIPT_TYPES(go->script), type); } -// * Изменение указанной целочисленной константы +// * long gm_char_field(CharData *ch, char *field, char *subfield, long val) { int tmpval; if (*subfield) { @@ -1452,7 +1452,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t snprintf(str, str_size, "%s", utils::TrimCopy(vd.value).c_str()); return true; } else if (!str_cmp(field, "fullword")) { -//не работает с русским +// /* std::string word = subfield; std::string sentence = vd->value; std::regex rgx("\\b" + word + "\\b"); @@ -1463,9 +1463,9 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t std::istringstream is(sentence); std::string temp; while(is >> temp) - tmpstr.push_back(temp); // перенесем фразу в вектор + tmpstr.push_back(temp); // if (std::find(tmpstr.begin(), tmpstr.end(), subfield) != tmpstr.end()) -// вариант номер два +// /* std::string sentence = vd->value; std::stringstream parser(sentence); std::istream_iterator start(parser); @@ -1503,7 +1503,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t return true; } else if (!str_cmp(field, "words")) { int n = 0; - // Подсчет количества слов или получение слова + // char buf1[kMaxTrglineLength]; char buf2[kMaxTrglineLength]; buf1[0] = 0; @@ -1546,7 +1546,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t return false; } -// Команды загрузки из dg_*cmd.cpp - для world.loadobj (загрузка по типу триггера) +// dg_*cmd.cpp - world.loadobj ( ) void do_mload(CharData *ch, char *argument, int cmd, int subcmd, Trigger *trig); void do_dgoload(ObjData *obj, char *argument, int cmd, int subcmd, Trigger *trig); void do_wload(RoomData *room, char *argument, int cmd, int subcmd, Trigger *trig); @@ -1588,7 +1588,7 @@ void find_replacement(void *go, const char *spellturntemp[] = {"mspellturntemp", "ospellturntemp", "wspellturntemp"}; if (!subfield) { - subfield = nullptr; // Чтобы проверок меньше было + subfield = nullptr; // } // X.global() will have a NULL trig @@ -1607,7 +1607,7 @@ void find_replacement(void *go, } else { if (!str_cmp(var, "self")) { long uid; - // заменить на UID + // UID switch (type) { case MOB_TRIGGER: uid = ((CharData *) go)->get_uid(); uid_type = UID_CHAR; @@ -1724,7 +1724,7 @@ void find_replacement(void *go, } else if (!str_cmp(var, "exist")) { if (!str_cmp(field, "mob") && (num = atoi(subfield)) > 0) { if (GetMobRnum(num) <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в exist.mob", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) exist.mob", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1734,7 +1734,7 @@ void find_replacement(void *go, auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в exist.obj", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) exist.obj", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1758,7 +1758,7 @@ void find_replacement(void *go, auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в curobjs", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) curobjs", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1772,7 +1772,7 @@ void find_replacement(void *go, int count = 0; if (rnum <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в gameobjs", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) gameobjs", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1790,7 +1790,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format("В зоне {} нет комнат", num)); + trig_log(trig, fmt::format(" {} ", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1804,7 +1804,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format("В зоне {} нет комнат", num)); + trig_log(trig, fmt::format(" {} ", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1831,7 +1831,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format("В зоне {} нет комнат", num)); + trig_log(trig, fmt::format(" {} ", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1854,20 +1854,20 @@ void find_replacement(void *go, } else if (!str_cmp(field, "recalczone")) { auto param = utils::Split(subfield, ','); if (param.size() != 4) { - trig_log(trig, "recalc: несоответствие параметров"); + trig_log(trig, "recalc: "); return; } ZoneRnum zrn = GetZoneRnum(atoi(param[0].c_str())); if (zrn == 0) { - trig_log(trig, "recalc: нет такой зоны"); + trig_log(trig, "recalc: "); return; } - // Строка нужна для разбора по логу, а не богам онлайн, поэтому log() в syslog, а не - // trig_log(), который дублирует запись в канал ERRLOG. Заодно пишем зону-оригинал - // (copy_from_zone): по одному номеру инстанса данжа непонятно, что пересчитывается. - // Имя и внум триггера подставляем сами -- их давал trig_log, а без них не найти, - // кто затеял пересчет. - log(fmt::format("Пересчитываю зону {} (оригинал {}) {} {} {} [триггер {}, VNum: {}]", + // , , log() syslog, + // trig_log(), ERRLOG. - + // (copy_from_zone): , . + // -- trig_log, , + // . + log(fmt::format(" {} ( {}) {} {} {} [ {}, VNum: {}]", param[0], zone_table[zrn].copy_from_zone, param[1], param[2], param[3], GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig))); auto value = param[0] + " " + param[1]+ " " + param[2] + " " +param[3]; @@ -1876,7 +1876,7 @@ void find_replacement(void *go, for (auto rrn = zone_table[zrn].RnumRoomsLocation.first; rrn <= zone_table[zrn].RnumRoomsLocation.second; rrn++) { dungeons::ClearRoom(world[rrn]); } - dungeons::ClearRoom(world[zone_table[zrn].RnumRoomsLocation.second + 1]); //виртуалку + dungeons::ClearRoom(world[zone_table[zrn].RnumRoomsLocation.second + 1]); // zone_repop_list.push_back(zrn); DecayObjectsOnRepop(zone_repop_list); ResetZone(zrn); @@ -1913,7 +1913,7 @@ void find_replacement(void *go, UniqueList zone_repop_list; ZoneRnum zrn = GetZoneRnum(num); if (zrn < 0) { - trig_log(trig, "Некорректный номер зоны в zreset"); + trig_log(trig, " zreset"); return; } zone_repop_list.push_back(zrn); @@ -1943,7 +1943,7 @@ void find_replacement(void *go, const auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в canbeloaded", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) canbeloaded", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1957,18 +1957,18 @@ void find_replacement(void *go, snprintf(str, str_size, "0"); } } else if (!str_cmp(field, "loadobj") && num > 0) { - // world.loadobj(vnum, chance): загрузить предмет с шансом chance (1..1000 промилле), - // учитывая MIW. Размещение делает штатная команда load по типу триггера. - // Возвращает "1" при загрузке, иначе "0". Загруженный предмет доступен через - // %LoadedUid% (выставляет load-команда). + // world.loadobj(vnum, chance): chance (1..1000 ), + // MIW. load . + // "1" , "0". + // %LoadedUid% ( load-). const auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в loadobj", num).c_str()); + trig_log(trig, fmt::format(" vnum ({}) loadobj", num).c_str()); snprintf(str, str_size, "0"); return; } - // второй аргумент (шанс) после vnum в subfield + // () vnum subfield const char *p = subfield; while (*p && *p != ',' && *p != ' ') { p++; @@ -1979,11 +1979,11 @@ void find_replacement(void *go, const int chance = atoi(p); if (chance < 1 || chance > 1000) { - trig_log(trig, fmt::format("Неверный шанс ({}) в loadobj, нужно 1..1000", chance).c_str()); + trig_log(trig, fmt::format(" ({}) loadobj, 1..1000", chance).c_str()); snprintf(str, str_size, "0"); return; } - // проверка максимума в мире (логика как в CanBeLoaded) + // ( CanBeLoaded) const bool can_load = stable_objs::IsTimerUnlimited(obj_proto[rnum].get()) || (GetObjMIW(rnum) < 0) || (obj_proto.actual_count(rnum) < GetObjMIW(rnum)); @@ -1992,12 +1992,12 @@ void find_replacement(void *go, snprintf(str, str_size, "0"); return; } - // бросок шанса + // if (number(1, 1000) > chance) { snprintf(str, str_size, "0"); return; } - // размещение штатной командой load в зависимости от типа триггера + // load char loadarg[kMaxInputLength]; snprintf(loadarg, sizeof(loadarg), "obj %d", num); switch (type) { @@ -2010,8 +2010,8 @@ void find_replacement(void *go, } else if ((!str_cmp(field, "maxobj") || !str_cmp(field, "maxobjs")) && num > 0) { num = GetObjRnum(num); if (num >= 0) { - // если у прототипа беск.таймер, - // то их оч много в мире + // ., + // if (stable_objs::IsTimerUnlimited(obj_proto[num].get()) || (GetObjMIW(num) < 0)) snprintf(str, str_size, "9999999"); else @@ -2047,24 +2047,24 @@ void find_replacement(void *go, } } else if (!str_cmp(field, "sunlight")) { switch (weather_info.sunlight) { - case kSunDark: snprintf(str, str_size, "ночь"); + case kSunDark: snprintf(str, str_size, ""); break; - case kSunSet: snprintf(str, str_size, "закат"); + case kSunSet: snprintf(str, str_size, ""); break; - case kSunLight: snprintf(str, str_size, "день"); + case kSunLight: snprintf(str, str_size, ""); break; - case kSunRise: snprintf(str, str_size, "рассвет"); + case kSunRise: snprintf(str, str_size, ""); break; } } else if (!str_cmp(field, "season")) { switch (weather_info.season) { - case ESeason::kWinter: snprintf(str, str_size, "зима"); + case ESeason::kWinter: snprintf(str, str_size, ""); break; - case ESeason::kSpring: snprintf(str, str_size, "весна"); + case ESeason::kSpring: snprintf(str, str_size, ""); break; - case ESeason::kSummer: snprintf(str, str_size, "лето"); + case ESeason::kSummer: snprintf(str, str_size, ""); break; - case ESeason::kAutumn: snprintf(str, str_size, "осень"); + case ESeason::kAutumn: snprintf(str, str_size, ""); break; } } @@ -2216,7 +2216,7 @@ void find_replacement(void *go, ++count; } } - snprintf(str, str_size, "%d", count + 1); // +1 за первое слово + snprintf(str, str_size, "%d", count + 1); // +1 return; } *(p++) = '\0'; @@ -2250,7 +2250,7 @@ void find_replacement(void *go, std::vector tokens = utils::Split(arg, ','); if (tokens.size() < 2 || tokens.size() > 3) { - snprintf(buf, sizeof(buf), "array.find: путанница в количестве аргументов"); + snprintf(buf, sizeof(buf), "array.find: "); trig_log(trig, buf); return; } @@ -2259,12 +2259,12 @@ void find_replacement(void *go, index = std::stoi(tokens.at(2)); } catch (const std::invalid_argument &) { - snprintf(buf, sizeof(buf), "array.find: index кривой, указано '%s'", tokens.at(2).c_str()); + snprintf(buf, sizeof(buf), "array.find: index , '%s'", tokens.at(2).c_str()); trig_log(trig, buf); return; } if (index < 0) { - snprintf(buf, sizeof(buf), "array.find: index меньше нуля, указано '%s'", tokens.at(2).c_str()); + snprintf(buf, sizeof(buf), "array.find: index , '%s'", tokens.at(2).c_str()); trig_log(trig, buf); return; } @@ -2272,7 +2272,7 @@ void find_replacement(void *go, std::vector arr = utils::Split(tokens.at(0)); std::string elem = tokens.at(1); if (index > static_cast(arr.size())) { - snprintf(buf, sizeof(buf), "%s", "array.find: index больше размера массива"); + snprintf(buf, sizeof(buf), "%s", "array.find: index "); trig_log(trig, buf); return; } @@ -2295,7 +2295,7 @@ void find_replacement(void *go, int index = 0; if (tokens.size() != 2) { - snprintf(buf, sizeof(buf), "array.remove: путанница в количестве аргументов"); + snprintf(buf, sizeof(buf), "array.remove: "); trig_log(trig, buf); return; } @@ -2303,22 +2303,22 @@ void find_replacement(void *go, index = std::stoi(tokens.at(1)); } catch (const std::invalid_argument &) { - snprintf(buf, sizeof(buf), "array.remove: index кривой или отсуствует, указано '%s'", tokens.at(1).c_str()); + snprintf(buf, sizeof(buf), "array.remove: index , '%s'", tokens.at(1).c_str()); trig_log(trig, buf); return; } if (index < 1) { - snprintf(buf, sizeof(buf), "array.remove: index меньше единицы, указано '%s'", tokens.at(1).c_str()); + snprintf(buf, sizeof(buf), "array.remove: index , '%s'", tokens.at(1).c_str()); trig_log(trig, buf); return; } std::vector arr = utils::Split(tokens.at(0)); if (index > static_cast(arr.size())) { - snprintf(buf, sizeof(buf), "%s", "array.remove: index больше размера массива"); + snprintf(buf, sizeof(buf), "%s", "array.remove: index "); trig_log(trig, buf); return; } - index--; // в DG массивы с 1 + index--; // DG 1 auto it = arr.begin(); std::advance(it, index); arr.erase(it); @@ -2332,7 +2332,7 @@ void find_replacement(void *go, snprintf(str, str_size, "%s", ss.c_str()); return; } - /*Вот тут можно наделать вот так еще: + /* : else if (!str_cmp(field, "pop")) {} else if (!str_cmp(field, "shift")) {} else if (!str_cmp(field, "push")) {} @@ -2403,7 +2403,7 @@ void find_replacement(void *go, mob->quested_remove(num); return; } else { - trig_log(trig, "Ошибка в параметрах unsetquest"); + trig_log(trig, " unsetquest"); return; } } @@ -2507,7 +2507,7 @@ void find_replacement(void *go, else snprintf(str, str_size, "%d", mob->get_hit_add()); } else if (!str_cmp(field, "maxhitp")) { - if (*subfield && mob->IsNpc()) // доступно тока мобам + if (*subfield && mob->IsNpc()) // mob->set_max_hit((int) gm_char_field(mob, field, subfield, (long) mob->get_max_hit())); else snprintf(str, str_size, "%d", mob->get_max_hit()); @@ -2590,9 +2590,9 @@ void find_replacement(void *go, snprintf(str, str_size, "%d", GET_AC_ADD(mob)); } else if (!str_cmp(field, "ac")) { snprintf(str, str_size, "%d", GetRealAc(mob)); - } else if (!str_cmp(field, "morale")) { // общая сумма морали + } else if (!str_cmp(field, "morale")) { // snprintf(str, str_size, "%d", mob->calc_morale()); - } else if (!str_cmp(field, "moraleadd")) {// добавочная мораль + } else if (!str_cmp(field, "moraleadd")) {// if (*subfield) GET_MORALE(mob) = (int) gm_char_field(mob, field, subfield, (long) GET_MORALE(mob)); else @@ -2610,7 +2610,7 @@ void find_replacement(void *go, } else if (!str_cmp(field, "linkdrop")) { if (!mob->IsNpc() && !mob->desc) { snprintf(str, str_size, "1"); - CharacterLinkDrop = false; // чтоб триггер тут не прерывался для упавших в ЛД + CharacterLinkDrop = false; // } else snprintf(str, str_size, "0"); @@ -2630,19 +2630,19 @@ void find_replacement(void *go, } else if ((!str_cmp(field, "restore")) || (!str_cmp(field, "fullrestore"))) { if (!str_cmp(field, "fullrestore")) { DoArenaRestore(mob, (char *) mob->get_name().c_str(), 0, kScmdRestoreTrigger); - trig_log(trig, "был произведен вызов DoArenaRestore!"); + trig_log(trig, " DoArenaRestore!"); } else { DoRestore(mob, (char *) mob->get_name().c_str(), 0, kScmdRestoreTrigger); - trig_log(trig, "был произведен вызов DoRestore!"); + trig_log(trig, " DoRestore!"); } } else if (!str_cmp(field, "dispel")) { if (!mob->affected.empty()) { - SendMsgToChar("Вы словно заново родились!\r\n", mob); + SendMsgToChar(" !\r\n", mob); mob->affected.clear(); affect_total(mob); } } else if (!str_cmp(field, "Currency")) { - // %actor.Currency()% - кол-во любой валюты на руках (валюта задаётся id-аргументом). + // %actor.Currency()% - - ( id-). const auto *money_cur = ¤cies::FindByTextIdNoCase(subfield); if (money_cur->GetId() < 0) { char errbuf[kMaxInputLength]; @@ -2651,7 +2651,7 @@ void find_replacement(void *go, } snprintf(str, str_size, "%ld", money_cur->GetId() >= 0 ? currencies::GetHand(*mob, money_cur->GetTextId()) : 0L); } else if (!str_cmp(field, "AddCurrency")) { - // %actor.AddCurrency(, )% - начислить (минус снимает). Слава и immortal-валюты не начисляются (страж откажет). + // %actor.AddCurrency(, )% - ( ). immortal- ( ). auto args = utils::Split(subfield, ','); if (args.size() >= 2) { utils::TrimLeft(args[0]); @@ -2679,7 +2679,7 @@ void find_replacement(void *go, currencies::SetHand(*mob, currencies::kGold, std::max(long(0), gm_char_field(mob, field, subfield, currencies::GetHand(*mob, currencies::kGold)))); value = currencies::GetHand(*mob, currencies::kGold) - before; snprintf(buf, sizeof(buf), - "<%s> {%d} получил триггером %d %s. [Trigger: %s, Vnum: %d]", + "<%s> {%d} %d %s. [Trigger: %s, Vnum: %d]", GET_PAD(mob, 0), GET_ROOM_VNUM(mob->in_room), value, @@ -2687,10 +2687,10 @@ void find_replacement(void *go, GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig)); mudlog(buf, NRM, kLvlGreatGod, MONEY_LOG, true); - // клан-налог + // - const long diff = currencies::GetHand(*mob, currencies::kGold) - before; split_or_clan_tax(mob, diff); - // стата для show money + // show money if (!mob->IsNpc() && mob->in_room > 0) { MoneyDropStat::add(zone_table[world[mob->in_room]->zone_rn].vnum, diff); } @@ -2701,10 +2701,10 @@ void find_replacement(void *go, if (*subfield) { const long before = currencies::GetBank(*mob, currencies::kGold); currencies::SetBank(*mob, currencies::kGold, std::max(long(0), gm_char_field(mob, field, subfield, currencies::GetBank(*mob, currencies::kGold)))); - // клан-налог + // - const long diff = currencies::GetBank(*mob, currencies::kGold) - before; split_or_clan_tax(mob, diff); - // стата для show money + // show money if (!mob->IsNpc() && mob->in_room > 0) { MoneyDropStat::add(zone_table[world[mob->in_room]->zone_rn].vnum, diff); } @@ -2714,7 +2714,7 @@ void find_replacement(void *go, if (!str_cmp(field, "questbodrich")) { if (*subfield) { if (IsCharmice(mob)) { -// SendMsgToChar(mob->get_master(), "Квест чармисом, берем мастера\r\n"); +// SendMsgToChar(mob->get_master(), " , \r\n"); mob->get_master()->dquest(atoi(subfield)); } else { @@ -2726,7 +2726,7 @@ void find_replacement(void *go, if (*subfield == '-') { experience::EndowExpToChar(mob, -std::max(1, atoi(subfield + 1))); snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) у %s уменьшен опыт на %d в триггере %d", + "SCRIPT_LOG (exp) %s %d %d", GET_NAME(mob), std::max(1, atoi(subfield + 1)), GET_TRIG_VNUM(trig)); @@ -2734,14 +2734,14 @@ void find_replacement(void *go, } else if (*subfield == '+') { experience::EndowExpToChar(mob, +std::max(1, atoi(subfield + 1))); snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) у %s увеличен опыт на %d в триггере %d", + "SCRIPT_LOG (exp) %s %d %d", GET_NAME(mob), std::max(1, atoi(subfield + 1)), GET_TRIG_VNUM(trig)); mudlog(buf, BRF, kLvlGreatGod, ERRLOG, 1); } else { snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) ОШИБКА! у %s напрямую указан опыт %d в триггере %d", + "SCRIPT_LOG (exp) ! %s %d %d", GET_NAME(mob), atoi(subfield + 1), GET_TRIG_VNUM(trig)); @@ -2801,10 +2801,10 @@ void find_replacement(void *go, else if (!str_cmp(field, "cancarryweight")) snprintf(str, str_size, "%d", CAN_CARRY_W(mob)); else if (!str_cmp(field, "CanBeSeen")) { - // С аргументом %char.canbeseen(<смотрящий>)% - видит ли указанный - // персонаж этого (sight::CanSee учитывает слепоту/невидимость/темноту). - // Без аргумента сохраняем прежнее поведение: в MOB_TRIGGER проверяем - // видимость мобом-хозяином триггера, иначе считаем видимым. + // %char.canbeseen(<>)% - + // (sight::CanSee //). + // : MOB_TRIGGER + // - , . CharData *viewer = *subfield ? get_char(subfield) : nullptr; if (viewer) { snprintf(str, str_size, sight::CanSee(viewer, mob) ? "1" : "0"); @@ -2978,7 +2978,7 @@ void find_replacement(void *go, mob->save_char(); return; } else { - trig_log(trig, "ошибка в параметрах loadroom"); + trig_log(trig, " loadroom"); return; } } @@ -3130,7 +3130,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке ApplyTypes", subfield); + snprintf(buf, sizeof(buf), " '%s' ApplyTypes", subfield); trig_log(trig, buf); return; } @@ -3154,13 +3154,13 @@ void find_replacement(void *go, else if (aff_add) { aff_flags.set(aff_id); *str = '\0'; } else { strcpy(str, aff_flags.get(aff_id) ? "1" : "0"); } } - //подозреваю что никто из билдеров даже не вкурсе насчет всего функционала этого affect - //к тому же аффекты в том списке не все кличи например никак там не отображаются + // affect + // } else if (!str_cmp(field, "affectedby")) { char *p = strchr(subfield, ','); // issue.affect-migration: an applied affect is disconnected from the cause that triggered // it, so "affectedby" tests for a specific AFFECT TYPE, not the casting spell. Builders - // pass the affect's Russian short name (e.g. "мантия теней", "волшебный.щит") -- dots and + // pass the affect's Russian short name (e.g. " ", ".") -- dots and // underscores read as spaces, mirroring the old FixName -- or its enum text-id (kPoisoned). auto resolve_affect = [](const char *name, EAffect &out) -> bool { std::string fixed(name ? name : ""); @@ -3172,7 +3172,7 @@ void find_replacement(void *go, if (!p) { EAffect aff_id; if (!resolve_affect(subfield, aff_id)) { - snprintf(buf, sizeof(buf), "Не найден аффект %s в списке AffectedBy", subfield); + snprintf(buf, sizeof(buf), " %s AffectedBy", subfield); trig_log(trig, buf); return; } @@ -3188,7 +3188,7 @@ void find_replacement(void *go, *(p++) = '\0'; EAffect aff_id; if (!resolve_affect(subfield, aff_id)) { - snprintf(buf, sizeof(buf), "Не найден аффект %s в списке AffectedBy", subfield); + snprintf(buf, sizeof(buf), " %s AffectedBy", subfield); trig_log(trig, buf); return; } @@ -3197,7 +3197,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке AffectedBy", p); + snprintf(buf, sizeof(buf), " '%s' AffectedBy", p); trig_log(trig, buf); return; } @@ -3214,7 +3214,7 @@ void find_replacement(void *go, // mudlog(fmt::format("mob flag {}", subfield)); bool val = mob->char_specials.saved.mob_flags.gm_flag(subfield, action_bits, str); if (!val) { - trig_log(trig, fmt::format("mobflag: неправильный параметр в скобках - ({})", subfield)); + trig_log(trig, fmt::format("mobflag: - ({})", subfield)); return; } } @@ -3223,7 +3223,7 @@ void find_replacement(void *go, // mudlog(fmt::format("npc flag {}", subfield)); bool val = mob->mob_specials.npc_flags.gm_flag(subfield, function_bits, str); if (!val) { - trig_log(trig, fmt::format("npcflag: неправильный параметр в скобках - ({})", subfield)); + trig_log(trig, fmt::format("npcflag: - ({})", subfield)); return; } } @@ -3235,8 +3235,8 @@ void find_replacement(void *go, } } else if (!str_cmp(field, "leader")) { if (subfield && *subfield) { - // %actor.leader(UID)% -- установить следование за указанным - // персонажем (по UID или имени), как делает команда follow (#3398). + // %actor.leader(UID)% -- + // ( UID ), follow (#3398). CharData *new_leader = get_char(subfield); if (new_leader && new_leader != mob && !follow::CircleFollow(mob, new_leader)) { if (mob->has_master()) { @@ -3247,7 +3247,7 @@ void find_replacement(void *go, group::RemoveGroupFlags(f); } follow::AddFollower(new_leader, mob); - // возвращаем UID нового лидера (если следование удалось установить) + // UID ( ) if (mob->get_master() == new_leader) { snprintf(str, str_size, "%c%ld", uid_type, new_leader->get_uid()); } @@ -3264,7 +3264,7 @@ void find_replacement(void *go, if (!l) { l = mob; } - // l - лидер группы + // l - size_t s_len = strlen(str); snprintf(str + s_len, str_size - s_len, "%c%ld ", uid_type, l->get_uid()); for (auto *f : l->followers) { @@ -3323,10 +3323,10 @@ void find_replacement(void *go, || !str_cmp(field, "all")) { int inroom; - // Составление списка (для mob) + // ( mob) inroom = mob->in_room; if (inroom == kNowhere) { - trig_log(trig, "mob-построитель списка в kNowhere"); + trig_log(trig, "mob- kNowhere"); return; } @@ -3443,13 +3443,13 @@ void find_replacement(void *go, } else if (!str_cmp(field, "type")) { snprintf(str, str_size, "%d", (int) obj->get_type()); } else if (!str_cmp(field, "CanBeSeen")) { - // В отличие от CAN_SEE_OBJ, не считаем предмет автоматически видимым - // только потому, что персонаж его держит/носит: иначе слепой всегда - // "видит" предмет в инвентаре. Проверяем слепоту/невидимость/темноту - // напрямую (как MORT_CAN_SEE_OBJ). Освещение берём по комнате - // смотрящего: предмет, который он может видеть, либо у него в руках, - // либо в его же комнате. Если смотрящий в kNowhere - он вне игрового - // мира и видеть не может. + // CAN_SEE_OBJ, + // , /: + // "" . // + // ( MORT_CAN_SEE_OBJ). ң + // : , , , + // . kNowhere - + // . CharData *viewer = *subfield ? get_char(subfield) : nullptr; bool seen = true; if (viewer && !(!viewer->IsNpc() && viewer->IsFlagged(EPrf::kHolylight))) { @@ -3524,13 +3524,13 @@ void find_replacement(void *go, if (!obj->get_dgscript_field().empty()) { saved_info = utils::Split(obj->get_dgscript_field(), '#'); } else { - snprintf(buf, sizeof(buf), "Нет сохраненных переменных"); + snprintf(buf, sizeof(buf), " "); trig_log(trig, buf); } for (auto &it : saved_info) { name = utils::ExtractFirstArgument(it, value); if (name.empty() || value.empty()) { - snprintf(buf, sizeof(buf), "Кривая переменная (нужно 'value text') сейчас '%s'", it.c_str()); + snprintf(buf, sizeof(buf), " ( 'value text') '%s'", it.c_str()); trig_log(trig, buf); continue; } @@ -3540,7 +3540,7 @@ void find_replacement(void *go, } } } else { - snprintf(buf, sizeof(buf), "Нет аргумента в команде LoadVar"); + snprintf(buf, sizeof(buf), " LoadVar"); trig_log(trig, buf); } } else if (!str_cmp(field, "savevar")) { @@ -3559,7 +3559,7 @@ void find_replacement(void *go, vd = find_var_cntx(worlds_vars, subfield, trig->context); } if (vd_tmp.name.empty()) { - snprintf(buf, sizeof(buf), "Не найдена переменная %s", subfield); + snprintf(buf, sizeof(buf), " %s", subfield); trig_log(trig, buf); return; } @@ -3570,7 +3570,7 @@ void find_replacement(void *go, for (auto &it : saved_info) { name = utils::ExtractFirstArgument(it, value); if (name.empty() || value.empty()) { - snprintf(buf, sizeof(buf), "Кривая переменная (нужно 'value text') сейчас '%s'", it.c_str()); + snprintf(buf, sizeof(buf), " ( 'value text') '%s'", it.c_str()); trig_log(trig, buf); continue; } @@ -3586,12 +3586,12 @@ void find_replacement(void *go, out << it << "#"; } if (out.str().size() > kMaxInputLength) { - snprintf(buf, sizeof(buf), "Список переменных переполнен, сократите на %zu символов", out.str().size() - kMaxInputLength); + snprintf(buf, sizeof(buf), " , %zu ", out.str().size() - kMaxInputLength); trig_log(trig, buf); } else obj->set_dgscript_field(out.str()); } else { - snprintf(buf, sizeof(buf), "Нет аргумента в команде SaveVar"); + snprintf(buf, sizeof(buf), " SaveVar"); trig_log(trig, buf); } } else if (!str_cmp(field, "maker")) { @@ -3611,7 +3611,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке apply_types", subfield); + snprintf(buf, sizeof(buf), " '%s' apply_types", subfield); trig_log(trig, buf); return; } @@ -3646,7 +3646,7 @@ void find_replacement(void *go, break; } if (skill_id == ESkill::kLast) { - snprintf(buf, sizeof(buf), "Не найдено умение '%s'", subfield); + snprintf(buf, sizeof(buf), " '%s'", subfield); trig_log(trig, buf); return; } @@ -3702,28 +3702,28 @@ void find_replacement(void *go, CharData *char_to = nullptr; RoomData *room_to = nullptr; if (!((*subfield == UID_CHAR) || (*subfield == UID_CHAR_ALL) || (*subfield == UID_OBJ) || (*subfield == UID_ROOM))) { - trig_log(trig, "object.put: недопустимый аргумент, необходимо указать UID"); + trig_log(trig, "object.put: , UID"); return; } if (*subfield == UID_OBJ) { obj_to = world_objects.find_by_id(atoi(subfield + 1)).get(); if (!(obj_to && obj_to->get_type() == EObjType::kContainer)) { - trig_log(trig, "object.put: объект-приемник не найден или не является контейнером"); + trig_log(trig, "object.put: - "); return; } } if ((*subfield == UID_CHAR) || (*subfield == UID_CHAR_ALL)) { char_to = find_char(atoi(subfield + 1)); if (!char_to) { - trig_log(trig, "object.put: субъект-приемник не найден"); + trig_log(trig, "object.put: - "); return; } } if (*subfield == UID_ROOM) { room_to = find_room(atoi(subfield + 1)); if (!(room_to && (room_to->vnum != kNowhere))) { - trig_log(trig, "object.put: недопустимая комната для размещения объекта"); + trig_log(trig, "object.put: "); return; } } @@ -3738,7 +3738,7 @@ void find_replacement(void *go, } else if (obj->get_in_room() > kNowhere) { RemoveObjFromRoom(obj); } else { - trig_log(trig, "object.put: не удалось извлечь объект"); + trig_log(trig, "object.put: "); return; } //finally, put it to destination @@ -3746,8 +3746,8 @@ void find_replacement(void *go, if (CanTakeObj(char_to, obj)) { PlaceObjToInventory(obj, char_to); } else { - act("Вы не смогли удержать и выбросили $o3 на землю.", false, char_to, obj, nullptr, kToChar); - act("$n не удержал$g $o3 и уронил$g на землю.", false, char_to, obj, nullptr, kToRoom); + act(" $o3 .", false, char_to, obj, nullptr, kToChar); + act("$n $g $o3 $g .", false, char_to, obj, nullptr, kToRoom); PlaceObjToRoom(obj, char_to->in_room); } } else if (obj_to) @@ -3756,7 +3756,7 @@ void find_replacement(void *go, PlaceObjToRoom(obj, GetRoomRnum(room_to->vnum)); else { snprintf(buf2, sizeof(buf2), - "object.put: ATTENTION! за время подготовки объекта >%s< к передаче перестал существовать адресат. Объект сейчас в kNowhere", + "object.put: ATTENTION! >%s< . kNowhere", obj->get_short_description().c_str()); trig_log(trig, buf2); return; @@ -3766,10 +3766,10 @@ void find_replacement(void *go, !str_cmp(field, "pc") || !str_cmp(field, "npc") || !str_cmp(field, "all")) { int inroom; - // Составление списка (для obj) + // ( obj) inroom = obj_room(obj); if (inroom == kNowhere) { - trig_log(trig, "obj-построитель списка в kNowhere"); + trig_log(trig, "obj- kNowhere"); return; } @@ -3803,8 +3803,8 @@ void find_replacement(void *go, if (*subfield) { skip_spaces(&subfield); int num = atoi(subfield); - // Убрал пока проверку. По идее 0 -- отсутствие владельца. - // Понадобилась возможность обнулить владельца из трига. + // . 0 -- . + // . obj->set_owner(num); } else { snprintf(str, str_size, "%d", obj->get_owner()); @@ -3845,13 +3845,13 @@ void find_replacement(void *go, strncpy(str + str_length, tmp, str_size - str_length - 1); str_length += n; } else { - snprintf(buf2, sizeof(buf2), "Предмет VNUM %d данные переполнены, далее содержимое не учитывается", GET_OBJ_VNUM(obj)); + snprintf(buf2, sizeof(buf2), " VNUM %d , ", GET_OBJ_VNUM(obj)); trig_log(trig, buf2); break; // too many carying objects } } } else { - snprintf(buf2, sizeof(buf2), "Предмет VNUM %d не контейнер, поля 'objs' нет.", GET_OBJ_VNUM(obj)); + snprintf(buf2, sizeof(buf2), " VNUM %d , 'objs' .", GET_OBJ_VNUM(obj)); trig_log(trig, buf2); } } else //get global var. obj.varname @@ -3970,10 +3970,10 @@ void find_replacement(void *go, || !str_cmp(field, "all")) { int inroom; - // Составление списка (для room) + // ( room) inroom = GetRoomRnum(room->vnum); if (inroom == kNowhere) { - trig_log(trig, "room-построитель списка в kNowhere"); + trig_log(trig, "room- kNowhere"); return; } @@ -4004,12 +4004,12 @@ void find_replacement(void *go, return; } else if (!str_cmp(field, "objects")) { - //mixaz Выдаем список объектов в комнате + //mixaz int inroom; - // Составление списка (для room) + // ( room) inroom = GetRoomRnum(room->vnum); if (inroom == kNowhere) { - trig_log(trig, "room-построитель списка в kNowhere"); + trig_log(trig, "room- kNowhere"); return; } @@ -4082,10 +4082,10 @@ void var_subst(void *go, Script *sc, Trigger *trig, int type, const char *line, field = p; subfield_p = subfield; //new - // Буфер обязательно гасим: ниже его длина участвует в расчете места под - // вложенную подстановку, а до первой записи здесь лежит мусор со стека. - // Из-за этого у %world.curobjs(66809)% отрезало последний символ -- движок - // ругался на несуществующий внум 6680. + // : + // , . + // - %world.curobjs(66809)% -- + // 6680. *subfield = '\0'; if (*p == '.') { *(p++) = '\0'; @@ -4105,10 +4105,10 @@ void var_subst(void *go, Script *sc, Trigger *trig, int type, const char *line, paren_count--; if (!paren_count) { *local_p = '\0'; - // Остаток буфера считаем по указателю, а не по strlen: подполя пишутся - // друг за другом, и длина содержимого тут ни при чем. Со strlen на - // неинициализированной памяти разность могла уйти в минус и как size_t - // стать огромной -- snprintf писал бы за пределы буфера. + // , strlen: + // , . strlen + // size_t + // -- snprintf . var_subst(go, sc, trig, type, local, subfield_p, sizeof(subfield) - static_cast(subfield_p - subfield)); local_p = nullptr; @@ -4225,9 +4225,9 @@ void eval_op(const char *op, else snprintf(result, result_size, "%d", !*rhs); } else if (!strcmp("#", op)) { - // Остаток от деления. Деление на ноль дает 0 -- так же, как у "/" выше: у DG нет - // способа сообщить билдеру об ошибке в середине выражения, а падать из-за опечатки - // в триггере сервер не должен. + // . 0 -- , "/" : DG + // , - + // . snprintf(result, result_size, "%d", (n = atoi(rhs)) ? (atoi(lhs) % n) : 0); } } @@ -4315,14 +4315,14 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go "/", "*", "!", - // Остаток от деления. Стоит последним, то есть связывает слабее всех остальных: - // "%a% + %b% # 2" читается как "(a + b) # 2". Так оператор и был задуман изначально - // (см. 607bdaa84 в ветке not_sale), и так он описан в руководстве билдеров. + // . , : + // "%a% + %b% # 2" "(a + b) # 2". + // (. 607bdaa84 not_sale), . "#", "\n" }; if (strlen(expr) > kMaxTrglineLength - 1) { - trig_log(trig, fmt::format("Ошибка! Слишком длинное выражение: {}", expr).c_str()); + trig_log(trig, fmt::format("! : {}", expr).c_str()); return 0; } snprintf(line, sizeof(line), "%s", expr); @@ -4367,19 +4367,19 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go } /*++ -cond - строка параметров цикла. Для комнады "foreach i .." cond = "i .." -go - уразатель на MOB/OBJ/ROOM (см. type) +cond - . "foreach i .." cond = "i .." +go - MOB/OBJ/ROOM (. type) sc - SCRIPT(go) -trig - исполняемый триггер -type - тип (MOB_TRIGGER,OBJ_TRIGGER,WLD_TRIGGER) +trig - +type - (MOB_TRIGGER,OBJ_TRIGGER,WLD_TRIGGER) -Запись -foreach i <список> -работает так: + +foreach i <> + : -1. Если список пустой - выйти -2. Переменная i равна к-ому элементу списка. Если это последний элемент - выйти -Иначе i = след. элемент и выполнить тело +1. - +2. i - . - + i = . --*/ // returns 1 if next iteration, else 0 int process_foreach_begin(const char *cond, void *go, Script *sc, Trigger *trig, int type) { @@ -4448,7 +4448,7 @@ int process_foreach_done(const char *cond, void *, Script *, Trigger *trig, int) if (!var_list_value || !var_list_pos_value) { trig_log(trig, "foreach utility vars not found"); - free(var_list_value); // issue #3574: str_dup выше -- освобождаем на всех выходах + free(var_list_value); // issue #3574: str_dup -- free(var_list_pos_value); return 0; } @@ -4463,7 +4463,7 @@ int process_foreach_done(const char *cond, void *, Script *, Trigger *trig, int) snprintf(value, kMaxTrglineLength, "%s%s", name, FOREACH_LIST_POS_GUID); remove_var_cntx(trig->var_list, value, 0); - free(var_list_value); // issue #3574: str_dup выше течёт на завершении foreach + free(var_list_value); // issue #3574: str_dup ޣ foreach free(var_list_pos_value); return 0; } @@ -4677,8 +4677,8 @@ cmdlist_element::shared_ptr find_case(Trigger *trig, } // processes any 'wait' commands in a trigger -// issue #3523: единая точка постановки wait на триггер через штатный -// trig_wait_event. from_current=true -> возобновление с текущей строки. +// issue #3523: wait +// trig_wait_event. from_current=true -> . void hang_trig_wait(void *go, Trigger *trig, int type, long time, bool from_current) { if (time <= 0) { return; @@ -4689,9 +4689,9 @@ void hang_trig_wait(void *go, Trigger *trig, int type, long time, bool from_curr wait_event_obj->go = go; wait_event_obj->type = type; wait_event_obj->from_current = from_current; - // issue #3655: from_current-пауза (стан/лаг) вешается на ТЕКУЩУЮ строку (curr_line). - // Синхронизируем wait_line с ней, иначе show trig показывает протухшую - // строку прошлого wait'а, а не место реальной паузы. + // issue #3655: from_current- (/) (curr_line). + // wait_line , show trig + // wait', . if (from_current && trig->curr_line) { trig->wait_line = trig->curr_line; } @@ -4708,10 +4708,10 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el if ((trig->get_attach_type() == MOB_TRIGGER && IS_SET(GET_TRIG_TYPE(trig), MTRIG_DEATH)) ||(trig->get_attach_type() == OBJ_TRIGGER && IS_SET(GET_TRIG_TYPE(trig), OTRIG_PURGE))) { - snprintf(buf, sizeof(buf), "&YВНИМАНИЕ&G Используется wait в триггере '%s' (VNUM=%d).", + snprintf(buf, sizeof(buf), "&Y&G wait '%s' (VNUM=%d).", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig)); mudlog(buf, BRF, kLvlBuilder, ERRLOG, true); - snprintf(buf, sizeof(buf), "&GКод триггера после wait выполнен НЕ БУДЕТ!"); + snprintf(buf, sizeof(buf), "&G wait !"); mudlog(buf, BRF, kLvlBuilder, ERRLOG, true); } @@ -4748,7 +4748,7 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el } } if (time == 0) { - trig_log(trig, "попытка запустить Wait 0"); + trig_log(trig, " Wait 0"); return; } hang_trig_wait(go, trig, type, time, from_current); @@ -4761,12 +4761,12 @@ void process_set(Script * /*sc*/, Trigger *trig, char *cmd) { value = two_arguments(cmd, arg, name); if (!*name) { - snprintf(buf2, sizeof(buf2), "set w/o an argument, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "set w/o an argument, : '%s'", cmd); trig_log(trig, buf2); return; } if (strlen(name) > kMaxTrglineLength) { - snprintf(buf2, sizeof(buf2), "eval result превышает максимальную длину триггерной строки (%zu), команда: '%s'", strlen(name), cmd); + snprintf(buf2, sizeof(buf2), "eval result (%zu), : '%s'", strlen(name), cmd); trig_log(trig, buf2); } add_var_cntx(trig->var_list, name, value, 0); @@ -4779,14 +4779,14 @@ void process_eval(void *go, Script *sc, Trigger *trig, int type, char *cmd) { expr = two_arguments(cmd, arg, name); if (!*name) { - snprintf(buf2, sizeof(buf2), "eval w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "eval w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } size_t len_expr = strlen(expr); if (len_expr > kMaxTrglineLength) { - snprintf(buf2, sizeof(buf2), "eval: expr превышает максимальную длину триггерной строки (%zu), команда: '%s'", len_expr, cmd); + snprintf(buf2, sizeof(buf2), "eval: expr (%zu), : '%s'", len_expr, cmd); trig_log(trig, buf2); } eval_expr(expr, result, sizeof(result), go, sc, trig, type); @@ -4807,13 +4807,13 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { skip_spaces(&id_p); if (!*trignum_s || atoi(trignum_s) == 0) { - snprintf(buf2, sizeof(buf2), "attach: нет или ошибка в аргументе 1: аргумент '%s', команда: '%s'", trignum_s, cmd); + snprintf(buf2, sizeof(buf2), "attach: 1: '%s', : '%s'", trignum_s, cmd); trig_log(trig, buf2); return; } if (!id_p || !*id_p || atoi(id_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "attach: нет или ошибка в аргументе 2, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "attach: 2, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -4823,7 +4823,7 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "attach: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", + "attach: 2- '%s' -- vnum, UID, . : '%s'", id_p, cmd); trig_log(trig, buf2); return; @@ -4835,14 +4835,14 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "attach: не найден аргумент 2 (кому), команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "attach: 2 (), : '%s'", cmd); trig_log(trig, buf2); return; } } } else { if (!c->IsNpc()) { - snprintf(buf2, sizeof(buf2), "attach: триггер нельзя прикрепить к игроку"); + snprintf(buf2, sizeof(buf2), "attach: "); trig_log(trig, buf2); return; } @@ -4910,13 +4910,13 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd skip_spaces(&id_p); if (!*trignum_s) { - snprintf(buf2, sizeof(buf2), "detach w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return retval; } if (!id_p || !*id_p || atoi(id_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "detach invalid id arg(1), команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach invalid id arg(1), : '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -4926,7 +4926,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "detach: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", + "detach: 2- '%s' -- vnum, UID, . : '%s'", id_p, cmd); trig_log(trig, buf2); return retval; @@ -4938,7 +4938,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "detach invalid id arg(2), команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach invalid id arg(2), : '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -4947,7 +4947,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd int tvnum = atoi(trignum_s); int trn = GetTriggerRnum(tvnum); if (trn == -1) { - snprintf(buf2, sizeof(buf2), "detach попытка удалить несуществующий триггер, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach , : '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -5008,12 +5008,12 @@ bool process_halt(Trigger *trig, char *cmd) { TrgRnum trn = GetTriggerRnum(tvn); if (tvn == 0) { - snprintf(buf2, sizeof(buf2), "halt: кривой аргумент, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "halt: , : '%s'", cmd); trig_log(trig, buf2); return false; } if (trn == -1) { - snprintf(buf2, sizeof(buf2), "halt: такой триггер не существует, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "halt: , : '%s'", cmd); trig_log(trig, buf2); return false; } @@ -5046,13 +5046,13 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int skip_spaces(&id_p); if (!*trignum_s) { - snprintf(buf2, sizeof(buf2), "run w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run w/o an arg, : '%s'", cmd); trig_log(*trig, buf2); return (false); } if (!id_p || !*id_p) { - snprintf(buf2, sizeof(buf2), "run invalid id arg(2), команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run invalid id arg(2), : '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5062,7 +5062,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "run: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", + "run: 2- '%s' -- vnum, UID, . : '%s'", id_p, cmd); trig_log(*trig, buf2); return false; @@ -5074,7 +5074,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "id not found - arg(2), команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "id not found - arg(2), : '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5082,7 +5082,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int } num = atoi(trignum_s); if (num == 0) { - snprintf(buf2, sizeof(buf2), "run invalid trignum, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run invalid trignum, : '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5115,7 +5115,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int } } if (!runtrig) { - snprintf(buf2, sizeof(buf2), "Не найден триггер, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), " , : '%s'", cmd); trig_log(*trig, buf2); return false; } @@ -5168,7 +5168,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { ObjData::shared_ptr object; object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5182,7 +5182,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { vnumum = number(15021, 15084); object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5196,7 +5196,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { vnumum = number(15021, 15084); object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5217,13 +5217,13 @@ void makeuid_var(void *go, Script *sc, Trigger *trig, int type, char *cmd) { skip_spaces(&uid_p); if (!*varname) { - snprintf(buf2, sizeof(buf2), "makeuid w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "makeuid w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!uid_p || !*uid_p || atoi(uid_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "makeuid invalid id arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "makeuid invalid id arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5236,8 +5236,8 @@ void makeuid_var(void *go, Script *sc, Trigger *trig, int type, char *cmd) { /** * Added 17/04/2000 * calculate a UID variable from the VNUM -* calcuid <переменная куда пишется id> <внум> <порядковый номер от 1 до х> -* если порядковый не указан - возвращается первое вхождение. +* calcuid < id> <> < 1 > +* - . */ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; @@ -5250,28 +5250,28 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { three_arguments(t, vnum, what, count); if (!*varname) { - snprintf(buf2, sizeof(buf2), "calcuid w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*vnum || (result = atoi(vnum)) == 0) { - snprintf(buf2, sizeof(buf2), "calcuid invalid VNUM arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid invalid VNUM arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*what) { - snprintf(buf2, sizeof(buf2), "calcuid exceed TYPE arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid exceed TYPE arg, : '%s'", cmd); trig_log(trig, buf2); return; } int count_num = 0; if (*count) { - count_num = atoi(count) - 1; //В dg индексация с 1 + count_num = atoi(count) - 1; // dg 1 if (count_num < 0) { - //Произойдет, если в dg пришел индекс 0 (ошибка) + //, dg 0 () snprintf(buf2, sizeof(buf2), "calcuid invalid count: '%s'", count); trig_log(trig, buf2); return; @@ -5287,7 +5287,7 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { uid_type = UID_OBJ; result = find_obj_by_id_vnum__calcuid(result, count_num, type, go); } else { - snprintf(buf2, sizeof(buf2), "calcuid unknown TYPE arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid unknown TYPE arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5305,9 +5305,9 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { } /* - * Поиск чаров с записью в переменную UID-а в случае онлайна человека - * Возвращает в указанную переменную UID первого PC, с именем которого - * совпадает аргумент + * UID- + * UID PC, + * */ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; @@ -5319,13 +5319,13 @@ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { three_arguments(cmd, arg, varname, who); if (!*varname) { - snprintf(buf2, sizeof(buf2), "charuid w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuid w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*who) { - snprintf(buf2, sizeof(buf2), "charuid name is missing, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuid name is missing, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5350,7 +5350,7 @@ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { add_var_cntx(trig->var_list, varname, uid, 0); } -// поиск всех чаров с лд +// void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; char who[kMaxInputLength], uid[kMaxInputLength]; @@ -5361,13 +5361,13 @@ void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { three_arguments(cmd, arg, varname, who); if (!*varname) { - snprintf(buf2, sizeof(buf2), "charuidall w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuidall w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*who) { - snprintf(buf2, sizeof(buf2), "charuidall name is missing, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuidall name is missing, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5394,7 +5394,7 @@ void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { } -// * Поиск мобов для calcuidall_var. +// * calcuidall_var. std::string ListAllMobsByVnum(MobVnum vnum) { Characters::list_t mobs; std::string str; @@ -5413,7 +5413,7 @@ std::string ListAllMobsByVnum(MobVnum vnum) { return str; } -// * Поиск предметов для calcuidall_var. +// * calcuidall_var. std::string ListAllObjsByVnum(MobVnum mvn) { std::string str; std::stringstream ss; @@ -5428,7 +5428,7 @@ std::string ListAllObjsByVnum(MobVnum mvn) { return str; } -// * Копи-паст с calcuid_var для возврата строки со всеми найденными уидами мобов/предметов (до 25ти вхождений). +// * - calcuid_var / ( 25 ). void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, char *cmd) { char arg[kMaxTrglineLength], varname[kMaxTrglineLength]; char *t, str_vnum[kMaxInputLength], what[kMaxInputLength]; @@ -5441,19 +5441,19 @@ void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, two_arguments(t, str_vnum, what); if (!*varname) { - snprintf(buf2, sizeof(buf2), "calcuidall w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*str_vnum || (vnum = atoi(str_vnum)) == 0) { - snprintf(buf2, sizeof(buf2), "calcuidall invalid VNUM arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall invalid VNUM arg, : '%s'", cmd); trig_log(trig, buf2); return; } if (!*what) { - snprintf(buf2, sizeof(buf2), "calcuidall exceed TYPE arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall exceed TYPE arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5463,7 +5463,7 @@ void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, } else if (!str_cmp(what, "obj")) { result = ListAllObjsByVnum(vnum); } else { - snprintf(buf2, sizeof(buf2), "calcuidall unknown TYPE arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall unknown TYPE arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5486,7 +5486,7 @@ int process_return(Trigger *trig, char *cmd) { two_arguments(cmd, arg1, arg2); if (!*arg2) { - snprintf(buf2, sizeof(buf2), "return w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "return w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return 1; } @@ -5501,14 +5501,14 @@ void ClearContextVar(Trigger *trig,char *cmd) { var = any_one_arg(cmd, arg); if (!*var) { - snprintf(buf2, sizeof(buf2), "clearcontext w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "clearcontext w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } id = atoi(var); if (id == 0) { - snprintf(buf2, sizeof(buf2), "clearcontext попытка удалить в 0 контексте, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "clearcontext 0 , : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5527,7 +5527,7 @@ void process_unset(Script *sc, Trigger *trig, char *cmd) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "unset w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "unset w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5558,7 +5558,7 @@ void process_remote(Script *sc, Trigger *trig, char *cmd) { skip_spaces(&uid_p); if (!*buf || !*buf2) { - snprintf(buf2, sizeof(buf2), "remote: invalid arguments, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "remote: invalid arguments, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5588,13 +5588,13 @@ void process_remote(Script *sc, Trigger *trig, char *cmd) { // for all but PC's, context comes from the existing context. // for PC's, context is 0 (global) // context = vd->context; -// Контекст можно брать как vd->context или sc->context -// Если брать vd->context, то теряем контекст при переносе локальной переменной -// Если брать sc->context, то по-моему получится правильней, а именно: -// Для локальной переменной контекст значения не играет, т.о. -// если есть желание перенести локальную переменную заранее установите -// контекст в 0. Для глобальной переменной переменная с контекстом 0 -// "покрывает" отсутствующие контексты +// vd->context sc->context +// vd->context, +// sc->context, - , : +// , .. +// +// 0. 0 +// "" context = trig->context; if ((room = get_room(buf2))) { @@ -5697,7 +5697,7 @@ void process_rdelete(Script * /*sc*/, Trigger *trig, char *cmd) { skip_spaces(&uid_p); if (!*buf || !*buf2) { - snprintf(buf2, sizeof(buf2), "rdelete: invalid arguments, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "rdelete: invalid arguments, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5743,7 +5743,7 @@ void process_global(Script *sc, Trigger *trig, char *cmd, long id) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "global w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "global w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5769,7 +5769,7 @@ void process_worlds(Script * /*sc*/, Trigger *trig, char *cmd, long id) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "worlds w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "worlds w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5795,7 +5795,7 @@ void process_context(Script * /*sc*/, Trigger *trig, char *cmd) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "context w/o an arg, команда: '%s'", cmd); + snprintf(buf2, sizeof(buf2), "context w/o an arg, : '%s'", cmd); trig_log(trig, buf2); return; } @@ -5833,7 +5833,7 @@ void extract_value(Script * /*sc*/, Trigger *trig, char *cmd) { int timed_script_driver(void *go, Trigger *trig, int type, int mode); int script_driver(void *go, Trigger *trig, int type, int mode) { - int timewarning = 50; // в текущий момент миллисекунды + int timewarning = 50; // int return_code; CharacterLinkDrop = false; @@ -5916,20 +5916,20 @@ int timed_script_driver(void *go, Trigger *trig, int type, int mode) { break; } if (trig->get_attach_type() != type) { - log("SCRIPTDRIVER: типы триггеров отличаются, триггер %d ", trig_index[trig->get_rnum()]->vnum); + log("SCRIPTDRIVER: , %d ", trig_index[trig->get_rnum()]->vnum); debug::backtrace(runtime_config.logs(SYSLOG).handle()); } if (!sc) { - log("SCRIPTDRIVER: SC отсутсвует триггер %d ", trig_index[trig->get_rnum()]->vnum); + log("SCRIPTDRIVER: SC %d ", trig_index[trig->get_rnum()]->vnum); return ret_val; } if (sc->is_purged()) { return ret_val; } - // issue #3523: моб выведен из игрового мира (extract_char выставил - // in_room == kNowhere) -- триггер на нём не выполняем. Признак надёжнее - // устаревшего флага purged (оставлен лишь для совместимости). + // issue #3523: (extract_char + // in_room == kNowhere) -- Σ . ģ + // purged ( ). if (type == MOB_TRIGGER && static_cast(go)->in_room == kNowhere) { return ret_val; } @@ -6186,9 +6186,9 @@ int timed_script_driver(void *go, Trigger *trig, int type, int mode) { break; } } - // issue #3523: если команда повесила на триггер wait (моб в стане), - // выходим из script_driver -- событие позже повторит ту же строку - // (TRIG_FROM_LINE). Без этого остаток триггера отработал бы вхолостую. + // issue #3523: wait ( ), + // script_driver -- + // (TRIG_FROM_LINE). . if (trig && GET_TRIG_WAIT(trig).time_remaining > 0) { depth--; cur_trig = prev_trig; @@ -6236,12 +6236,12 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { two_arguments(argument, buf, buf2); if (!*buf) { - SendMsgToChar("Usage: tlist <начальный номер или зона> [<конечный номер>]\r\n", ch); + SendMsgToChar("Usage: tlist < > [< >]\r\n", ch); return; } - // Один аргумент -- номер зоны: листаем все её триггеры (vnum / 100 == зона). - // Два аргумента -- диапазон vnum [first, last]. + // -- : ţ (vnum / 100 == ). + // -- vnum [first, last]. first = atoi(buf); if (*buf2) { last = atoi(buf2); @@ -6251,30 +6251,30 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if ((first < 0) || (first > kMaxProtoNumber) || (last < 0) || (last > kMaxProtoNumber)) { - snprintf(buf, sizeof(buf), "Значения должны быть между 0 и %d.\n\r", kMaxProtoNumber); + snprintf(buf, sizeof(buf), " 0 %d.\n\r", kMaxProtoNumber); SendMsgToChar(buf, ch); return; } if (first > last) { - SendMsgToChar("Второе значение должно быть больше первого.\n\r", ch); + SendMsgToChar(" .\n\r", ch); return; } if (first + 200 < last) { - SendMsgToChar("Максимальный показываемый промежуток - 200.\n\r", ch); + SendMsgToChar(" - 200.\n\r", ch); return; } - // Билдеру доступна только его зона (сравниваем по зоне vnum, а не по сырому аргументу). + // ( vnum, ). if (!privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false) && (GET_OLC_ZONE(ch) != first / 100 || GET_OLC_ZONE(ch) != last / 100)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } - // trig_index отсортирован по vnum -- бинарным поиском находим первый триггер - // с vnum >= first, а не сканируем все >16k триггеров последовательно. + // trig_index vnum -- + // vnum >= first, >16k . const auto start = std::lower_bound(trig_index, trig_index + top_of_trigt, first, [](const IndexData *trig, int vnum) { return trig->vnum < vnum; }); nr = start - trig_index; @@ -6300,7 +6300,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { out += "[WLD] "; out += trgtypes; } - out += "\r\nПрикреплен к: "; + out += "\r\n : "; if (!owner_trig[trig_index[nr]->vnum].empty()) { for (auto it = owner_trig[trig_index[nr]->vnum].begin(); it != owner_trig[trig_index[nr]->vnum].end(); ++it) { @@ -6311,7 +6311,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { out_tmp += buf; } if (it->first != -1) { - out += "attach из " + std::to_string(it->first) + " к: "; + out += "attach " + std::to_string(it->first) + " : "; } out += out_tmp;// + "]"; } @@ -6323,7 +6323,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if (!found) { - SendMsgToChar("В этом промежутке триггеров нет.\n\r", ch); + SendMsgToChar(" .\n\r", ch); } else { page_string(ch->desc, pagebuf, true); } @@ -6342,7 +6342,7 @@ void do_tstat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } auto first = atoi(str); if (!(privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false)) && (GET_OLC_ZONE(ch) != first / 100)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } if (*str) { @@ -6517,8 +6517,8 @@ TriggersList::~TriggersList() { bool TriggersList::add(Trigger *trigger, const bool to_front /*= false*/) { for (const auto &i : m_list) { if (trigger->get_rnum() == i->get_rnum() && !trigger->is_runned) { - //Мы не можем здесь заменить имеющийся триггер на новый - //т.к. имеющийся триггер может уже использоваться или быть в ожидание (wait command) + // + //.. (wait command) return false; } } diff --git a/src/engine/ui/cmd/do_statistic.cpp b/src/engine/ui/cmd/do_statistic.cpp index 4f7e817f2..a29ecb03e 100644 --- a/src/engine/ui/cmd/do_statistic.cpp +++ b/src/engine/ui/cmd/do_statistic.cpp @@ -53,11 +53,11 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ ++total; } /* - * Тут нужно использовать форматирование таблицы + * * \todo table format */ std::ostringstream out; - out << kColorBoldCyn << " Статистика по персонажам в игре (всего / 25 ур. и выше / ниже 25 ур.):" + out << kColorBoldCyn << " ( / 25 . / 25 .):" << kColorNrm << "\r\n" << "\r\n" << " "; int count{1}; const int columns{2}; @@ -83,29 +83,29 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ const int headline_width{33}; - out << fmt::format("{:<{}}", " Всего игроков:", headline_width); + out << fmt::format("{:<{}}", " :", headline_width); PrintValue(out, number_col_width, total); - out << fmt::format("{:<{}}", " Игроков выше|ниже 25 уровня:", headline_width); + out << fmt::format("{:<{}}", " | 25 :", headline_width); PrintPair(out, number_col_width, hilvl, lowlvl); - out << fmt::format("{:<{}}", " Игроков с перевоплощениями|без:", headline_width); + out << fmt::format("{:<{}}", " |:", headline_width); PrintPair(out, number_col_width, rem, norem); - out << fmt::format("{:<{}}", " Клановых|внеклановых игроков:", headline_width); + out << fmt::format("{:<{}}", " | :", headline_width); PrintPair(out, number_col_width, clan, noclan); - out << fmt::format("{:<{}}", " Игроков с флагами ПК|без ПК:", headline_width); + out << fmt::format("{:<{}}", " | :", headline_width); PrintPair(out, number_col_width, pk, nopk); - out << fmt::format("{:<{}}", " Героев (без ПК) | Тварей убито:", headline_width); + out << fmt::format("{:<{}}", " ( ) | :", headline_width); const int kills_col_width{5}; PrintPair(out, kills_col_width, char_stat::players_killed, char_stat::mobs_killed); out << "\r\n"; char_stat::PrintClassesExpStat(out); - out << " Времени с перезагрузки: "; + out << " : "; PrintUptime(out); SendMsgToChar(out.str(), ch); diff --git a/src/engine/ui/cmd/do_toggle.cpp b/src/engine/ui/cmd/do_toggle.cpp index 2bfaed51d..f326c98a6 100644 --- a/src/engine/ui/cmd/do_toggle.cpp +++ b/src/engine/ui/cmd/do_toggle.cpp @@ -13,19 +13,19 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; if (GET_WIMP_LEV(ch) == 0) - strcpy(buf2, "нет"); + strcpy(buf2, ""); else sprintf(buf2, "%-3d", GET_WIMP_LEV(ch)); if (GetRealLevel(ch) >= kLvlImmortal || ch->IsFlagged(EPrf::kCoderinfo)) { snprintf(buf, kMaxStringLength, - " Нет агров : %-3s " - " Супервидение : %-3s " - " Флаги комнат : %-3s \r\n" - " Частный режим : %-3s " - " Замедление : %-3s " - " Кодер : %-3s \r\n" - " Опечатки : %-3s \r\n", + " : %-3s " + " : %-3s " + " : %-3s \r\n" + " : %-3s " + " : %-3s " + " : %-3s \r\n" + " : %-3s \r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kNohassle)), BoolToOnOffStr(ch->IsFlagged(EPrf::kHolylight)), BoolToOnOffStr(ch->IsFlagged(EPrf::kRoomFlags)), @@ -37,47 +37,47 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { } snprintf(buf, kMaxStringLength, - " Автовыходы : %-3s " - " Краткий режим : %-3s " - " Сжатый режим : %-3s \r\n" - " Повтор команд : %-3s " - " Обращения : %-3s " - " Кто-то : %s \r\n" - " Болтать : %-3s " - " Орать : %-3s \r\n" - " Аукцион : %-3s " - " Базар : %-3s " - " Автозаучивание: %-3s \r\n" - " Призыв : %-3s " - " Автозавершение: %-3s " - " Группа (вид) : %s \r\n" - " Без двойников : %-3s " - " Автопомощь : %-3s " - " Автодележ : %-3s \r\n" - " Автограбеж : %-7s " - " Брать куны : %-3s " - " Арена : %-3s \r\n" - " Трусость : %-3s " - " Ширина экрана : %-3d " - " Высота экрана : %-3d \r\n" - " Сжатие : %s " - " Новости (вид) : %-5s " - " Доски : %-3s \r\n" - " Хранилище : %-8s" - " Пклист : %-3s " - " Политика : %-3s \r\n" - " Пкформат : %s " - " Соклановцы : %-8s" - " Оффтоп : %-3s \r\n" - " Потеря связи : %-3s " - " Ингредиенты : %-3s " - " Вспомнить : %-3u \r\n", + " : %-3s " + " : %-3s " + " : %-3s \r\n" + " : %-3s " + " : %-3s " + " - : %s \r\n" + " : %-3s " + " : %-3s \r\n" + " : %-3s " + " : %-3s " + " : %-3s \r\n" + " : %-3s " + " : %-3s " + " () : %s \r\n" + " : %-3s " + " : %-3s " + " : %-3s \r\n" + " : %-7s " + " : %-3s " + " : %-3s \r\n" + " : %-3s " + " : %-3d " + " : %-3d \r\n" + " : %s " + " () : %-5s " + " : %-3s \r\n" + " : %-8s" + " : %-3s " + " : %-3s \r\n" + " : %s " + " : %-8s" + " : %-3s \r\n" + " : %-3s " + " : %-3s " + " : %-3u \r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kAutoexit)), BoolToOnOffStr(ch->IsFlagged(EPrf::kBrief)), BoolToOnOffStr(ch->IsFlagged(EPrf::kCompact)), (ch->IsFlagged(EPrf::kNoRepeat) ? "NO" : "YES"), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoTell)), - native_text::pad_right(ch->IsFlagged(EPrf::kNoInvistell) ? "нельзя" : "можно", 6).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kNoInvistell) ? "" : "", 6).c_str(), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoGossip)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoHoller)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoAuction)), @@ -85,7 +85,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { BoolToOnOffStr(ch->IsFlagged(EPrf::kAutomem)), BoolToOnOffStr(ch->IsFlagged(EPrf::KSummonable)), BoolToOnOffStr(ch->IsFlagged(EPrf::kGoAhead)), - native_text::pad_right(ch->IsFlagged(EPrf::kShowGroup) ? "полный" : "краткий", 7).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kShowGroup) ? "" : "", 7).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kNoClones)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutoassist)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutosplit)), @@ -96,16 +96,16 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { (ch)->player_specials->saved.stringLength, (ch)->player_specials->saved.stringWidth, #if defined(HAVE_ZLIB) - native_text::pad_right(ch->desc->deflate == nullptr ? "нет" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), 6).c_str(), + native_text::pad_right(ch->desc->deflate == nullptr ? "" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), 6).c_str(), #else "N/A", #endif - ch->IsFlagged(EPrf::kNewsMode) ? "доска" : "лента", + ch->IsFlagged(EPrf::kNewsMode) ? "" : "", BoolToOnOffStr(ch->IsFlagged(EPrf::kBoardMode)), GetChestMode(ch).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kPklMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kPolitMode)), - native_text::pad_right(ch->IsFlagged(EPrf::kPkFormatMode) ? "краткий" : "полный", 6).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kPkFormatMode) ? "" : "", 6).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kClanmembersMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kOfftopMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAntiDcMode)), @@ -113,27 +113,27 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { ch->remember_get_num()); SendMsgToChar(buf, ch); if ((ch)->player_specials->saved.ntfyExchangePrice > 0) { - sprintf(buf, " Уведомления : %-7ld ", (ch)->player_specials->saved.ntfyExchangePrice); + sprintf(buf, " : %-7ld ", (ch)->player_specials->saved.ntfyExchangePrice); } else { - sprintf(buf, " Уведомления : %s ", native_text::pad_right("Нет", 7).c_str()); + sprintf(buf, " : %s ", native_text::pad_right("", 7).c_str()); } SendMsgToChar(buf, ch); snprintf(buf, kMaxStringLength, - " Карта : %-3s " - " Вход в зону : %-3s \r\n" - " Магщиты (вид) : %s" - " Автопризыв : %-5s " - " Маппер : %-3s \r\n" - " Контроль IP : %-6s ", + " : %-3s " + " : %-3s \r\n" + " () : %s" + " : %-5s " + " : %-3s \r\n" + " IP : %-6s ", BoolToOnOffStr(ch->IsFlagged(EPrf::kDrawMap)), BoolToOnOffStr(ch->IsFlagged(EPrf::kShowZoneNameOnEnter)), - native_text::pad_right(ch->IsFlagged(EPrf::kBriefShields) ? "краткий" : "полный", 8).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kBriefShields) ? "" : "", 8).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutonosummon)), BoolToOnOffStr(ch->IsFlagged(EPrf::kMapper)), BoolToOnOffStr(ch->IsFlagged(EPrf::kIpControl))); SendMsgToChar(buf, ch); if (GET_GOD_FLAG(ch, EGf::kAllowTesterMode)) - sprintf(buf, " Тестер : %-3s\r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kTester))); + sprintf(buf, " : %-3s\r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kTester))); else sprintf(buf, "\r\n"); SendMsgToChar(buf, ch); diff --git a/src/engine/ui/cmd_god/do_print_armor.cpp b/src/engine/ui/cmd_god/do_print_armor.cpp index 9fa366274..5dd57ab39 100644 --- a/src/engine/ui/cmd_god/do_print_armor.cpp +++ b/src/engine/ui/cmd_god/do_print_armor.cpp @@ -22,14 +22,14 @@ struct FilterType { EWearFlag wear; int wear_message; int material; - std::vector affect; // аффекты weap - std::vector affect2; // аффекты apply - std::vector affect3; // экстрафлаг + std::vector affect; // weap + std::vector affect2; // apply + std::vector affect3; // }; void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || (!privilege::IsGrGod(ch) && !ch->IsFlagged(EPrf::kCoderinfo))) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } @@ -39,84 +39,84 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { while (*argument) { switch (native_text::first_char_code(argument)) { case rus::kEmUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "булат")) { + if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kBulat; - } else if (utils::IsAbbr(tmpbuf, "бронза")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kBronze; - } else if (utils::IsAbbr(tmpbuf, "железо")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kIron; - } else if (utils::IsAbbr(tmpbuf, "сталь")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kSteel; - } else if (utils::IsAbbr(tmpbuf, "кованая.сталь")) { + } else if (utils::IsAbbr(tmpbuf, ".")) { filter.material = EObjMaterial::kForgedSteel; - } else if (utils::IsAbbr(tmpbuf, "драг.металл")) { + } else if (utils::IsAbbr(tmpbuf, ".")) { filter.material = EObjMaterial::kPreciousMetel; - } else if (utils::IsAbbr(tmpbuf, "кристалл")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kCrystal; - } else if (utils::IsAbbr(tmpbuf, "дерево")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kWood; - } else if (utils::IsAbbr(tmpbuf, "прочное.дерево")) { + } else if (utils::IsAbbr(tmpbuf, ".")) { filter.material = EObjMaterial::kHardWood; - } else if (utils::IsAbbr(tmpbuf, "керамика")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kCeramic; - } else if (utils::IsAbbr(tmpbuf, "стекло")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kGlass; - } else if (utils::IsAbbr(tmpbuf, "камень")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kStone; - } else if (utils::IsAbbr(tmpbuf, "кость")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kBone; - } else if (utils::IsAbbr(tmpbuf, "ткань")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kCloth; - } else if (utils::IsAbbr(tmpbuf, "кожа")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kSkin; - } else if (utils::IsAbbr(tmpbuf, "органика")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kOrganic; - } else if (utils::IsAbbr(tmpbuf, "береста")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.material = EObjMaterial::kPaper; - } else if (utils::IsAbbr(tmpbuf, "драг.камень")) { + } else if (utils::IsAbbr(tmpbuf, ".")) { filter.material = EObjMaterial::kDiamond; } else { - SendMsgToChar("Неверный материал предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } find_param = true; break; case rus::kTeUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "броня") || utils::IsAbbr(tmpbuf, "armor")) { + if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "armor")) { filter.type = EObjType::kArmor; - } else if (utils::IsAbbr(tmpbuf, "легкие") || utils::IsAbbr(tmpbuf, "легкая")) { + } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { filter.type = EObjType::kLightArmor; - } else if (utils::IsAbbr(tmpbuf, "средние") || utils::IsAbbr(tmpbuf, "средняя")) { + } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { filter.type = EObjType::kMediumArmor; - } else if (utils::IsAbbr(tmpbuf, "тяжелые") || utils::IsAbbr(tmpbuf, "тяжелая")) { + } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { filter.type = EObjType::kHeavyArmor; } else { - SendMsgToChar("Неверный тип предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } find_param = true; break; case rus::kOUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "тело")) { + if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kBody; filter.wear_message = 3; - } else if (utils::IsAbbr(tmpbuf, "голова")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kHead; filter.wear_message = 4; - } else if (utils::IsAbbr(tmpbuf, "ноги")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kLegs; filter.wear_message = 5; - } else if (utils::IsAbbr(tmpbuf, "ступни")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kFeet; filter.wear_message = 6; - } else if (utils::IsAbbr(tmpbuf, "кисти")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kHands; filter.wear_message = 7; - } else if (utils::IsAbbr(tmpbuf, "руки")) { + } else if (utils::IsAbbr(tmpbuf, "")) { filter.wear = EWearFlag::kArms; filter.wear_message = 8; } else { - SendMsgToChar("Неверное место одевания предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } find_param = true; @@ -125,18 +125,18 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { bool tmp_find = false; argument = one_argument(++argument, tmpbuf); if (!strlen(tmpbuf)) { - SendMsgToChar("Неверный аффект предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (filter.affect.size() + filter.affect2.size() + filter.affect3.size() >= 3) { break; } switch (*tmpbuf) { - case '1': sprintf(tmpbuf, "можно вплавить 1 камень"); + case '1': sprintf(tmpbuf, " 1 "); break; - case '2': sprintf(tmpbuf, "можно вплавить 2 камня"); + case '2': sprintf(tmpbuf, " 2 "); break; - case '3': sprintf(tmpbuf, "можно вплавить 3 камня"); + case '3': sprintf(tmpbuf, " 3 "); break; default: break; } @@ -145,7 +145,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int num = 0; for (int flag = 0; flag < 4; ++flag) { - for (/* тут ничего не надо */; *equipment_affects[num] != '\n'; ++num) { + for (/* */; *equipment_affects[num] != '\n'; ++num) { if (strlen(equipment_affects[num]) < len) continue; if (!strncmp(equipment_affects[num], tmpbuf, len)) { @@ -170,11 +170,11 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - // поиск по экстрафлагу + // if (!tmp_find) { num = 0; for (int flag = 0; flag < 4; ++flag) { - for (/* тут ничего не надо */; *extra_bits[num] != '\n'; ++num) { + for (/* */; *extra_bits[num] != '\n'; ++num) { if (strlen(extra_bits[num]) < len) continue; if (!strncmp(extra_bits[num], tmpbuf, len)) { @@ -190,7 +190,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } if (!tmp_find) { - sprintf(buf, "Неверный аффект предмета: '%s'.\r\n", tmpbuf); + sprintf(buf, " : '%s'.\r\n", tmpbuf); SendMsgToChar(buf, ch); return; } @@ -201,12 +201,12 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } if (!find_param) { - SendMsgToChar("Формат команды:\r\n" - " armor Т[броня|легкие|средние|тяжелые] О[тело|голова|ногиступни|кисти|руки] А[аффект] М[материал]\r\n", + SendMsgToChar(" :\r\n" + " armor [|||] [||||] [] []\r\n", ch); return; } - std::string buffer = "Выборка по следующим параметрам: "; + std::string buffer = " : "; if (filter.material >= 0) { buffer += material_name[filter.material]; buffer += " "; @@ -237,25 +237,25 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { buffer += " "; } } - buffer += "\r\nСредний уровень мобов в зоне | внум предмета | материал | имя предмета + аффекты если есть\r\n"; + buffer += "\r\n | | | + \r\n"; SendMsgToChar(buffer, ch); std::multimap tmp_list; for (const auto &i : obj_proto) { - // материал + // if (filter.material >= 0 && filter.material != i->get_material()) { continue; } - // тип + // if (filter.type >= 0 && filter.type != i->get_type()) { continue; } - // куда можно одеть + // if (filter.wear != EWearFlag::kUndefined && !i->has_wear_flag(filter.wear)) { continue; } - // аффекты + // bool find = true; if (!filter.affect.empty()) { for (int it : filter.affect) { @@ -264,7 +264,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - // аффект не найден, продолжать смысла нет + // , if (!find) { continue; } @@ -280,7 +280,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - // доп.свойство не найдено, продолжать смысла нет + // . , if (!find) { continue; } @@ -293,7 +293,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - // экстрафлаг не найден, продолжать смысла нет + // , if (!find) { continue; } @@ -334,15 +334,15 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { snprintf(buf, kMaxStringLength, " %s%s%s%s%s%d%s\r\n", kColorCyn, buf2, kColorNrm, kColorCyn, - negative ? " ухудшает на " : " улучшает на ", abs(drsdice), kColorNrm); + negative ? " " : " ", abs(drsdice), kColorNrm); out << " | | | " << buf; } } if (!out.str().empty()) { - SendMsgToChar(ch, "Всего найдено предметов: %zu\r\n\r\n", tmp_list.size()); + SendMsgToChar(ch, " : %zu\r\n\r\n", tmp_list.size()); page_string(ch->desc, out.str()); } else { - SendMsgToChar("Ничего не найдено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } diff --git a/src/engine/ui/cmd_god/do_stat.cpp b/src/engine/ui/cmd_god/do_stat.cpp index da239904f..03ec538a6 100644 --- a/src/engine/ui/cmd_god/do_stat.cpp +++ b/src/engine/ui/cmd_god/do_stat.cpp @@ -54,18 +54,18 @@ std::string print_special(CharData *mob) { // issue.specials: identity via the registry; a mob may carry several specials (comma-joined). std::string out; for (const auto s : specials::MobSpecials(GET_MOB_VNUM(mob))) { - const char *name = "глюк"; + const char *name = ""; switch (s) { - case specials::ESpecial::kShop: name = "торговец"; break; - case specials::ESpecial::kRent: name = "рентер"; break; - case specials::ESpecial::kMail: name = "почтальон"; break; - case specials::ESpecial::kBank: name = "банкир"; break; - case specials::ESpecial::kExchange: name = "зазывала"; break; - case specials::ESpecial::kHorse: name = "конюх"; break; - case specials::ESpecial::kGuild: name = "учитель"; break; - case specials::ESpecial::kTorc: name = "меняла"; break; - case specials::ESpecial::kOutfit: name = "нубхелпер"; break; - case specials::ESpecial::kMercenary: name = "ватажник"; break; + case specials::ESpecial::kShop: name = ""; break; + case specials::ESpecial::kRent: name = ""; break; + case specials::ESpecial::kMail: name = ""; break; + case specials::ESpecial::kBank: name = ""; break; + case specials::ESpecial::kExchange: name = ""; break; + case specials::ESpecial::kHorse: name = ""; break; + case specials::ESpecial::kGuild: name = ""; break; + case specials::ESpecial::kTorc: name = ""; break; + case specials::ESpecial::kOutfit: name = ""; break; + case specials::ESpecial::kMercenary: name = ""; break; default: break; } if (!out.empty()) { @@ -73,25 +73,25 @@ std::string print_special(CharData *mob) { } out += name; } - return out.empty() ? "нет" : out; + return out.empty() ? "" : out; } void do_statip(CharData *ch, CharData *k) { log("Start logon list stat"); - // Отображаем список ip-адресов с которых персонаж входил + // ip- if (!LOGON_LIST(k).empty()) { - // update: логон-лист может быть капитально большим, поэтому пишем это в свой дин.буфер, а не в buf2 - // заодно будет постраничный вывод ип, чтобы имма не посылало на йух с **OVERFLOW** - std::ostringstream out("Персонаж заходил с IP-адресов:\r\n"); + // update: - , ., buf2 + // , **OVERFLOW** + std::ostringstream out(" IP-:\r\n"); for (const auto &logon : LOGON_LIST(k)) { snprintf(buf1, sizeof(buf1), "%16s %5ld %20s%s\r\n", logon.ip.c_str(), logon.count, rustime(localtime(&logon.lasttime)), - logon.is_first ? " (создание)" : ""); + logon.is_first ? " ()" : ""); out << buf1; } @@ -103,10 +103,10 @@ void do_statip(CharData *ch, CharData *k) { void DoStatKarma(CharData *ch, CharData *victim) { std::stringstream ss; if (!KARMA(victim)) { - SendMsgToChar(ch, "Карма у игрока %s отсутствует.\r\n", GET_NAME(victim)); + SendMsgToChar(ch, " %s .\r\n", GET_NAME(victim)); return; } - ss << "Карма игрока " << GET_NAME(victim) << ":\r\n" << KARMA(victim); + ss << " " << GET_NAME(victim) << ":\r\n" << KARMA(victim); SendMsgToChar(ss.str(), ch); } @@ -126,7 +126,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sline = fmt::sprintf("%s %s ", utils::sprintGender(to_underlying(k->get_sex())).c_str(), smallBuf); } snprintf(buf2, sizeof(buf2), - "%s '%s' В комнате [%d] Текущий UID:[%ld]", + "%s '%s' [%d] UID:[%ld]", (!k->IsNpc() ? "PC" : "MOB"), GET_NAME(k), k_room, @@ -134,10 +134,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sline += buf2; SendMsgToChar(sline, ch); } - SendMsgToChar(ch, " ЛАГ: [%d]\r\n", k->get_wait()); + SendMsgToChar(ch, " : [%d]\r\n", k->get_wait()); if (k->IsNpc()) { snprintf(buf, sizeof(buf), - "Синонимы: &S%s&s, VNum: [%5d], RNum: [%5d]\r\n", + ": &S%s&s, VNum: [%5d], RNum: [%5d]\r\n", k->GetCharAliases().c_str(), GET_MOB_VNUM(k), k->get_rnum()); @@ -145,7 +145,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } snprintf(buf, sizeof(buf), - "Падежи: %s/%s/%s/%s/%s/%s ", + ": %s/%s/%s/%s/%s/%s ", GET_PAD(k, 0), GET_PAD(k, 1), GET_PAD(k, 2), @@ -157,17 +157,17 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!k->IsNpc()) { if (!(k)->player_specials->saved.NameGod) { - snprintf(buf, sizeof(buf), "Имя никем не одобрено!\r\n"); + snprintf(buf, sizeof(buf), " !\r\n"); SendMsgToChar(buf, ch); } else if ((k)->player_specials->saved.NameGod < 1000) { - snprintf(buf, sizeof(buf), "Имя запрещено! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); + snprintf(buf, sizeof(buf), " ! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); SendMsgToChar(buf, ch); } else { - snprintf(buf, sizeof(buf), "Имя одобрено! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); + snprintf(buf, sizeof(buf), " ! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); SendMsgToChar(buf, ch); } - snprintf(buf, sizeof(buf), "Вероисповедание: %s\r\n", religion_name[(int) GET_RELIGION(k)][(int) k->get_sex()]); + snprintf(buf, sizeof(buf), ": %s\r\n", religion_name[(int) GET_RELIGION(k)][(int) k->get_sex()]); SendMsgToChar(buf, ch); std::string file_name = GET_NAME(k); @@ -180,70 +180,70 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { SendMsgToChar(ch, "Registered by email from %s\r\n", text.c_str()); if (k->player_specials->saved.telegram_id != 0) - SendMsgToChar(ch, "Подключен Телеграм, chat_id: %lu\r\n", k->player_specials->saved.telegram_id); + SendMsgToChar(ch, " , chat_id: %lu\r\n", k->player_specials->saved.telegram_id); if (k->IsFlagged(EPlrFlag::kFrozen) && punishments::Get(k, punishments::EType::kFreeze).duration) { - snprintf(buf, sizeof(buf), "Заморожен : %ld час [%s].\r\n", + snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kFreeze).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kFreeze).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kFreeze).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kHelled) && punishments::Get(k, punishments::EType::kHell).duration) { - snprintf(buf, sizeof(buf), "Находится в темнице : %ld час [%s].\r\n", + snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kHell).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kHell).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kHell).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kNameDenied) && punishments::Get(k, punishments::EType::kName).duration) { - snprintf(buf, sizeof(buf), "Находится в комнате имени : %ld час.\r\n", + snprintf(buf, sizeof(buf), " : %ld .\r\n", static_cast((punishments::Get(k, punishments::EType::kName).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kMuted) && punishments::Get(k, punishments::EType::kMute).duration) { - snprintf(buf, sizeof(buf), "Будет молчать : %ld час [%s].\r\n", + snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kMute).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kMute).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kMute).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kDumbed) && punishments::Get(k, punishments::EType::kDumb).duration) { - snprintf(buf, sizeof(buf), "Будет нем : %ld мин [%s].\r\n", + snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kDumb).duration - time(nullptr)) / 60), punishments::Get(k, punishments::EType::kDumb).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kDumb).reason.c_str()); SendMsgToChar(buf, ch); } if (!k->IsFlagged(EPlrFlag::kRegistred) && punishments::Get(k, punishments::EType::kUnreg).duration) { - snprintf(buf, sizeof(buf), "Не будет зарегистрирован : %ld час [%s].\r\n", + snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kUnreg).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kUnreg).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kUnreg).reason.c_str()); SendMsgToChar(buf, ch); } if (GET_GOD_FLAG(k, EGf::kGodsLike) && punishments::Get(k, punishments::EType::kGcurse).duration) { - snprintf(buf, sizeof(buf), "Под защитой Богов : %ld час.\r\n", + snprintf(buf, sizeof(buf), " : %ld .\r\n", static_cast((punishments::Get(k, punishments::EType::kGcurse).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } if (GET_GOD_FLAG(k, EGf::kGodscurse) && punishments::Get(k, punishments::EType::kGcurse).duration) { - snprintf(buf, sizeof(buf), "Проклят Богами : %ld час.\r\n", + snprintf(buf, sizeof(buf), " : %ld .\r\n", static_cast((punishments::Get(k, punishments::EType::kGcurse).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } } const auto &title = k->GetTitleStr(); - snprintf(buf, sizeof(buf), "Титул: %s\r\n", (title.empty() ? "<Нет>" : title.c_str())); + snprintf(buf, sizeof(buf), ": %s\r\n", (title.empty() ? "<>" : title.c_str())); SendMsgToChar(buf, ch); if (k->IsNpc()) - snprintf(buf, sizeof(buf), "L-Des: %s", (!k->player_data.long_descr.empty() ? k->player_data.long_descr.c_str() : "<Нет>\r\n")); + snprintf(buf, sizeof(buf), "L-Des: %s", (!k->player_data.long_descr.empty() ? k->player_data.long_descr.c_str() : "<>\r\n")); else snprintf(buf, sizeof(buf), "L-Des: %s", - (!k->player_data.description.empty() ? k->player_data.description.c_str() : "<Нет>\r\n")); + (!k->player_data.description.empty() ? k->player_data.description.c_str() : "<>\r\n")); SendMsgToChar(buf, ch); if (!k->IsNpc()) { snprintf(smallBuf, sizeof(smallBuf), "%s", MUD::Class(k->GetClass()).GetCName()); - snprintf(buf, sizeof(buf), "Род: %s, Профессия: %s", + snprintf(buf, sizeof(buf), ": %s, : %s", MUD::RaceMessages().GetMessage(GET_RACE(k), k->get_sex()).c_str(), smallBuf); SendMsgToChar(buf, ch); @@ -252,20 +252,20 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->get_role_bits().any()) { print_bitset(k->get_role_bits(), npc_role_types, ",", str); } else { - str += "нет"; + str += ""; } - SendMsgToChar(ch, "Роли NPC: %s%s%s", kColorCyn, str.c_str(), kColorNrm); + SendMsgToChar(ch, " NPC: %s%s%s", kColorCyn, str.c_str(), kColorNrm); } char tmp_buf[256]; if (experience::GetZoneGroup(k) > 1) { - snprintf(tmp_buf, sizeof(tmp_buf), " : групповой %ldx%d", + snprintf(tmp_buf, sizeof(tmp_buf), " : %ldx%d", k->get_exp() / experience::GetZoneGroup(k), experience::GetZoneGroup(k)); } else { tmp_buf[0] = '\0'; } - snprintf(buf, sizeof(buf), ", Уровень: [%s%2d%s], Опыт: [%s%10ld%s]%s, Наклонности: [%4d]\r\n", + snprintf(buf, sizeof(buf), ", : [%s%2d%s], : [%s%10ld%s]%s, : [%4d]\r\n", kColorYel, GetRealLevel(k), kColorNrm, kColorYel, k->get_exp(), kColorNrm, tmp_buf, alignment::GetAlignment(k)); @@ -273,10 +273,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!k->IsNpc()) { if (CLAN(k)) { - SendMsgToChar(ch, "Статус дружины: %s\r\n", GET_CLAN_STATUS(k)); + SendMsgToChar(ch, " : %s\r\n", GET_CLAN_STATUS(k)); } - //added by WorM когда статишь файл собсно показывалось текущее время а не время последнего входа + //added by WorM time_t ltime = GetLastlogonByUnique(k->get_uid()); char t1[11]; char t2[11]; @@ -285,14 +285,14 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { t1[10] = t2[10] = '\0'; snprintf(buf, sizeof(buf), - "Создан: [%s] Последний вход: [%s] Играл: [%dh %dm] Возраст: [%d]\r\n", + ": [%s] : [%s] : [%dh %dm] : [%d]\r\n", t1, t2, k->player_data.time.played / 3600, ((k->player_data.time.played % 3600) / 60), CalcCharAge(k)->year); SendMsgToChar(buf, ch); { - // сегменты без запятых -- разделитель ", " и перенос по ширине добавит OutWordsList + // -- ", " OutWordsList std::vector parts; - parts.push_back(fmt::sprintf("Рента: [%d], Денег: [%9ld], В банке: [%9ld] (Всего: %ld)", + parts.push_back(fmt::sprintf(": [%d], : [%9ld], : [%9ld] (: %ld)", GET_LOADROOM(k), currencies::GetHand(*k, currencies::kGold), currencies::GetBank(*k, currencies::kGold), currencies::GetTotal(*k, currencies::kGold))); for (const auto &cur : MUD::Currencies()) { if (cur.GetId() < 0 || cur.GetTextId() == currencies::kGold) { continue; } @@ -310,16 +310,16 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } } else { int mob_online = mob_index[k->get_rnum()].total_online - (virt ? 1 : 0); - snprintf(buf, sizeof(buf), "Сейчас в мире : %d, макс %d. ", mob_online, mob_index[k->get_rnum()].stored); + snprintf(buf, sizeof(buf), " : %d, %d. ", mob_online, mob_index[k->get_rnum()].stored); SendMsgToChar(buf, ch); std::string stats; mob_stat::GetLastMobKill(k, stats); - snprintf(buf, sizeof(buf), "Последний раз убит: &r%s&n", stats.c_str()); + snprintf(buf, sizeof(buf), " : &r%s&n", stats.c_str()); SendMsgToChar(buf, ch); } snprintf(buf, sizeof(buf), - "Сила: [%s%d/%d%s] Инт : [%s%d/%d%s] Мудр : [%s%d/%d%s] \r\n" - "Ловк: [%s%d/%d%s] Тело:[%s%d/%d%s] Обаян:[%s%d/%d%s] Размер: [%s%d/%d%s]\r\n", + ": [%s%d/%d%s] : [%s%d/%d%s] : [%s%d/%d%s] \r\n" + ": [%s%d/%d%s] :[%s%d/%d%s] :[%s%d/%d%s] : [%s%d/%d%s]\r\n", kColorCyn, k->GetInbornStr(), GetRealStr(k), kColorNrm, kColorCyn, k->GetInbornInt(), GetRealInt(k), kColorNrm, kColorCyn, k->GetInbornWis(), GetRealWis(k), kColorNrm, @@ -329,12 +329,12 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { kColorCyn, GET_SIZE(k), GET_REAL_SIZE(k), kColorNrm); SendMsgToChar(buf, ch); - snprintf(buf, sizeof(buf), "Жизни :[%s%d/%d+%d%s] Энергии :[%s%d/%d+%d%s]", + snprintf(buf, sizeof(buf), " :[%s%d/%d+%d%s] :[%s%d/%d+%d%s]", kColorGrn, k->get_hit(), k->get_real_max_hit(), hit_gain(k), kColorNrm, kColorGrn, k->get_move(), k->get_real_max_move(), move_gain(k), kColorNrm); SendMsgToChar(buf, ch); if (IS_MANA_CASTER(k)) { - snprintf(buf, sizeof(buf), " Мана :[%s%d/%d+%d%s]\r\n", + snprintf(buf, sizeof(buf), " :[%s%d/%d+%d%s]\r\n", kColorGrn, k->mem_queue.stored, Mana(GetRealWis(k)), CalcManaGain(k), kColorNrm); } else { snprintf(buf, sizeof(buf), "\r\n"); @@ -342,7 +342,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Glory: [%d], ConstGlory: [%ld], AC: [%d/%d(%d)], Броня: [%d], Попадания: [%2d/%2d/%d], Повреждения: [%2d/%2d/%d]\r\n", + "Glory: [%d], ConstGlory: [%ld], AC: [%d/%d(%d)], : [%d], : [%2d/%2d/%d], : [%2d/%2d/%d]\r\n", Glory::get_glory(k->get_uid()), currencies::GetHand(*k, currencies::kGlory), GET_AC(k), @@ -357,7 +357,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { GetRealDamroll(k) + str_bonus(GetRealStr(k), STR_TO_DAM)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Защитн.аффекты: (базовые) [Will:%d/Crit.:%d/Stab.:%d/Reflex:%d], (полные) Поглощ: [%d], Воля: [%d], Здор.: [%d], Стойк.: [%d], Реакц.: [%d]\r\n", + ".: () [Will:%d/Crit.:%d/Stab.:%d/Reflex:%d], () : [%d], : [%d], .: [%d], .: [%d], .: [%d]\r\n", GetBasicSave(k, ESaving::kWill), GetBasicSave(k, ESaving::kCritical), GetBasicSave(k, ESaving::kStability), @@ -369,12 +369,12 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { CalcSaving(k, k, ESaving::kReflex, 0)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Резисты: [Огонь:%d/Воздух:%d/Вода:%d/Земля:%d/Жизнь:%d/Разум:%d/Иммунитет:%d/Тьма:%d]\r\n", + ": [:%d/:%d/:%d/:%d/:%d/:%d/:%d/:%d]\r\n", GET_RESIST(k, 0), GET_RESIST(k, 1), GET_RESIST(k, 2), GET_RESIST(k, 3), GET_RESIST(k, 4), GET_RESIST(k, 5), GET_RESIST(k, 6), GET_RESIST(k, 7)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Защита от маг. аффектов: [%d], Защита от маг. урона: [%d], Защита от физ. урона: [%d], Маг.урон: [%d], Физ.урон: [%d]\r\n", + " . : [%d], . : [%d], . : [%d], .: [%d], .: [%d]\r\n", GET_AR(k), GET_MR(k), GET_PR(k), @@ -382,7 +382,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { k->add_abils.percent_physdam_add); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Запом: [%d], УспехКолд: [%d], ВоссЖиз: [%d], ВоссСил: [%d], Поглощ: [%d], Удача: [%d], Иниц: [%d]\r\n", + ": [%d], : [%d], : [%d], : [%d], : [%d], : [%d], : [%d]\r\n", GET_MANAREG(k), GET_CAST_SUCCESS(k), k->get_hitreg(), @@ -394,14 +394,14 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sprinttype(static_cast(k->GetPosition()), position_types, smallBuf); { - std::string sline = fmt::sprintf("Положение: %s, Сражается: %s, Экипирован в металл: %s", - smallBuf, (k->GetEnemy() ? GET_NAME(k->GetEnemy()) : "Нет"), (IsEquipInMetall(k) ? "Да" : "Нет")); + std::string sline = fmt::sprintf(": %s, : %s, : %s", + smallBuf, (k->GetEnemy() ? GET_NAME(k->GetEnemy()) : ""), (IsEquipInMetall(k) ? "" : "")); if (k->IsNpc()) { - sline += ", Тип атаки: "; + sline += ", : "; sline += fight::GetAttackTypeDescription(k->mob_specials.attack_type); } if (k->desc) { - sline += ", Соединение: "; + sline += ", : "; sline += GetConDescription(k->desc->state); } sline += "\r\n"; @@ -409,9 +409,9 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } { sprinttype(static_cast(k->mob_specials.default_pos), position_types, buf2); - std::string sline = std::string("Позиция по умолчанию: ") + buf2; + std::string sline = std::string(" : ") + buf2; if (k->char_specials.timer > 0) { - sline += fmt::sprintf(", Таймер отсоединения (тиков) [%d]\r\n", k->char_specials.timer); + sline += fmt::sprintf(", () [%d]\r\n", k->char_specials.timer); } else if (k->extract_timer > 0) { sline += fmt::sprintf(", Extract timer [%d]\r\n", k->extract_timer); } else { @@ -422,29 +422,29 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->IsNpc()) { k->char_specials.saved.mob_flags.sprintbits(action_bits, smallBuf, sizeof(smallBuf), ",", 4); - snprintf(buf, sizeof(buf), "MOB флаги: %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); + snprintf(buf, sizeof(buf), "MOB : %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); SendMsgToChar(buf, ch); k->mob_specials.npc_flags.sprintbits(function_bits, smallBuf, sizeof(smallBuf), ",", 4); - snprintf(buf, sizeof(buf), "NPC флаги: %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); + snprintf(buf, sizeof(buf), "NPC : %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); SendMsgToChar(buf, ch); SendMsgToChar(ch, - "Количество атак: %s%d%s. ", + " : %s%d%s. ", kColorCyn, k->mob_specials.extra_attack + 1, kColorNrm); SendMsgToChar(ch, - "Вероятность использования умений: %s%d%%%s. ", + " : %s%d%%%s. ", kColorCyn, k->mob_specials.like_work, kColorNrm); SendMsgToChar(ch, - "Убить до начала замакса: %s%d%s\r\n", + " : %s%d%s\r\n", kColorCyn, k->mob_specials.MaxFactor, kColorNrm); - // issue #3429: длинные списки умений/заклинаний/способностей переносим - // по ширине экрана игрока через utils::OutWordsList (цвет метки/элементов - // зашит в префикс, ширину считаем без цветокодов). + // issue #3429: // + // utils::OutWordsList ( / + // , ). const size_t list_width = (!ch->IsNpc() && ch->player_specials->saved.stringLength > 0) ? ch->player_specials->saved.stringLength : 120; { @@ -454,10 +454,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(fmt::sprintf("%s:[%d]", skill.GetName(), GetSkill(k, skill.GetId()))); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GУмения:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); } if (!k->mob_specials.have_spell) { - SendMsgToChar(ch, "&GЗаклинания: &Rнет&n\r\n"); + SendMsgToChar(ch, "&G: &R&n\r\n"); } else { std::vector parts; for (auto spell_id = ESpell::kFirst; spell_id <= ESpell::kLast; ++spell_id) { @@ -468,7 +468,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(fmt::sprintf("%s:[%d]", MUD::Spell(spell_id).GetCName(), GET_SPELL_MEM(k, spell_id))); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GЗаклинания:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); } { std::vector parts; @@ -480,11 +480,11 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(feat.GetCName()); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GСпособности:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); } - // информация о маршруте моба + // if (k->mob_specials.dest_count > 0 && k->in_room != kNowhere) { - // подготавливаем путевые точки + // std::stringstream str_dest_list; for (auto i = 0; i < k->mob_specials.dest_count; i++) { if (i) { @@ -494,18 +494,18 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } SendMsgToChar(ch, - "Заданные путевые точки: %s%s%s\r\n", + " : %s%s%s\r\n", kColorCyn, str_dest_list.str().c_str(), kColorNrm); - // Предполагаемый маршрут считаем и показываем только для живого моба - // (не vstat): vstat ставит временный прототип в комнату rnum 1 чужой - // зоны, а kStayZone-моб не строит путь между зонами. В лог это теперь - // не сыплется в любом случае -- find_first_step молчит, пока его не - // попросят жаловаться (issue #3384). + // + // ( vstat): vstat rnum 1 + // , kStayZone- . + // -- find_first_step , + // (issue #3384). if (!virt) { - // пытаемся просчитать маршрут на несколько клеток вперед + // std::vector predictive_path_vnum_list; static const int max_path_size = 25; RoomVnum current_room = world[k->in_room]->vnum; @@ -519,7 +519,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { break; } } - // конвертируем путь из внумов в строку + // std::stringstream str_predictive_path; for (const auto &room : predictive_path_vnum_list) { if (!str_predictive_path.str().empty()) { @@ -529,7 +529,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } SendMsgToChar(ch, - "Предполагаемый маршрут: %s%s%s\r\n", + " : %s%s%s\r\n", kColorCyn, str_predictive_path.str().c_str(), kColorNrm); @@ -559,26 +559,26 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->IsNpc()) { - snprintf(buf, sizeof(buf), "Mob СпецПроц: &R%s&n, NPC сила удара: %dd%d\r\n", + snprintf(buf, sizeof(buf), "Mob : &R%s&n, NPC : %dd%d\r\n", print_special(k).c_str(), k->mob_specials.damnodice, k->mob_specials.damsizedice); SendMsgToChar(buf, ch); } { - std::string sline = fmt::sprintf("Несет - вес %d, предметов %d; ", k->GetCarryingWeight(), k->GetCarryingQuantity()); + std::string sline = fmt::sprintf(" - %d, %d; ", k->GetCarryingWeight(), k->GetCarryingQuantity()); for (i = 0, j = k->carrying; j; j = j->get_next_content(), i++); - sline += fmt::sprintf("(в инвентаре) : %d, ", i); + sline += fmt::sprintf("( ) : %d, ", i); for (i = 0, i2 = 0; i < EEquipPos::kNumEquipPos; i++) { if (GET_EQ(k, i)) { i2++; } } - sline += fmt::sprintf("(надето): %d\r\n", i2); + sline += fmt::sprintf("(): %d\r\n", i2); SendMsgToChar(sline, ch); } if (!k->IsNpc()) { - snprintf(buf, sizeof(buf), "Голод: %d, Жажда: %d, Опьянение: %d\r\n", + snprintf(buf, sizeof(buf), ": %d, : %d, : %d\r\n", GET_COND(k, condition::kFull), GET_COND(k, condition::kThirst), GET_COND(k, condition::kDrunk)); SendMsgToChar(buf, ch); } @@ -586,10 +586,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (god_level >= kLvlGreatGod) { std::string fl_str; - // "Ведущий: X, Ведомые: " передаём префиксом -- его длина (с именем - // ведущего) учитывается в ширине строки автоматически, без магического -30. + // ": X, : " -- ( + // ) , -30. const std::string lead_prefix = - fmt::format("Ведущий: {}, Ведомые: ", (k->has_master() ? GET_NAME(k->get_master()) : "<нет>")); + fmt::format(": {}, : ", (k->has_master() ? GET_NAME(k->get_master()) : "<>")); for (auto &it : k->followers) { if (!it->IsNpc()) { fl_str += " " + it->get_name(); @@ -603,26 +603,26 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { for (auto &helper : k->summon_helpers) { fl_str += " " + std::to_string(helper); } - SendMsgToChar(ch, "%s\r\n", utils::OutWordsList(fl_str, ch->player_specials->saved.stringLength, ", ", "Помогают: ").c_str()); + SendMsgToChar(ch, "%s\r\n", utils::OutWordsList(fl_str, ch->player_specials->saved.stringLength, ", ", ": ").c_str()); } } // Showing the bitvector snprintf(smallBuf, sizeof(smallBuf), "%s", affects::DescribeActive(k->char_specials.saved.affected_by, ", ").c_str()); std::vector out_str = utils::Split(smallBuf, ','); - sprintf(buf, "Аффекты: %s%s%s\r\n", kColorYel, utils::OutWordsList(out_str, ch->player_specials->saved.stringLength - 10).c_str(), kColorNrm); + sprintf(buf, ": %s%s%s\r\n", kColorYel, utils::OutWordsList(out_str, ch->player_specials->saved.stringLength - 10).c_str(), kColorNrm); SendMsgToChar(buf, ch); - snprintf(buf, sizeof(buf), "&GПеревоплощений: %d\r\n&n", remort::GetRealRemort(k)); + snprintf(buf, sizeof(buf), "&G: %d\r\n&n", remort::GetRealRemort(k)); SendMsgToChar(buf, ch); // Routine to show what spells a char is affected by if (!k->affected.empty()) { for (const auto &aff : k->affected) { - std::string sline = fmt::sprintf("Заклинания: (%3d%s|%s) %s%s%s ", + std::string sline = fmt::sprintf(": (%3d%s|%s) %s%s%s ", aff->duration + 1, - (aff->battleflag.get(kAfPulsedec)) || (aff->battleflag.get(kAfSameTime)) ? "плс" : "мин", - (aff->battleflag.get(kAfBattledec)) || (aff->battleflag.get(kAfSameTime)) ? "рнд" : "мин", + (aff->battleflag.get(kAfPulsedec)) || (aff->battleflag.get(kAfSameTime)) ? "" : "", + (aff->battleflag.get(kAfBattledec)) || (aff->battleflag.get(kAfSameTime)) ? "" : "", kColorCyn, // issue.affect-migration: affect name by its own identity (affect_type), spell fallback. - // Ширина поля -- в символах: printf меряет %-21s в байтах (issue #3681). + // -- : printf %-21s (issue #3681). native_text::pad_right( affects::AffectMsg(aff->affect_type, affects::EAffectMsgType::kShortDesc), 21).c_str(), kColorNrm); @@ -651,7 +651,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { do_sstat_character(ch, k); if (MEMORY(k)) { struct mob_ai::MemoryRecord *memchar; - SendMsgToChar("Помнит:\r\n", ch); + SendMsgToChar(":\r\n", ch); for (memchar = MEMORY(k); memchar; memchar = memchar->next) { snprintf(buf, sizeof(buf), "%10ld - %10ld\r\n", static_cast(memchar->id), @@ -664,7 +664,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!SCRIPT(k)->global_vars.empty()) { char name[kMaxInputLength]; void find_uid_name(const char *uid, char *name, size_t name_size); - SendMsgToChar("Глобальные переменные:\r\n", ch); + SendMsgToChar(" :\r\n", ch); // currently, variable context for players is always 0, so it is // not displayed here. in the future, this might change for (auto tv : k->script->global_vars) { @@ -685,20 +685,20 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { std::string quested(k->quested_print()); if (!quested.empty()) { - // issue #3429: список выполненных квестов бывает очень длинным -- - // переносим каждую строку (зону) по ширине экрана через WrapText. + // issue #3429: -- + // () WrapText. const size_t width = (!ch->IsNpc() && ch->player_specials->saved.stringLength > 0) ? ch->player_specials->saved.stringLength : 120; - SendMsgToChar(ch, "Выполнил квесты:\r\n%s\r\n", utils::WrapText(quested, width).c_str()); + SendMsgToChar(ch, " :\r\n%s\r\n", utils::WrapText(quested, width).c_str()); } if (NORENTABLE(k)) { - snprintf(buf, sizeof(buf), "Не может уйти на постой %ld\r\n", + snprintf(buf, sizeof(buf), " %ld\r\n", static_cast(NORENTABLE(k) - time(nullptr))); SendMsgToChar(buf, ch); } if (AGRO(k)) { - snprintf(buf, sizeof(buf), "Агрессор %ld\r\n", static_cast(AGRO(k) - time(nullptr))); + snprintf(buf, sizeof(buf), " %ld\r\n", static_cast(AGRO(k) - time(nullptr))); SendMsgToChar(buf, ch); } pk_list_sprintf(k, buf); @@ -716,21 +716,21 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { vnum = GET_OBJ_VNUM(j); rnum = j->get_rnum(); - snprintf(buf, sizeof(buf), "Название: '%s%s%s',\r\nСинонимы: '&c%s&n',", + snprintf(buf, sizeof(buf), ": '%s%s%s',\r\n: '&c%s&n',", kColorYel, (!j->get_short_description().empty() ? j->get_short_description().c_str() : ""), kColorNrm, j->get_aliases().c_str()); SendMsgToChar(buf, ch); if (j->get_custom_label() && !j->get_custom_label()->text_label.empty()) { - snprintf(buf, sizeof(buf), " нацарапано: '&c%s&n',", j->get_custom_label()->text_label.c_str()); + snprintf(buf, sizeof(buf), " : '&c%s&n',", j->get_custom_label()->text_label.c_str()); SendMsgToChar(buf, ch); } snprintf(buf, sizeof(buf), "\r\n"); SendMsgToChar(buf, ch); sprinttype(j->get_type(), item_types, buf1); if (rnum >= 0) { - snprintf(buf2, sizeof(buf2), "%s", (obj_proto.func(j->get_rnum()) ? "Есть" : "Нет")); + snprintf(buf2, sizeof(buf2), "%s", (obj_proto.func(j->get_rnum()) ? "" : "")); } else { snprintf(buf2, sizeof(buf2), "None"); } @@ -738,41 +738,41 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(ch, "VNum: [%s%5d%s], RNum: [%5d], UniqueID: [%ld], Id: [%ld]\r\n", kColorGrn, vnum, kColorNrm, j->get_rnum(), j->get_unique_id(), j->get_id()); - SendMsgToChar(ch, "Расчет критерия: %f, мортов: (%f) \r\n", j->show_koef_obj(), j->show_mort_req()); - SendMsgToChar(ch, "Тип: %s, СпецПроцедура: %s", buf1, buf2); + SendMsgToChar(ch, " : %f, : (%f) \r\n", j->show_koef_obj(), j->show_mort_req()); + SendMsgToChar(ch, ": %s, : %s", buf1, buf2); if (j->get_owner()) { auto tmpstr = GetPlayerNameByUnique(j->get_owner()); - SendMsgToChar(ch, ", Владелец : %s", tmpstr.empty() ? "УДАЛЕН": tmpstr.c_str()); + SendMsgToChar(ch, ", : %s", tmpstr.empty() ? "": tmpstr.c_str()); } // if (GET_OBJ_ZONE(j)) - SendMsgToChar(ch, ", Принадлежит зоне VNUM : %d", j->get_vnum_zone_from()); + SendMsgToChar(ch, ", VNUM : %d", j->get_vnum_zone_from()); if (j->get_crafter_uid()) { auto to_name = GetPlayerNameByUnique(j->get_crafter_uid()); if (to_name.empty()) { - SendMsgToChar(ch, ", Создатель : не найден"); + SendMsgToChar(ch, ", : "); } else { - SendMsgToChar(ch, ", Создатель : %s", to_name.c_str()); + SendMsgToChar(ch, ", : %s", to_name.c_str()); } } if (obj_proto[j->get_rnum()]->get_parent_rnum() > -1) { - SendMsgToChar(ch, ", Родитель(VNum) : [%d]", obj_proto[j->get_rnum()]->get_parent_vnum()); + SendMsgToChar(ch, ", (VNum) : [%d]", obj_proto[j->get_rnum()]->get_parent_vnum()); } if (j->get_craft_timer() > 0) { - SendMsgToChar(ch, ", &Yскрафчена с таймером : [%d]&n", j->get_craft_timer()); + SendMsgToChar(ch, ", &Y : [%d]&n", j->get_craft_timer()); } - if (j->get_is_rename()) // изменены падежи + if (j->get_is_rename()) // { - SendMsgToChar(ch, ", &Gпадежи отличны от прототипа&n"); + SendMsgToChar(ch, ", &G &n"); } SendMsgToChar(ch, "\r\n%s", sight::diag_weapon_to_char(j, 2)); snprintf(buf, sizeof(buf), "L-Des: %s\r\n%s", - !j->get_description().empty() ? j->get_description().c_str() : "Нет", + !j->get_description().empty() ? j->get_description().c_str() : "", kColorNrm); SendMsgToChar(buf, ch); if (!j->get_ex_description().empty()) { - std::string sline = fmt::sprintf("Экстра описание:%s", kColorCyn); + std::string sline = fmt::sprintf(" :%s", kColorCyn); for (const auto &desc : j->get_ex_description()) { sline += " "; sline += desc.keyword; @@ -781,27 +781,27 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { sline += "\r\n"; SendMsgToChar(sline, ch); } - SendMsgToChar("Может быть надет : ", ch); + SendMsgToChar(" : ", ch); sprintbit(j->get_wear_flags(), wear_bits, buf, sizeof(buf)); SendMsgToChar(ch, "%s\r\n", buf); sprinttype(j->get_material(), material_name, buf2); - snprintf(buf, sizeof(buf), "Материал : %s, макс.прочность : %d, тек.прочность : %d\r\n", + snprintf(buf, sizeof(buf), " : %s, . : %d, . : %d\r\n", buf2, j->get_maximum_durability(), j->get_current_durability()); SendMsgToChar(buf, ch); - SendMsgToChar("Неудобства : ", ch); + SendMsgToChar(" : ", ch); j->get_no_flags().sprintbits(no_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - SendMsgToChar("Запреты : ", ch); + SendMsgToChar(" : ", ch); j->get_anti_flags().sprintbits(anti_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - SendMsgToChar("Устанавливает аффекты : ", ch); + SendMsgToChar(" : ", ch); j->get_affect_flags().sprintbits(equipment_affects, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); if (j->has_suppressed_affects()) { - SendMsgToChar("Подавленные аффекты : ", ch); + SendMsgToChar(" : ", ch); std::string sup; for (const auto &pr : j->suppressed_equip_affects()) { char one[128]; @@ -813,23 +813,23 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(ch, "%s\r\n", sup.c_str()); } - SendMsgToChar("Дополнительные флаги : ", ch); + SendMsgToChar(" : ", ch); j->get_extra_flags().sprintbits(extra_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - snprintf(buf, sizeof(buf), "Вес: %d, Цена: %d, Рента(eq): %d, Рента(inv): %d, ", + snprintf(buf, sizeof(buf), ": %d, : %d, (eq): %d, (inv): %d, ", j->get_weight(), j->get_cost(), j->get_rent_on(), j->get_rent_off()); SendMsgToChar(buf, ch); if (stable_objs::IsTimerUnlimited(j)) - snprintf(buf, sizeof(buf), "Таймер: нерушимо, "); + snprintf(buf, sizeof(buf), ": , "); else - snprintf(buf, sizeof(buf), "Таймер: %d, ", j->get_timer()); + snprintf(buf, sizeof(buf), ": %d, ", j->get_timer()); SendMsgToChar(buf, ch); - // Таймер на земле тикает только пока вещь лежит в комнате, и выставляется при попадании - // туда (PlaceObjToRoom). У вещи в инвентаре в этом поле лежит умолчание конструктора (60), - // которое выглядело настоящим таймером, хотя таким никогда не станет. + // , + // (PlaceObjToRoom). (60), + // , . if (j->get_in_room() != kNowhere) { - snprintf(buf, sizeof(buf), "Таймер на земле: %d\r\n", j->get_destroyer()); + snprintf(buf, sizeof(buf), " : %d\r\n", j->get_destroyer()); } else { snprintf(buf, sizeof(buf), "\r\n"); } @@ -846,7 +846,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (str.empty()) { for (ExchangeItem *tmp_obj = exchange_item_list; tmp_obj; tmp_obj = tmp_obj->next) { if (GET_EXCHANGE_ITEM(tmp_obj)->get_unique_id() == j->get_unique_id()) { - str = fmt::format("продается на базаре, лот #{}\r\n", GET_EXCHANGE_ITEM_LOT(tmp_obj)); + str = fmt::format(" , #{}\r\n", GET_EXCHANGE_ITEM_LOT(tmp_obj)); break; } } @@ -857,7 +857,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (!tmp_obj) { continue; } - str = fmt::format("можно купить в магазине: {}\r\n", shop->GetDictionaryName()); + str = fmt::format(" : {}\r\n", shop->GetDictionaryName()); } } if (!str.empty()) { @@ -866,33 +866,33 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { } else { auto room = get_room_where_obj(j); - std::string sline = "&CНаходится в комнате : "; + std::string sline = "&C : "; if (room == kNowhere || !is_grgod) { - sline += "нигде"; + sline += ""; } else { sline += std::to_string(room); } - sline += ", В контейнере: "; + sline += ", : "; if (j->get_in_obj() && is_grgod) { sline += fmt::sprintf("[%d] %s", GET_OBJ_VNUM(j->get_in_obj()), j->get_in_obj()->get_short_description().c_str()); } else { - sline += "Нет"; + sline += ""; } - sline += ", В инвентаре: "; + sline += ", : "; if (j->get_carried_by() && is_grgod) { sline += GET_NAME(j->get_carried_by()); } else if (j->get_in_obj() && j->get_in_obj()->get_carried_by() && is_grgod) { sline += GET_NAME(j->get_in_obj()->get_carried_by()); } else { - sline += "Нет"; + sline += ""; } - sline += ", Надет: "; + sline += ", : "; if (j->get_worn_by() && is_grgod) { sline += GET_NAME(j->get_worn_by()); } else if (j->get_in_obj() && j->get_in_obj()->get_worn_by() && is_grgod) { sline += GET_NAME(j->get_in_obj()->get_worn_by()); } else { - sline += "Нет"; + sline += ""; } sline += "\r\n&n"; SendMsgToChar(sline, ch); @@ -905,112 +905,112 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EBook::kSpell: { auto spell_id = static_cast(GET_OBJ_VAL(j, 1)); if (spell_id >= ESpell::kFirst && spell_id <= ESpell::kLast) { - snprintf(buf, sizeof(buf), "содержит заклинание : \"%s\"", MUD::Spell(spell_id).GetCName()); + snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Spell(spell_id).GetCName()); } else - snprintf(buf, sizeof(buf), "неверный номер заклинания"); + snprintf(buf, sizeof(buf), " "); break; } case EBook::kSkill: { auto skill_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Skill(skill_id).IsValid()) { - snprintf(buf, sizeof(buf), "содержит секрет умения : \"%s\"", MUD::Skill(skill_id).GetName()); + snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Skill(skill_id).GetName()); } else - snprintf(buf, sizeof(buf), "неверный номер умения"); + snprintf(buf, sizeof(buf), " "); break; } case EBook::kSkillUpgrade: { auto skill_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Skills().IsValid(skill_id)) { if (GET_OBJ_VAL(j, 3) > 0) { - snprintf(buf, sizeof(buf), "повышает умение \"%s\" (максимум %d)", + snprintf(buf, sizeof(buf), " \"%s\" ( %d)", MUD::Skill(skill_id).GetName(), GET_OBJ_VAL(j, 3)); } else { - snprintf(buf, sizeof(buf), "повышает умение \"%s\" (не больше максимума текущего перевоплощения)", + snprintf(buf, sizeof(buf), " \"%s\" ( )", MUD::Skill(skill_id).GetName()); } } else { - snprintf(buf, sizeof(buf), "неверный номер повышаемого умения"); + snprintf(buf, sizeof(buf), " "); } } break; case EBook::kFeat: { const auto feat_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Feat(feat_id).IsValid()) { - snprintf(buf, sizeof(buf), "содержит секрет способности : \"%s\"", MUD::Feat(feat_id).GetCName()); + snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Feat(feat_id).GetCName()); } else { - snprintf(buf, sizeof(buf), "неверный номер способности"); + snprintf(buf, sizeof(buf), " "); } } break; case EBook::kReceipt: { const auto recipe = im_get_recipe(GET_OBJ_VAL(j, 1)); if (recipe >= 0) { - // issue.class-recipes: требования теперь per-class; класса тут нет, поэтому - // показываем минимальные уровень/реморт среди владеющих классов. + // issue.class-recipes: per-class; , + // / . const auto [minlevel, minremort] = classes::GetRecipeMinRequirements(imrecipes[recipe].str_id); if ((minlevel >= 0) && (minremort >= 0)) { const auto recipelevel = std::max(GET_OBJ_VAL(j, 2), minlevel); snprintf(buf, sizeof(buf), - "содержит рецепт отвара : \"%s\", мин. уровень изучения: %d, мин. количество ремортов: %d", + " : \"%s\", . : %d, . : %d", imrecipes[recipe].name, recipelevel, minremort); } else { snprintf(buf, sizeof(buf), - "содержит рецепт отвара : \"%s\" (не доступен ни одному классу)", + " : \"%s\" ( )", imrecipes[recipe].name); } } else - snprintf(buf, sizeof(buf), "Некорректная запись рецепта"); + snprintf(buf, sizeof(buf), " "); } break; - default:snprintf(buf, sizeof(buf), "НЕВЕРНО УКАЗАН ТИП КНИГИ"); + default:snprintf(buf, sizeof(buf), " "); break; } break; case EObjType::kLightSource: if (GET_OBJ_VAL(j, 2) < 0) { - snprintf(buf, sizeof(buf), "Вечный свет!"); + snprintf(buf, sizeof(buf), " !"); } else { - snprintf(buf, sizeof(buf), "Осталось светить: [%d]", GET_OBJ_VAL(j, 2)); + snprintf(buf, sizeof(buf), " : [%d]", GET_OBJ_VAL(j, 2)); } break; case EObjType::kScroll: { - // issue.magic-items: заклинания свитка лежат в extra_values, сила -- умение мастера + // issue.magic-items: extra_values, -- snprintf(buf, sizeof(buf), "%s", utils::OutWordsList(SpellItemSpellsWithPotency(j), ch->player_specials->saved.stringLength, ", ", - std::string(kColorGrn) + "Заклинания:" + kColorNrm + " ").c_str()); + std::string(kColorGrn) + ":" + kColorNrm + " ").c_str()); break; } case EObjType::kPotion: { snprintf(buf, sizeof(buf), "%s", utils::OutWordsList(SpellItemSpellsWithPotency(j), ch->player_specials->saved.stringLength, ", ", - std::string(kColorGrn) + "Заклинания:" + kColorNrm + " ").c_str()); + std::string(kColorGrn) + ":" + kColorNrm + " ").c_str()); break; } case EObjType::kWand: case EObjType::kStaff: - // issue.magic-items: заклинание и заряды берем из extra_values, val[] у посохов нулевые + // issue.magic-items: extra_values, val[] { const auto staff_spell = static_cast(j->GetSpellItemSpellNum(1)); const int potency = static_cast(MagicItemPotency(j, staff_spell) + 0.5f); - // issue #3611: у вещи из прототипа сила посчитана по зашитым умолчаниям, а не по - // умению мастера -- помечаем так же, как в перечне заклинаний свитков и зелий. - snprintf(buf, sizeof(buf), "%sЗаклинание:%s %s%s%s (сила %d%s), %d (из %d) зарядов осталось", + // issue #3611: , + // -- , . + snprintf(buf, sizeof(buf), "%s:%s %s%s%s ( %d%s), %d ( %d) ", kColorGrn, kColorNrm, kColorCyn, MUD::Spell(staff_spell).GetCName(), kColorNrm, potency, - IsPotencyFromProto(j) ? ", базовая" : "", + IsPotencyFromProto(j) ? ", " : "", j->GetPotionValueKey(ObjVal::EValueKey::kCurCharges), j->GetPotionValueKey(ObjVal::EValueKey::kMaxCharges)); } break; case EObjType::kWeapon: - snprintf(buf, sizeof(buf), "Повреждения: %dd%d, Тип повреждения: %d", + snprintf(buf, sizeof(buf), ": %dd%d, : %d", GET_OBJ_VAL(j, 1), GET_OBJ_VAL(j, 2), GET_OBJ_VAL(j, 3)); @@ -1019,7 +1019,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kArmor: case EObjType::kLightArmor: case EObjType::kMediumArmor: - case EObjType::kHeavyArmor:snprintf(buf, sizeof(buf), "AC: [%d] Броня: [%d]", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); + case EObjType::kHeavyArmor:snprintf(buf, sizeof(buf), "AC: [%d] : [%d]", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); break; case EObjType::kTrap:snprintf(buf, sizeof(buf), "Spell: %d, - Hitpoints: %d", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); @@ -1027,10 +1027,10 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kContainer:sprintbit(GET_OBJ_VAL(j, 1), container_bits, smallBuf, sizeof(smallBuf)); if (IS_CORPSE(j)) { - snprintf(buf, sizeof(buf), "Объем: %d, Тип ключа: %s, VNUM моба: %d, Труп: да", + snprintf(buf, sizeof(buf), ": %d, : %s, VNUM : %d, : ", GET_OBJ_VAL(j, 0), smallBuf, GET_OBJ_VAL(j, 2)); } else { - snprintf(buf, sizeof(buf), "Объем: %d, Тип ключа: %s, Номер ключа: %d, Сложность замка: %d", + snprintf(buf, sizeof(buf), ": %d, : %s, : %d, : %d", GET_OBJ_VAL(j, 0), smallBuf, GET_OBJ_VAL(j, 2), GET_OBJ_VAL(j, 3)); } break; @@ -1040,7 +1040,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { { std::string spells = drinkcon::print_spells(j); utils::Trim(spells); - snprintf(buf, sizeof(buf), "Объем: %d, Содержит: %d, Свежесть: %d, Отрава: %d, Жидкость: %s\r\n%s", + snprintf(buf, sizeof(buf), ": %d, : %d, : %d, : %d, : %s\r\n%s", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidTimer), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidPoison), smallBuf, spells.c_str()); @@ -1055,14 +1055,14 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kFood: snprintf(buf, sizeof(buf), - "Насыщает(час): %d, Свежесть: %d, Отрава: %d", + "(): %d, : %d, : %d", GET_OBJ_VAL(j, 0), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidTimer), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidPoison)); break; case EObjType::kMoney: - snprintf(buf, sizeof(buf), "Сумма: %d\r\nВалюта: %s", GET_OBJ_VAL(j, 0), + snprintf(buf, sizeof(buf), ": %d\r\n: %s", GET_OBJ_VAL(j, 0), MUD::Currency(GET_OBJ_VAL(j, 1)).GetCName(grammar::ECase::kNom)); break; @@ -1071,35 +1071,35 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (IS_SET(j->get_spec_param(), kItemCheckUses)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить %d раз", GET_OBJ_VAL(j, 2)); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %d ", GET_OBJ_VAL(j, 2)); } if (IS_SET(j->get_spec_param(), kItemCheckLag)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить 1 раз в %d сек", (i = GET_OBJ_VAL(j, 0) & 0xFF)); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n 1 %d ", (i = GET_OBJ_VAL(j, 0) & 0xFF)); buf_len = strlen(buf); if (GET_OBJ_VAL(j, 3) == 0 || GET_OBJ_VAL(j, 3) + i < time(nullptr)) { - snprintf(buf + buf_len, sizeof(buf) - buf_len, "(можно применять)."); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "( )."); } else { li = GET_OBJ_VAL(j, 3) + i - time(nullptr); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "(осталось %ld сек).", li); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "( %ld ).", li); } } if (IS_SET(j->get_spec_param(), kItemCheckLevel)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить с %d уровня.", (GET_OBJ_VAL(j, 0) >> 8) & 0x1F); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %d .", (GET_OBJ_VAL(j, 0) >> 8) & 0x1F); } if ((i = GetObjRnum(GET_OBJ_VAL(j, 1))) >= 0) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nпрототип %s%s%s.", + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %s%s%s.", kColorBoldCyn, obj_proto[i]->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); } break; case EObjType::kMagicContaner: case EObjType::kMagicArrow: - snprintf(buf, sizeof(buf), "Заклинание: [%s]. Объем [%d]. Осталось стрел[%d].", + snprintf(buf, sizeof(buf), ": [%s]. [%d]. [%d].", MUD::Spell(static_cast(GET_OBJ_VAL(j, 0))).GetCName(), GET_OBJ_VAL(j, 1), GET_OBJ_VAL(j, 2)); break; @@ -1115,7 +1115,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { // * more or less useless and just takes up valuable screen space. if (j->get_contains()) { - std::string sline = fmt::sprintf("\r\nСодержит:%s", kColorGrn); + std::string sline = fmt::sprintf("\r\n:%s", kColorGrn); for (found = 0, j2 = j->get_contains(); j2; j2 = j2->get_next_content()) { sline += fmt::sprintf("%s %s", found++ ? "," : "", j2->get_short_description().c_str()); if (sline.size() >= 62) { @@ -1132,7 +1132,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(kColorNrm, ch); } found = 0; - SendMsgToChar("Аффекты:", ch); + SendMsgToChar(":", ch); for (i = 0; i < kMaxObjAffect; i++) { if (j->get_affected(i).modifier) { sprinttype(j->get_affected(i).location, apply_types, smallBuf); @@ -1141,14 +1141,14 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { } } if (!found) { - SendMsgToChar(" Нет", ch); + SendMsgToChar(" ", ch); } // issue.obj-affects: obj affects on the item (gods see everything -> nullptr viewer). { const std::string oaff = obj_affects::Diag(j, nullptr); if (!oaff.empty()) { - SendMsgToChar("\r\nОбъектные аффекты:\r\n", ch); + SendMsgToChar("\r\n :\r\n", ch); SendMsgToChar(oaff, ch); } } @@ -1157,7 +1157,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { CObjectPrototype::skills_t skills; j->get_skills(skills); - SendMsgToChar("\r\nУмения :", ch); + SendMsgToChar("\r\n :", ch); for (const auto &it : skills) { if (it.second == 0) { continue; @@ -1169,18 +1169,18 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar("\r\n", ch); if (j->get_ilevel() > 0) { - SendMsgToChar(ch, "Уровень (ilvl): %f\r\n", j->get_ilevel()); + SendMsgToChar(ch, " (ilvl): %f\r\n", j->get_ilevel()); } if (j->get_minimum_remorts() != 0) { - SendMsgToChar(ch, "Проставлено поле перевоплощений: %d\r\n", j->get_minimum_remorts()); + SendMsgToChar(ch, " : %d\r\n", j->get_minimum_remorts()); } else if (j->get_auto_mort_req() > 0) { - SendMsgToChar(ch, "Вычислено поле минимальных перевоплощений: %d\r\n", j->get_auto_mort_req()); + SendMsgToChar(ch, " : %d\r\n", j->get_auto_mort_req()); } - SendMsgToChar(ch, "Сохраненные переменные из DGScript: %s\r\n", j->get_dgscript_field().empty() ? "ничего" : j->get_dgscript_field().c_str()); + SendMsgToChar(ch, " DGScript: %s\r\n", j->get_dgscript_field().empty() ? "" : j->get_dgscript_field().c_str()); if (is_grgod) { snprintf(buf, sizeof(buf), - "Сейчас в мире : %d. На постое : %d. Макс в мире: %d\r\n", + " : %d. : %d. : %d\r\n", rnum >= 0 ? obj_proto.total_online(rnum) - (virt ? 1 : 0) : -1, rnum >= 0 ? obj_proto.stored(rnum) : -1, GetObjMIW(j->get_rnum())); @@ -1200,24 +1200,24 @@ void do_stat_room(CharData *ch, const int rnum = 0) { rm = world[rnum]; } - snprintf(buf, sizeof(buf), "Комната : %s%s%s\r\n", kColorCyn, rm->name, kColorNrm); + snprintf(buf, sizeof(buf), " : %s%s%s\r\n", kColorCyn, rm->name, kColorNrm); SendMsgToChar(buf, ch); sprinttype(rm->sector_type, sector_types, smallBuf); snprintf(buf, sizeof(buf), - "Зона: [%3d], VNum: [%s%5d%s], RNum: [%5d], Тип сектора: %s\r\n", + ": [%3d], VNum: [%s%5d%s], RNum: [%5d], : %s\r\n", zone_table[rm->zone_rn].vnum, kColorGrn, rm->vnum, kColorNrm, rnum, smallBuf); SendMsgToChar(buf, ch); rm->flags_sprint(smallBuf, sizeof(smallBuf), ","); - snprintf(buf, sizeof(buf), "СпецПроцедура: %s, Флаги: %s\r\n", (rm->func == nullptr) ? "None" : "Exists", smallBuf); + snprintf(buf, sizeof(buf), ": %s, : %s\r\n", (rm->func == nullptr) ? "None" : "Exists", smallBuf); SendMsgToChar(buf, ch); - SendMsgToChar("Описание:\r\n", ch); + SendMsgToChar(":\r\n", ch); SendMsgToChar(GlobalObjects::descriptions().get(rm->description_num), ch); if (!rm->ex_description.empty()) { - std::string sline = fmt::sprintf("Доп. описание:%s", kColorCyn); + std::string sline = fmt::sprintf(". :%s", kColorCyn); for (const auto &desc : rm->ex_description) { sline += " "; sline += desc.keyword; @@ -1227,7 +1227,7 @@ void do_stat_room(CharData *ch, const int rnum = 0) { SendMsgToChar(sline, ch); } { - std::string sline = fmt::sprintf("Живые существа:%s", kColorYel); + std::string sline = fmt::sprintf(" :%s", kColorYel); found = 0; size_t counter = 0; for (auto k_i = rm->people.begin(); k_i != rm->people.end(); ++k_i) { @@ -1252,7 +1252,7 @@ void do_stat_room(CharData *ch, const int rnum = 0) { SendMsgToChar(kColorNrm, ch); } if (!rm->contents.empty()) { - std::string sline = fmt::sprintf("Предметы:%s", kColorGrn); + std::string sline = fmt::sprintf(":%s", kColorGrn); found = 0; for (auto it = rm->contents.begin(); it != rm->contents.end(); ++it) { auto j = *it; @@ -1281,31 +1281,31 @@ void do_stat_room(CharData *ch, const int rnum = 0) { GET_ROOM_VNUM(rm->dir_option[i]->to_room()), kColorNrm); sprintbit(rm->dir_option[i]->exit_info.get_plane(0), exit_bits, tmpBuf, sizeof(tmpBuf)); snprintf(buf, sizeof(buf), - "Выход %s%s%s: Ведет в : [%s], Ключ: [%5d], Название: %s (%s), Тип: %s\r\n", + " %s%s%s: : [%s], : [%5d], : %s (%s), : %s\r\n", kColorCyn, native_text::pad_right(dirs[i], 5).c_str(), kColorNrm, smallBuf, rm->dir_option[i]->key, - rm->dir_option[i]->keyword ? rm->dir_option[i]->keyword : "Нет(дверь)", - rm->dir_option[i]->vkeyword ? rm->dir_option[i]->vkeyword : "Нет(дверь)", tmpBuf); + rm->dir_option[i]->keyword ? rm->dir_option[i]->keyword : "()", + rm->dir_option[i]->vkeyword ? rm->dir_option[i]->vkeyword : "()", tmpBuf); SendMsgToChar(buf, ch); if (!rm->dir_option[i]->general_description.empty()) { snprintf(buf, sizeof(buf), " %s\r\n", rm->dir_option[i]->general_description.c_str()); } else { - snprintf(buf, sizeof(buf), " Нет описания выхода.\r\n"); + snprintf(buf, sizeof(buf), " .\r\n"); } SendMsgToChar(buf, ch); } } if (!rm->affected.empty()) { - snprintf(buf1, sizeof(buf1), "&GАффекты на комнате:\r\n&n"); + snprintf(buf1, sizeof(buf1), "&G :\r\n&n"); for (const auto &aff : rm->affected) { size_t buf1_len = strlen(buf1); - snprintf(buf1 + buf1_len, sizeof(buf1) - buf1_len, " Заклинание \"%s\" (длит: %d, модиф: %d, сила: %.1f) - %s.\r\n", + snprintf(buf1 + buf1_len, sizeof(buf1) - buf1_len, " \"%s\" (: %d, : %d, : %.1f) - %s.\r\n", NAME_BY_ITEM(aff->affect_type).c_str(), aff->duration, room_spells::IsPortalAffect(aff->affect_type) ? world[aff->modifier]->vnum : aff->modifier, aff->potency, - (k = find_char(aff->caster_id)) ? GET_NAME(k) : "неизвестно"); + (k = find_char(aff->caster_id)) ? GET_NAME(k) : ""); } SendMsgToChar(buf1, ch); } @@ -1316,15 +1316,15 @@ void do_stat_room(CharData *ch, const int rnum = 0) { if (!ex || ex->affected.empty()) { continue; } - snprintf(buf1, sizeof(buf1), "&GАффекты на выходе (%s):\r\n&n", dirs_rus[d]); + snprintf(buf1, sizeof(buf1), "&G (%s):\r\n&n", dirs_rus[d]); for (const auto &aff : ex->affected) { const size_t len = strlen(buf1); snprintf(buf1 + len, sizeof(buf1) - len, - " Заклинание \"%s\" (длит: %d, модиф: %d, сила: %.1f, заряды: %s) - %s.\r\n", + " \"%s\" (: %d, : %d, : %.1f, : %s) - %s.\r\n", NAME_BY_ITEM(aff->affect_type).c_str(), aff->duration, aff->modifier, aff->potency, - (aff->charges == -1 ? "беск" : std::to_string(aff->charges).c_str()), - (k = find_char(aff->caster_id)) ? GET_NAME(k) : "неизвестно"); + (aff->charges == -1 ? "" : std::to_string(aff->charges).c_str()), + (k = find_char(aff->caster_id)) ? GET_NAME(k) : ""); } SendMsgToChar(buf1, ch); } @@ -1342,11 +1342,11 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { half_chop(argument, buf1, buf2); if (!(privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false)) && (GET_OLC_ZONE(ch) <=0)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } if (!*buf1) { - SendMsgToChar("Состояние КОГО или ЧЕГО?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } if (*buf1 && privilege::IsImmortal(ch)) { @@ -1356,7 +1356,7 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { if ((rnum = GetRoomRnum(vnum)) != kNowhere) do_stat_room(ch, rnum); else - SendMsgToChar("Состояние какой комнаты?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); } if (!*buf2) do_stat_room(ch); @@ -1364,91 +1364,91 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if (utils::IsAbbr(buf1, "mob") && level >= kLvlBuilder) { if (!*buf2) - SendMsgToChar("Состояние какого создания?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); else { victim = target_resolver::FindCharInWorld(ch, buf2); if ((victim != nullptr)) do_stat_character(ch, victim, 0); else - SendMsgToChar("Нет такого создания в этом МАДе.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return; } if (utils::IsAbbr(buf1, "player")) { if (!*buf2) { - SendMsgToChar("Состояние какого игрока?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) do_stat_character(ch, victim); else - SendMsgToChar("Этого персонажа сейчас нет в игре.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return; } if (utils::IsAbbr(buf1, "ip")) { if (!*buf2) { - SendMsgToChar("Состояние ip какого игрока?\r\n", ch); + SendMsgToChar(" ip ?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) { do_statip(ch, victim); return; } else { - SendMsgToChar("Этого персонажа сейчас нет в игре, смотрим пфайл.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); } Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { do_statip(ch, &t_vict); } else { - SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } return; } - if (utils::IsAbbr(buf1, "karma") || utils::IsAbbr(buf1, "карма")) { + if (utils::IsAbbr(buf1, "karma") || utils::IsAbbr(buf1, "")) { if (!*buf2) { - SendMsgToChar("Карму какого игрока?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) { DoStatKarma(ch, victim); return; } else { - SendMsgToChar("Этого персонажа сейчас нет в игре, смотрим пфайл.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); } Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { DoStatKarma(ch, &t_vict); } else { - SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } return; } if (utils::IsAbbr(buf1, "file")) { if (!*buf2) { - SendMsgToChar("Состояние какого игрока(из файла)?\r\n", ch); + SendMsgToChar(" ( )?\r\n", ch); } else { Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { if (GetRealLevel(&t_vict) > level) { - SendMsgToChar("Извините, вам это еще рано.\r\n", ch); + SendMsgToChar(", .\r\n", ch); } else { Clan::SetClanData(&t_vict); do_stat_character(ch, &t_vict); } } else { - SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } return; } if (utils::IsAbbr(buf1, "object") && level >= kLvlBuilder) { if (!*buf2) - SendMsgToChar("Состояние какого предмета?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); else { if ((object = target_resolver::FindObjInWorld(ch, buf2)) != nullptr) do_stat_object(ch, object); else - SendMsgToChar("Нет такого предмета в игре.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return; } @@ -1499,5 +1499,5 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { return; } } - SendMsgToChar("Ничего похожего с этим именем нет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } diff --git a/src/engine/ui/commands.cpp b/src/engine/ui/commands.cpp index 80210898c..2eafa4cc9 100644 --- a/src/engine/ui/commands.cpp +++ b/src/engine/ui/commands.cpp @@ -210,7 +210,7 @@ void CommandEmbranchmentImplementation::make_suggestion(const CommandContext::sh std::string suggestions_string; joinList(suggestions, suggestions_string); std::stringstream ss; - ss << "Подкоманда [" << wrong_subcommand << "] не существует. Возможно, вы имели ввиду одну из следующих: " + ss << " [" << wrong_subcommand << "] . , : " << suggestions_string << "?"; reply(context, ss.str()); @@ -246,7 +246,7 @@ CommandEmbranchment::shared_ptr CommandEmbranchment::create(const std::string &h return result; } -ParentalHelp::ParentalHelp(const shared_ptr &parent) : CommandWithHelp("Показать справку."), m_parent(parent) { +ParentalHelp::ParentalHelp(const shared_ptr &parent) : CommandWithHelp(" ."), m_parent(parent) { } void ParentalHelp::execute(const CommandContext::shared_ptr &context, diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index f97b2ff2c..3fe31195e 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -72,12 +72,12 @@ using namespace ClanSystem; extern int mortal_start_room; extern void sight::list_obj_to_char(ObjData *list, CharData *ch, int mode, int show); extern int AllocateBufferForFile(const char *name, char **destination_buf); -// TODO: думать надо с этим, или глобально следить за спамом, или игноров напихать на все случаи жизни, или так и оставить +// TODO: , , , extern void SetWait(CharData *ch, int waittime, int victim_in_room); extern const char *sight::show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_state, int how); extern bool char_to_pk_clan(CharData *ch); -void fix_ingr_chest_rnum(const int room_rnum)//Нужно чтоб позиция короба не съехала +void fix_ingr_chest_rnum(const int room_rnum)// { for (const auto &i : Clan::ClanList) { if (i->get_ingr_chest_room_rnum() >= room_rnum) @@ -96,17 +96,17 @@ long long clan_level_exp[kMaxClanlevel + 1] = 1000000000000LL // BIG NUMBER. // }; -// vnum кланового сундука +// vnum const int CLAN_CHEST_VNUM = 330; int CLAN_CHEST_RNUM = -1; -// vnum кланового сундука +// vnum const int INGR_CHEST_VNUM = 333; int INGR_CHEST_RNUM = -1; -// налог на хранилище ингров (в день) +// ( ) const int INGR_CHEST_TAX = 1000; -// макс. длина сообщения дружины +// . const int MAX_MOD_LENGTH = 3 * 80; -// макс. длина названия ранга в дружине +// . const unsigned MAX_RANK_LENGHT = 10; enum { @@ -116,21 +116,21 @@ enum { void prepare_write_mod(CharData *ch, std::string ¶m) { utils::Trim(param); - if (!param.empty() && (CompareParam(param, "очистить") || CompareParam(param, "удалить"))) { + if (!param.empty() && (CompareParam(param, "") || CompareParam(param, ""))) { std::string zero_str; CLAN(ch)->write_mod(zero_str); - SendMsgToChar("Сообщение удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - SendMsgToChar("Можете писать сообщение. (/s записать /h помощь)\r\n", ch); + SendMsgToChar(" . (/s /h )\r\n", ch); ch->desc->state = EConState::kWriteMod; utils::AbstractStringWriter::shared_ptr writer(new utils::StdStringWriter()); string_write(ch->desc, writer, MAX_MOD_LENGTH, 0, nullptr); } /** -* Обрезание названий рангов дружины до MAX_RANK_LENGHT символов -* и перевод всего слова в нижний регистр. +* MAX_RANK_LENGHT +* . */ void check_rank(std::string &rank) { if (rank.size() > MAX_RANK_LENGHT) { @@ -141,7 +141,7 @@ void check_rank(std::string &rank) { } // namespace -// для сортировки вывода членов клана по рангам, когда оно через поля чара дергается +// , class SortRank { public: bool operator()(const CharData::shared_ptr ch1, const CharData::shared_ptr ch2); @@ -153,7 +153,7 @@ inline bool SortRank::operator()(const CharData::shared_ptr ch1, const CharData: Clan::ClanListType Clan::ClanList; -// поиск to_room в зонах клан-замков, выставляет за замок, если найдено +// to_room -, , RoomRnum Clan::CloseRent(RoomRnum to_room) { for (auto &clan : Clan::ClanList) if (world[to_room]->zone_rn == world[GetRoomRnum(clan->rent)]->zone_rn) @@ -165,7 +165,7 @@ int Clan::get_chest_room() { return this->chest_room; } -// проверяет находится ли чар в зоне чужого клана +// bool Clan::InEnemyZone(CharData *ch) { int zone = world[ch->in_room]->zone_rn; for (auto &clan : Clan::ClanList) @@ -191,7 +191,7 @@ Clan::Clan() : Clan::~Clan() { } -// релоад одного отдельного клана, абр. указывать на латинице! +// , . ! void Clan::ClanReload(const std::string &index) { std::ifstream file(LIB_CLANS "index"); if (!file.is_open()) { @@ -203,10 +203,10 @@ void Clan::ClanReload(const std::string &index) { while (file >> buffer) clanIndex.push_back(buffer); file.close(); - // ищем наш клан + // for (const auto &it : clanIndex) { if (it == index) { - // надо удалить наш клан из общего списка + // for (auto clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { std::string name_buffer = (*clan)->abbrev; CreateFileName(name_buffer); @@ -221,7 +221,7 @@ void Clan::ClanReload(const std::string &index) { } -// лоад отдельного клана из файла +// void Clan::ClanLoadSingle(const std::string &index) { std::string buffer; const auto tempClan = std::make_shared(); @@ -257,7 +257,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'Rent:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // зоны может и не быть + // if (!GetRoomRnum(rent)) { log("Room %d is no longer exist (%s).", rent, filename.c_str()); break; @@ -269,7 +269,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'OutRent:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // зоны может и не быть + // if (!GetRoomRnum(out_rent)) { log("Room %d is no longer exist (%s).", out_rent, filename.c_str()); break; @@ -281,7 +281,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'ChestRoom:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // зоны может и не быть + // if (!GetRoomRnum(chest_room)) { log("Room %d is no longer exist (%s).", chest_room, filename.c_str()); break; @@ -295,7 +295,7 @@ void Clan::ClanLoadSingle(const std::string &index) { break; } if (tmp_vnum != 0) { - // зоны может и не быть + // int ingr_chest_room_rnum = GetRoomRnum(tmp_vnum); if (ingr_chest_room_rnum > 0) { tempClan->ingr_chest_room_rnum_ = ingr_chest_room_rnum; @@ -317,7 +317,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'Guard:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // как и охранника + // if (GetMobRnum(guard) < 0) { log("Guard %d is no longer exist (%s).", guard, filename.c_str()); break; @@ -334,7 +334,7 @@ void Clan::ClanLoadSingle(const std::string &index) { unsigned long priv = 0; std::string buffer2; - // для верности воеводе проставим все привилегии, заодно сверим с файлом + // , if (!(stream >> buffer >> buffer2 >> priv)) { log("Error open 'Ranks' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; @@ -358,7 +358,7 @@ void Clan::ClanLoadSingle(const std::string &index) { check_rank(buffer2); tempClan->ranks.push_back(buffer); tempClan->ranks_female.push_back(buffer2); - // на случай уменьшения привилегий + // if (priv > tempClan->privileges[0].to_ulong()) priv = tempClan->privileges[0].to_ulong(); tempClan->privileges.push_back(std::bitset(priv)); @@ -403,9 +403,9 @@ void Clan::ClanLoadSingle(const std::string &index) { } } else if (buffer == "Bank:") { file >> tempClan->bank; - log("Clans in bank, file (%s) банк %ld.", filename.c_str(), tempClan->bank); + log("Clans in bank, file (%s) %ld.", filename.c_str(), tempClan->bank); if (tempClan->bank <= 0) { - log("Clan has 0 in bank, file (%s) возможно будет удален.", filename.c_str()); + log("Clan has 0 in bank, file (%s) .", filename.c_str()); } } else if (buffer == "Pk:") { file >> tempClan->pk; @@ -428,14 +428,14 @@ void Clan::ClanLoadSingle(const std::string &index) { long unique = 0; long long money = 0; long long exp = 0; - int exp_persent = 0; // заглушка + int exp_persent = 0; // long long clan_exp = 0; if (!(file >> unique >> money >> exp >> exp_persent >> clan_exp)) { log("Error open 'Owner:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // воеводы тоже уже может не быть + // const auto tempMember = std::make_shared(); tempMember->name = GetNameByUnique(unique); if (tempMember->name.empty()) { @@ -451,12 +451,12 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->owner = tempMember->name; } else if (buffer == "Members:") { - // параметры, критичные для мемберов, нужно загрузить до того как (ранги например) + // , , ( ) long unique = 0; unsigned rank = 0; long long money = 0; long long exp = 0; - int exp_persent = 0; // заглушка + int exp_persent = 0; // long long clan_exp = 0; std::getline(file, buffer, '~'); @@ -466,12 +466,12 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error read %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // на случай, если рангов стало меньше + // , if (!tempClan->ranks.empty() && rank > tempClan->ranks.size() - 1) { rank = static_cast(tempClan->ranks.size()) - 1; } - // удаленные персонажи просто игнорируются + // const auto tempMember = std::make_shared(); tempMember->name = GetNameByUnique(unique); if (tempMember->name.empty()) { @@ -490,8 +490,8 @@ void Clan::ClanLoadSingle(const std::string &index) { } file.close(); - // тут нужно проверить наличие критичных для клана полей - // т.к. загрузка без привязки к положению в файле - что-то может не проинициализироваться + // + // .. - - if (tempClan->abbrev.empty() || tempClan->name.empty() || tempClan->title.empty() || tempClan->rent == 0 || tempClan->guard == 0 || tempClan->out_rent == 0 @@ -499,12 +499,12 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Clan read fail: %s", filename.c_str()); return; } - // удаление неактивных кланов + // tempClan->exp_history.load(tempClan->get_file_abbrev()); - // иним на случай полной неактивности по итогам месяца, чтобы не было пропусков в списке + // , tempClan->exp_history.add_exp(0); if (tempClan->exp_history.need_destroy() && !tempClan->test_clan) { - // клан-банк на воеводу + // - if (tempClan->bank > 0) { Player t_victim; Player *victim = &t_victim; @@ -518,20 +518,20 @@ void Clan::ClanLoadSingle(const std::string &index) { } Boards::Static::clan_delete_message(tempClan->abbrev, tempClan->rent / 100); char smallbuf[128]; - sprintf(smallbuf, "неактивность"); + sprintf(smallbuf, ""); DestroyClan(tempClan, smallbuf); log("Clan deleted: %s", filename.c_str()); } - // по дефолту жен род для титула берем из основного + // if (tempClan->title_female.empty()) { tempClan->title_female = tempClan->title; } - // сундук по дефолту на ренте + // if (!tempClan->chest_room) tempClan->chest_room = tempClan->rent; - // чтобы не получилось потерь/прибавок экспы + // / if (tempClan->exp_buf) { tempClan->exp += tempClan->exp_buf; if (tempClan->exp < 0) @@ -539,7 +539,7 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->exp_buf = 0; } - // подгружаем пкл/дрл + // / std::ifstream pkFile((filename + ".pkl").c_str()); if (pkFile.is_open()) { int author = 0; @@ -558,14 +558,14 @@ void Clan::ClanLoadSingle(const std::string &index) { std::string victimName = GetNameByUnique(victim, true); name_convert(authorName); name_convert(victimName); - // если автора уже нет - сбросим УИД для верности + // - if (authorName.empty()) author = 0; - // если жертвы уже нет, или жертва выбилась в БОГИ - не грузим + // , - if (!victimName.empty()) { ClanPkPtr tempRecord(new ClanPk); tempRecord->author = author; - tempRecord->authorName = authorName.empty() ? "Того уж с нами нет" : authorName; + tempRecord->authorName = authorName.empty() ? " " : authorName; tempRecord->victimName = victimName; tempRecord->time = tempTime; tempRecord->text = buffer; @@ -577,7 +577,7 @@ void Clan::ClanLoadSingle(const std::string &index) { } pkFile.close(); } - //подгружаем кланстафф + // std::ifstream stuffFile((filename + ".stuff").c_str()); if (stuffFile.is_open()) { int i; @@ -602,7 +602,7 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->clanstuff.push_back(temp); } } - // лоад доп. параметров клана + // . tempClan->load_mod(); tempClan->pk_log.load(tempClan->get_file_abbrev()); tempClan->last_exp.load(tempClan->get_file_abbrev()); @@ -611,22 +611,22 @@ void Clan::ClanLoadSingle(const std::string &index) { if ((tempClan->bank <= 0) && (tempClan->m_members.size() > 0) && !tempClan->test_clan) { Boards::Static::clan_delete_message(tempClan->abbrev, tempClan->rent / 100); char smallbuf[128]; - sprintf(smallbuf, "банк_пуст"); + sprintf(smallbuf, "_"); DestroyClan(tempClan, smallbuf); log("Clan deleted bank 0: %s", filename.c_str()); } Clan::ClanList.push_back(tempClan); } -// лоад/релоад индекса и файлов кланов +// / void Clan::ClanLoad() { const bool reload = Clan::ClanList.empty() ? false : true; init_chest_rnum(); - // на случай релоада + // Clan::ClanList.clear(); - // файл со списком кланов + // std::ifstream file(LIB_CLANS "index"); if (!file.is_open()) { log("Error open file: %s! (%s %s %d)", LIB_CLANS "index", __FILE__, __func__, __LINE__); @@ -637,7 +637,7 @@ void Clan::ClanLoad() { while (file >> buffer) clanIndex.push_back(buffer); file.close(); - // собственно грузим кланы + // for (const auto &it : clanIndex) { Clan::ClanLoadSingle(it); } @@ -653,8 +653,8 @@ void Clan::ClanLoad() { HelpSystem::reload(HelpSystem::DYNAMIC); } - // на случай релоада кланов для выставления изменений игрокам онлайн - // лдшникам воткнется в другом месте, можно и тут чар-лист прогнать, варианты одинаково корявые + // + // , - , for (auto d = descriptor_list; d; d = d->next) { if (d->character) { Clan::SetClanData(d->character.get()); @@ -662,11 +662,11 @@ void Clan::ClanLoad() { } } -// вывод имму информации о кланах +// void Clan::HconShow(CharData *ch) { std::ostringstream buffer; buffer - << "Abbrev| Rent|OutRent| Chest|iChest| Guard|CreateDate| StoredExp| Bank|Items| Ing |DayTax|Lvl|Test|Распущена\r\n"; + << "Abbrev| Rent|OutRent| Chest|iChest| Guard|CreateDate| StoredExp| Bank|Items| Ing |DayTax|Lvl|Test|\r\n"; std::string_view show("{:<4}|{:<6}|{:<7}|{:<6}|{:<6}|{:<7}|{:<10}|{:<15}|{:<10}|{:<5}|{:<5}|{:<6}|{:<3}|{:<4}|{:<9}\r\n"); int total_day_tax = 0; @@ -683,14 +683,14 @@ void Clan::HconShow(CharData *ch) { GET_ROOM_VNUM(clan->get_ingr_chest_room_rnum()), clan->guard, timeBuf, clan->clan_exp, clan->bank, clan->chest_objcount, clan->ingr_chest_objcount_, cost, clan->clan_level, - (clan->test_clan ? "y" : "n"), ((clan->m_members.size() > 0) ? "Нет" : "Да")); + (clan->test_clan ? "y" : "n"), ((clan->m_members.size() > 0) ? "" : "")); } buffer << "Total day tax: " << total_day_tax << "\r\n"; SendMsgToChar(buffer.str().c_str(), ch); } -// формирование содержимого основного файла клана +// std::string Clan::build_clan_file() const { std::ostringstream file; @@ -756,7 +756,7 @@ std::string Clan::build_clan_file() const { return file.str(); } -// формирование содержимого пкл/дрл файла клана +// / std::string Clan::build_pk_file() const { std::ostringstream file; @@ -772,7 +772,7 @@ std::string Clan::build_pk_file() const { namespace { -// запись файла, если его содержимое изменилось с прошлого сохранения +// , bool write_if_changed(const std::string &filename, const std::string &contents, std::optional &cache) { if (cache.has_value() && *cache == contents) { @@ -797,7 +797,7 @@ void Clan::save_clan_file(const std::string &filename) const { write_if_changed(filename, build_clan_file(), saved_clan_file_); } -// сохранение кланов в файлы +// void Clan::ClanSave() { std::ofstream index(LIB_CLANS "index"); if (!index.is_open()) { @@ -806,13 +806,13 @@ void Clan::ClanSave() { } for (auto &clan : Clan::ClanList) { - // именем файла для клана служит его аббревиатура (английский и нижний регистр) + // ( ) std::string buffer = clan->abbrev; CreateFileName(buffer); - // в индексе дружина остается всегда, иначе она пропадет из игры после перезагрузки + // , index << buffer << "\n"; - // дружина без игроков считается распущенной: ее файлы больше не трогаем + // : if (clan->m_members.empty()) { continue; } @@ -826,30 +826,30 @@ void Clan::ClanSave() { return; } } - // основной файл клана + // clan->save_clan_file(filepath); - // пкл/дрл + // / write_if_changed(filepath + ".pkl", clan->build_pk_file(), clan->saved_pk_file_); } index.close(); } /** -* проставляем персонажу нужные поля если он клановый -* отсюда и далее для проверки на клановость пользуем CLAN() -* может быть вызвана с пустым дескриптором персонажа (stat file) +* +* CLAN() +* (stat file) */ void Clan::SetClanData(CharData *ch) { CLAN(ch).reset(); CLAN_MEMBER(ch).reset(); - // если правит свою дружину в олц, то радостно его выкидываем + // , if (ch->desc && ch->desc->state == EConState::kClanedit) { ch->desc->clan_olc.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar("Редактирование отменено из-за обновления ваших данных в дружине.\r\n", ch); + SendMsgToChar(" - .\r\n", ch); } - // если куда-то приписан, то дергаем сразу итераторы на клан и список членов + // - , for (auto &clan : Clan::ClanList) { const auto member = clan->m_members.find(ch->get_uid()); if (member != clan->m_members.end()) { @@ -858,13 +858,13 @@ void Clan::SetClanData(CharData *ch) { break; } } - // никуда не приписан + // if (!CLAN(ch)) { free(GET_CLAN_STATUS(ch)); GET_CLAN_STATUS(ch) = nullptr; return; } - // куда-то таки приписан + // - std::string buffer; if (IsMale(ch)) buffer = CLAN(ch)->ranks[CLAN_MEMBER(ch)->rank_num] + " " + CLAN(ch)->title; @@ -872,13 +872,13 @@ void Clan::SetClanData(CharData *ch) { buffer = CLAN(ch)->ranks_female[CLAN_MEMBER(ch)->rank_num] + " " + CLAN(ch)->title_female; GET_CLAN_STATUS(ch) = str_dup(buffer.c_str()); - // чтобы при выходе не смог приписаться опять за один ребут мада + // if (ch->desc) { ch->desc->clan_invite.reset(); } } -// проверка комнаты на принадлежность какому-либо замку +// - Clan::shared_ptr Clan::GetClanByRoom(RoomRnum room) { for (const auto &clan : ClanList) { if (world[room]->zone_rn == world[GetRoomRnum(clan->rent)]->zone_rn) { @@ -889,7 +889,7 @@ Clan::shared_ptr Clan::GetClanByRoom(RoomRnum room) { return nullptr; } -// может ли персонаж зайти в замок +// bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { const auto clan = GetClanByRoom(room); if (!clan @@ -912,7 +912,7 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { int _mode = mode ? kHousePortal : kHouseAtrium; switch (_mode) { - // вход через дверь - контролирует охранник + // - case kHouseAtrium: for (const auto mobs : world[ch->in_room]->people) { if (clan->guard == GET_MOB_VNUM(mobs) @@ -921,20 +921,20 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { } } - // охранника нет - свободный доступ + // - return true; - // телепортация + // case kHousePortal: if (!isMember) { - SendMsgToChar("Частная собственность - посторонним в ней делать нечего!\r\n", ch); + SendMsgToChar(" - !\r\n", ch); return false; } - // с временным флагом тоже курят + // if (NORENTABLE(ch)) { if (mode == kHouseAtrium) { - SendMsgToChar("Пускай сначала кровь с тебя стечет, а потом входи сколько угодно.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); } return false; } @@ -944,27 +944,27 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { return false; } -// в зависимости от доступности команды будут видны только нужные строки +// const char *HOUSE_FORMAT[] = { - " клан информация\r\n", - " клан принять имя звание\r\n", - " клан изгнать имя\r\n", - " клан привилегии\r\n", - " гдругам (текст)\r\n", - " политика <имя дружины> <нейтралитет|война|альянс>\r\n", - " дрновости <писать|очистить>\r\n", - " пклист|дрлист <добавить|удалить>\r\n", - " класть вещи в хранилище\r\n", - " брать вещи из хранилища\r\n", - " казна (снимать)\r\n", - " клан покинуть (выход из дружины)\r\n", - " клан сообщение (написание сообщения дружины)\r\n", - " клан налог <процент отдаваемых кун>\r\n", - " дрвече писать\r\n" + " \r\n", + " \r\n", + " \r\n", + " \r\n", + " ()\r\n", + " < > <||>\r\n", + " <|>\r\n", + " | <|>\r\n", + " \r\n", + " \r\n", + " ()\r\n", + " ( )\r\n", + " ( )\r\n", + " < >\r\n", + " \r\n" }; -// репутация +// int Clan::get_rep() { return this->reputation; } @@ -973,9 +973,9 @@ void Clan::set_rep(int rep) { this->reputation = rep; } -// house информация +// house void Clan::HouseInfo(CharData *ch) { - // думаю, вываливать список сортированный по уиду некрасиво, поэтому перебираем по рангам + // , , std::vector temp_list; for (const auto &it : m_members) { temp_list.push_back(it.second); @@ -987,7 +987,7 @@ void Clan::HouseInfo(CharData *ch) { }); std::ostringstream buffer; - buffer << "К замку приписаны:\r\n"; + buffer << " :\r\n"; size_t char_num = 0; std::string temp; @@ -1015,7 +1015,7 @@ void Clan::HouseInfo(CharData *ch) { char_num += it->name.size() + 1; } - buffer << "\r\nПривилегии:\r\n"; + buffer << "\r\n:\r\n"; int num = 0; for (auto it = ranks.begin(); it != ranks.end(); ++it, ++num) { @@ -1023,114 +1023,114 @@ void Clan::HouseInfo(CharData *ch) { for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (this->privileges[num][i]) { switch (i) { - case MAY_CLAN_INFO: buffer << " инфо"; + case MAY_CLAN_INFO: buffer << " "; break; - case MAY_CLAN_ADD: buffer << " принять"; + case MAY_CLAN_ADD: buffer << " "; break; - case MAY_CLAN_REMOVE: buffer << " изгнать"; + case MAY_CLAN_REMOVE: buffer << " "; break; - case MAY_CLAN_PRIVILEGES: buffer << " привилегии"; + case MAY_CLAN_PRIVILEGES: buffer << " "; break; - case MAY_CLAN_CHANNEL: buffer << " гд"; + case MAY_CLAN_CHANNEL: buffer << " "; break; - case MAY_CLAN_POLITICS: buffer << " политика"; + case MAY_CLAN_POLITICS: buffer << " "; break; - case MAY_CLAN_NEWS: buffer << " дрн"; + case MAY_CLAN_NEWS: buffer << " "; break; - case MAY_CLAN_PKLIST: buffer << " пкл"; + case MAY_CLAN_PKLIST: buffer << " "; break; - case MAY_CLAN_CHEST_PUT: buffer << " хран.полож"; + case MAY_CLAN_CHEST_PUT: buffer << " ."; break; - case MAY_CLAN_CHEST_TAKE: buffer << " хран.взять"; + case MAY_CLAN_CHEST_TAKE: buffer << " ."; break; - case MAY_CLAN_BANK: buffer << " казна"; + case MAY_CLAN_BANK: buffer << " "; break; - case MAY_CLAN_EXIT: buffer << " выход"; + case MAY_CLAN_EXIT: buffer << " "; break; - case MAY_CLAN_MOD: buffer << " сообщение дружины"; + case MAY_CLAN_MOD: buffer << " "; break; - case MAY_CLAN_TAX: buffer << " налог"; + case MAY_CLAN_TAX: buffer << " "; break; - case MAY_CLAN_BOARD: buffer << " дрвече"; + case MAY_CLAN_BOARD: buffer << " "; break; } } } buffer << "\r\n"; } - //инфа о экспе замка левеле замка, рейтинге замка и плюшках - buffer << "Ваш замок набрал " << this->clan_exp - << " очков опыта и имеет уровень " << this->clan_level << "\r\n" - << "Рейтинг вашего замка: " << this->exp - << " Это очень круто :), но ничего вам не дает.\r\n" - << "Ваша дружина имеет " << this->get_rep() << " очков репутации.\r\n" - << "В хранилище замка может храниться до " << this->ChestMaxObjects() + // , + buffer << " " << this->clan_exp + << " " << this->clan_level << "\r\n" + << " : " << this->exp + << " :), .\r\n" + << " " << this->get_rep() << " .\r\n" + << " " << this->ChestMaxObjects() << " " << grammar::GetDeclensionInNumber(this->ChestMaxObjects(), grammar::EWhat::kObjU) - << " с общим весом не более чем " << this->ChestMaxWeight() << "\r\n" - << "В хранилище ингредиентов может храниться до " << this->ingr_chest_max_objects() + << " " << this->ChestMaxWeight() << "\r\n" + << " " << this->ingr_chest_max_objects() << " " << grammar::GetDeclensionInNumber(this->ingr_chest_max_objects(), grammar::EWhat::kObjU) << ".\r\n"; - // инфа о банке и хранилище + // int cost = ChestTax(); int ingr_cost = ingr_chest_tax(); int options_tax = calculate_clan_tax(); int total_tax = cost + ingr_cost + options_tax; - buffer << "В хранилище вашей дружины " << this->chest_objcount << " " + buffer << " " << this->chest_objcount << " " << grammar::GetDeclensionInNumber(this->chest_objcount, grammar::EWhat::kObject) - << " общим весом в " << this->chest_weight - << " (" << cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str() << " в день).\r\n" - << "В хранилище ингредиентов " << ingr_chest_objcount_ << " " + << " " << this->chest_weight + << " (" << cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str() << " ).\r\n" + << " " << ingr_chest_objcount_ << " " << grammar::GetDeclensionInNumber(ingr_chest_objcount_, grammar::EWhat::kObject) - << " (" << ingr_cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(ingr_cost, grammar::ECase::kNom).c_str() << " в день).\r\n\r\n" - << "Состояние казны: " << this->bank << " " + << " (" << ingr_cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(ingr_cost, grammar::ECase::kNom).c_str() << " ).\r\n\r\n" + << " : " << this->bank << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(this->bank, grammar::ECase::kNom).c_str() << ".\r\n" - << "Расходы на инфраструктуру замка: " << options_tax << " " + << " : " << options_tax << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(options_tax, grammar::ECase::kNom).c_str() - << " в день, Общие расходы: " << total_tax << " " - << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(total_tax, grammar::ECase::kNom).c_str() << " в день.\r\n"; + << " , : " << total_tax << " " + << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(total_tax, grammar::ECase::kNom).c_str() << " .\r\n"; if (total_tax <= 0) { - buffer << "Ваших денег хватит на нереальное количество дней.\r\n"; + buffer << " .\r\n"; } else { - buffer << "Ваших денег хватит примерно на " + buffer << " " << bank / total_tax << " " << grammar::GetDeclensionInNumber(bank / total_tax, grammar::EWhat::kDay) << ".\r\n"; } - buffer << "Налог для ратников дружины: " << get_gold_tax_pct() << "%\r\n"; + buffer << " : " << get_gold_tax_pct() << "%\r\n"; SendMsgToChar(buffer.str(), ch); exp_history.show(ch); } -// клан принять, повлиять можно только на соклановцев ниже рангом +// , void Clan::HouseAdd(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); if (buffer2.empty()) { - SendMsgToChar("Укажите имя персонажа.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } long unique = GetUniqueByName(buffer2); if (!unique) { - SendMsgToChar("Неизвестный персонаж.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } std::string name = buffer2; native_text::capitalize_first(name); if (unique == ch->get_uid()) { - SendMsgToChar("Сам себя повысил, самому себе вынес благодарность?\r\n", ch); + SendMsgToChar(" , ?\r\n", ch); return; } - // изменение звания у членов дружины - // даже если они находятся оффлайн + // + // const auto it_member = this->m_members.find(unique); if (it_member != this->m_members.end()) { if (it_member->second->rank_num <= CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Вы можете менять звания только у нижестоящих членов дружины.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } @@ -1139,7 +1139,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { GetOneParam(buffer, buffer2); if (buffer2.empty()) { - buffer = "Укажите звание персонажа.\r\nДоступные положения: "; + buffer = " .\r\n : "; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) buffer += "'" + *it + "' "; buffer += "\r\n"; @@ -1157,21 +1157,21 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { editedChar = d->character; Clan::SetClanData(d->character.get()); SendMsgToChar(d->character.get(), - "%sВаше звание изменили, теперь вы %s.%s\r\n", + "%s , %s.%s\r\n", kColorWht, (*it).c_str(), kColorNrm); - sprintf(buf, "Звание в дружине изменено на %s", (*it).c_str()); + sprintf(buf, " %s", (*it).c_str()); AddKarma(d->character.get(), buf, ch->get_name().c_str()); } - // оповещение соклановцев о изменении звания + // for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && CLAN(d->character) && CLAN(d->character)->GetRent() == this->GetRent() && editedChar != d->character) { - SendMsgToChar(d->character.get(), "%s%s теперь %s.%s\r\n", + SendMsgToChar(d->character.get(), "%s%s %s.%s\r\n", kColorWht, it_member->second->name.c_str(), (*it).c_str(), kColorNrm); } } @@ -1180,7 +1180,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } } - buffer = "Неверное звание, доступные положения:\r\n"; + buffer = " , :\r\n"; for (std::vector::const_iterator it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) { buffer += "'" + *it + "' "; } @@ -1192,40 +1192,40 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { DescriptorData *d = DescriptorByUid(unique); if (!d || !sight::CanSee(ch, d->character)) { - SendMsgToChar("Этого персонажа нет в игре!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return; } if (d->character->IsFlagged(EPrf::kCoderinfo) || (GetRealLevel(d->character) >= kLvlGod)) { - SendMsgToChar("Вы не можете приписать этого игрока.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (CLAN(d->character) && CLAN(ch) != CLAN(d->character)) { - SendMsgToChar("Вы не можете приписать члена другой дружины.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (d->clan_invite) { if (d->clan_invite->clan == CLAN(ch)) { - SendMsgToChar("Вы уже пригласили этого игрока, ждите реакции.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); return; } else { - SendMsgToChar("Он уже приглашен в другую дружину, дождитесь его ответа и пригласите снова.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); return; } } GetOneParam(buffer, buffer2); - // чтобы учесть воеводу с 0 рангом во время приписки и не дать ему приписать еще 10 воевод + // 0 10 int rank = CLAN_MEMBER(ch)->rank_num; if (!rank) { ++rank; } if (buffer2.empty()) { - buffer = "Укажите звание персонажа.\r\nДоступные положения: "; + buffer = " .\r\n : "; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) buffer += "'" + *it + "' "; buffer += "\r\n"; @@ -1233,25 +1233,25 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { return; } - int temp_rank = rank; // дальше rank тоже мб использован в случае неверного выбора + int temp_rank = rank; // rank for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it, ++temp_rank) { if (CompareParam(buffer2, *it)) { - // не приписан - втыкаем ему приглашение и курим, пока не согласится + // - , std::shared_ptr temp_invite(new ClanInvite); temp_invite->clan = CLAN(ch); temp_invite->rank = temp_rank; - temp_invite->invite_name = "Игроком " + ch->get_name(); + temp_invite->invite_name = " " + ch->get_name(); d->clan_invite = temp_invite; buffer = kColorWht; - buffer += "$N приглашен$G в вашу дружину, статус - " + *it + "."; + buffer += "$N $G , - " + *it + "."; buffer += kColorNrm; - // оповещаем счастливца + // act(buffer.c_str(), false, ch, nullptr, d->character.get(), kToChar); buffer = kColorWht; - buffer += "Вы получили приглашение в дружину '" + this->name + "', статус - " + *it + ".\r\n" - + "Чтобы принять приглашение наберите 'клан согласен', для отказа 'клан отказать'.\r\n" - + "Никто не сможет послать вам новое приглашение до тех пор, пока вы не разберетесь с этим.\r\n" - + "Настоятельно рекомендуем ознакомиться с уставом дружины и разделом справки ПРАВИЛАДРУЖИНЫ.\r\n"; + buffer += " '" + this->name + "', - " + *it + ".\r\n" + + " ' ', ' '.\r\n" + + " , .\r\n" + + " .\r\n"; buffer += kColorNrm; SendMsgToChar(buffer, d->character.get()); @@ -1259,7 +1259,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } } - buffer = "Неверное звание, доступные положения:\r\n"; + buffer = " , :\r\n"; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) { buffer += "'" + *it + "' "; } @@ -1269,8 +1269,8 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } /** -* Отписывание персонажа от клана с оповещением всех заинтересованных сторон, -* выдворением за пределы замка и изменением ренты при необходимости. +* , +* . */ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { const auto it = m_members.find(key); @@ -1281,17 +1281,17 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { DescriptorData *k = DescriptorByUid(unique); if (k && k->character) { Clan::SetClanData(k->character.get()); - SendMsgToChar(k->character.get(), "Вас исключили из дружины '%s'!\r\n", this->name.c_str()); - sprintf(buf, "Исключен(а) из дружины '%s'", this->name.c_str()); + SendMsgToChar(k->character.get(), " '%s'!\r\n", this->name.c_str()); + sprintf(buf, "() '%s'", this->name.c_str()); AddKarma(k->character.get(), buf, reason); const auto clan = Clan::GetClanByRoom(k->character->in_room); if (clan) { char_from_room(k->character); - act("$n был$g выдворен$a за пределы замка!", true, k->character.get(), nullptr, nullptr, kToRoom); - SendMsgToChar("Вы были выдворены за пределы замка!\r\n", k->character.get()); + act("$n $g $a !", true, k->character.get(), nullptr, nullptr, kToRoom); + SendMsgToChar(" !\r\n", k->character.get()); PlaceCharToRoom(k->character.get(), GetRoomRnum(clan->out_rent)); sight::look_at_room(k->character.get(), GetRoomRnum(clan->out_rent)); - act("$n свалил$u с небес, выкрикивая какие-то ругательства!", + act("$n $u , - !", true, k->character.get(), nullptr, @@ -1302,7 +1302,7 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { Player p_vict; CharData *vict = &p_vict; if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) > -1) { - sprintf(buf, "Исключен(а) из дружины '%s'", this->name.c_str()); + sprintf(buf, "() '%s'", this->name.c_str()); AddKarma(vict, buf, reason); vict->save_char(); } @@ -1312,12 +1312,12 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->GetRent()) { native_text::capitalize_first(name); - SendMsgToChar(d->character.get(), "%s более не является членом вашей дружины.\r\n", name.c_str()); + SendMsgToChar(d->character.get(), "%s .\r\n", name.c_str()); } } } -// house изгнать (только званием ниже своего) +// house ( ) void Clan::HouseRemove(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -1325,17 +1325,17 @@ void Clan::HouseRemove(CharData *ch, std::string &buffer) { const auto it = this->m_members.find(unique); if (buffer2.empty()) { - SendMsgToChar("Укажите имя персонажа.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else if (!unique) { - SendMsgToChar("Неизвестный персонаж.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else if (unique == ch->get_uid()) { - SendMsgToChar("Выглядит довольно странно...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); } if (it == this->m_members.end()) { - SendMsgToChar("Он и так не приписан к вашей дружине.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else if (it->second->rank_num <= CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Вы можете исключить из дружины только персонажа со званием ниже вашего.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { char tmpstr[kMaxInputLength]; sprintf(tmpstr, "%s", GET_NAME(ch)); @@ -1345,8 +1345,8 @@ void Clan::HouseRemove(CharData *ch, std::string &buffer) { void Clan::HouseLeave(CharData *ch) { if (!CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Если вы хотите распустить свою дружину, то обращайтесь к Богам.\r\n" - "А если вам просто нечем заняться, то передайте воеводство и идите куда хотите...\r\n", ch); + SendMsgToChar(" , .\r\n" + " , ...\r\n", ch); return; } @@ -1354,12 +1354,12 @@ void Clan::HouseLeave(CharData *ch) { const auto it = this->m_members.find(member_id); if (it != this->m_members.end()) { char tmpstr[kMaxInputLength]; - sprintf(tmpstr, "самовыход"); + sprintf(tmpstr, ""); remove_member(member_id, tmpstr); } } -// удаляет объект из хранилищ клана +// int Clan::delete_obj(int vnum) { int num = 0; for (auto &clan : Clan::ClanList) { @@ -1378,11 +1378,11 @@ int Clan::delete_obj(int vnum) { return num; } -// Предмет рассыпался по таймеру внутри кланового хранилища. Savers сундука и ингр-храна -- -// dirty-трекеры (сохраняют только помеченные кланы), а распад сам по себе dirty не метил: -// файл не переписывался, и предмет возвращался из него после ребута. Метим владельца dirty и -// уменьшаем счётчик вещей (при take он уменьшается, при распаде забывали). Вызывается из -// obj_point_update, пока предмет ещё лежит в сундуке (до ExtractObjFromWorld). +// . Savers - -- +// dirty- ( ), dirty : +// , . dirty +// ޣ ( take , ). +// obj_point_update, ݣ ( ExtractObjFromWorld). void Clan::OnChestObjDecay(ObjData *j) { ObjData *container = j ? j->get_in_obj() : nullptr; if (!container) { @@ -1409,13 +1409,13 @@ void Clan::OnChestObjDecay(ObjData *j) { } } -// * hcontrol outcast имя - отписывание любого персонажа от дружины, кроме воеводы. +// * hcontrol outcast - , . void Clan::hcon_outcast(CharData *ch, std::string &buffer) { std::string name; GetOneParam(buffer, name); long member_uid = GetUniqueByName(name); if (!member_uid) { - SendMsgToChar("Неизвестный персонаж.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } for (const auto &clan : Clan::ClanList) { @@ -1423,30 +1423,30 @@ void Clan::hcon_outcast(CharData *ch, std::string &buffer) { if (it != clan->m_members.end()) { if (!it->second->rank_num) { SendMsgToChar(ch, - "Вы не можете исключить воеводу, для удаления дружины существует hcontrol destroy.\r\n"); + " , hcontrol destroy.\r\n"); return; } char tmpstr[kMaxInputLength]; - sprintf(tmpstr, "Богом %s", GET_NAME(ch)); + sprintf(tmpstr, " %s", GET_NAME(ch)); clan->remove_member(member_uid, tmpstr); native_text::capitalize_first(name); - SendMsgToChar(ch, "%s исключен(a) из дружины '%s'.\r\n", name.c_str(), clan->name.c_str()); + SendMsgToChar(ch, "%s (a) '%s'.\r\n", name.c_str(), clan->name.c_str()); return; } } - SendMsgToChar("Он и так не состоит ни в какой дружине.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } -// бог, текст, клан/альянс +// , , / void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { utils::Trim(text); - // на счет скобок я хз, по-моему так нагляднее все же в ифах, где условий штук 5, мож опять индентом пройтись? Ж) + // , - , 5, ? ) if (text.empty()) { - SendMsgToChar("Что вы хотите им сообщить?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } switch (subcmd) { - // большой БОГ говорит какой-то дружине + // - case kScmdChannel: for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character @@ -1456,17 +1456,17 @@ void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { && CLAN(d->character).get() == this && !AFF_FLAGGED(d->character, EAffect::kDeafness)) { SendMsgToChar(d->character.get(), - "%s ВАШЕЙ дружине: %s'%s'%s\r\n", + "%s : %s'%s'%s\r\n", GET_NAME(ch), kColorBoldRed, text.c_str(), kColorNrm); } } - SendMsgToChar(ch, "Вы дружине %s: %s'%s'.%s\r\n", this->abbrev.c_str(), kColorBoldRed, text.c_str(), kColorNrm); + SendMsgToChar(ch, " %s: %s'%s'.%s\r\n", this->abbrev.c_str(), kColorBoldRed, text.c_str(), kColorNrm); break; - // он же в канал союзников этой дружины, если они вообще есть + // , case kScmdAchannel: for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character @@ -1476,35 +1476,35 @@ void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { && d->character.get() != ch) { if (CheckPolitics(CLAN(d->character)->GetRent()) == kPoliticsAlliance || CLAN(d->character).get() == this) { - // проверка на альянс с обеих сторон, иначе это не альянс + // , if (CLAN(d->character).get() != this) { if (CLAN(d->character)->CheckPolitics(this->rent) == kPoliticsAlliance) { SendMsgToChar(d->character.get(), - "%s ВАШИМ СОЮЗНИКАМ: %s'%s'%s\r\n", + "%s : %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); } } - // первоначальному клану выдается всегда + // else { SendMsgToChar(d->character.get(), - "%s ВАШИМ СОЮЗНИКАМ: %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); + "%s : %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); } } } } - SendMsgToChar(ch, "Вы союзникам %s: %s'%s'.%s\r\n", abbrev.c_str(), kColorBoldGrn, text.c_str(), kColorNrm); + SendMsgToChar(ch, " %s: %s'%s'.%s\r\n", abbrev.c_str(), kColorBoldGrn, text.c_str(), kColorNrm); break; } } -// чар (или клановый бог), текст, клан/альянс +// ( ), , / void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { utils::Trim(text); if (text.empty()) { - SendMsgToChar("Что вы хотите сообщить?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } @@ -1514,15 +1514,15 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { } if (ch->IsFlagged(EPlrFlag::kDumbed)) { - SendMsgToChar("Вам запрещено обращаться к другим игрокам!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return; } switch (subcmd) { - // своей дружине + // case kScmdChannel: - // вспомнить - snprintf(buf, kMaxStringLength, "%s дружине: &R'%s'.&n\r\n", GET_NAME(ch), text.c_str()); + // + snprintf(buf, kMaxStringLength, "%s : &R'%s'.&n\r\n", GET_NAME(ch), text.c_str()); CLAN(ch)->add_remember(buf, Remember::CLAN); for (auto d = descriptor_list; d; d = d->next) { @@ -1534,7 +1534,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { && !ignores(d->character.get(), ch, EIgnore::kClan)) { snprintf(buf, kMaxStringLength, - "%s дружине: %s'%s'.%s\r\n", + "%s : %s'%s'.%s\r\n", GET_NAME(ch), kColorBoldRed, text.c_str(), @@ -1546,7 +1546,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { snprintf(buf, kMaxStringLength, - "Вы дружине: %s'%s'.%s\r\n", + " : %s'%s'.%s\r\n", kColorBoldRed, text.c_str(), kColorNrm); @@ -1555,10 +1555,10 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { break; - // союзникам + // case kScmdAchannel: - // вспомнить - snprintf(buf, kMaxStringLength, "%s союзникам: &G'%s'.&n\r\n", GET_NAME(ch), text.c_str()); + // + snprintf(buf, kMaxStringLength, "%s : &G'%s'.&n\r\n", GET_NAME(ch), text.c_str()); for (auto &clan : Clan::ClanList) { if ((CLAN(ch)->CheckPolitics(clan->GetRent()) == kPoliticsAlliance && clan->CheckPolitics(CLAN(ch)->GetRent()) == kPoliticsAlliance) @@ -1576,12 +1576,12 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { && !ignores(d->character.get(), ch, EIgnore::kAlliance)) { if (CLAN(ch)->CheckPolitics(CLAN(d->character)->GetRent()) == kPoliticsAlliance || CLAN(ch) == CLAN(d->character)) { - // проверка на альянс с обеих сторон, шоб не спамили друг другу на зло + // , if ((CLAN(d->character)->CheckPolitics(CLAN(ch)->GetRent()) == kPoliticsAlliance) || CLAN(ch) == CLAN(d->character)) { snprintf(buf, kMaxStringLength, - "%s союзникам: %s'%s'.%s\r\n", + "%s : %s'%s'.%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), @@ -1595,7 +1595,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { snprintf(buf, kMaxStringLength, - "Вы союзникам: %s'%s'.%s\r\n", + " : %s'%s'.%s\r\n", kColorBoldGrn, text.c_str(), kColorNrm); @@ -1606,8 +1606,8 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { } // switch } -// возвращает состояние политики clan по отношению к victim -// при отсутствии подразумевается нейтралитет +// clan victim +// int Clan::CheckPolitics(int victim) { auto it = politics.find(victim); if (it != politics.end()) { @@ -1616,8 +1616,8 @@ int Clan::CheckPolitics(int victim) { return kPoliticsNeutral; } -// выставляем клану политику(state) по отношению к victim -// нейтралитет означает просто удаление записи, если она была +// (state) victim +// , void Clan::SetPolitics(int victim, int state) { auto it = politics.find(victim); if (it == politics.end() && state == kPoliticsNeutral) @@ -1628,9 +1628,9 @@ void Clan::SetPolitics(int victim, int state) { politics[victim] = state; } -const char *politicsnames[] = {"Нейтралитет", "Война", "Альянс"}; +const char *politicsnames[] = {"", "", ""}; -// показывает, может ли чар писать в дрв +// , bool Clan::check_write_board(CharData *ch) { if (this->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_BOARD]) { return true; @@ -1655,12 +1655,12 @@ void Clan::SetPk(CharData *ch, std::string buffer) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", vnum); + SendMsgToChar(ch, " %d .\r\n", vnum); return; } clan->get()->change_pk_status(); - SendMsgToChar(ch, "Статус дружины изменен.\r\n"); + SendMsgToChar(ch, " .\r\n"); } bool char_to_pk_clan(CharData *ch) { @@ -1669,7 +1669,7 @@ bool char_to_pk_clan(CharData *ch) { return false; } -// выставляем политику с уведомлением другой дружины о войне или альянсе +// void Clan::ManagePolitics(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -1681,54 +1681,54 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { if (CompareParam(buffer2, (*vict)->abbrev)) break; if ((vict == Clan::ClanList.end()) || ((*vict)->m_members.size() == 0)) { - SendMsgToChar("Нет такой дружины.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (*vict == CLAN(ch)) { - SendMsgToChar("Менять политику по отношению к своей дружине? Что за бред...\r\n", ch); + SendMsgToChar(" ? ...\r\n", ch); return; } GetOneParam(buffer, buffer2); if (buffer2.empty()) - SendMsgToChar("Укажите действие: нейтралитет|война|альянс.\r\n", ch); - else if (CompareParam(buffer2, "нейтралитет")) { + SendMsgToChar(" : ||.\r\n", ch); + else if (CompareParam(buffer2, "")) { if (CheckPolitics((*vict)->rent) == kPoliticsNeutral) { SendMsgToChar(ch, - "Ваша дружина уже находится в состоянии нейтралитета с дружиной %s.\r\n", + " %s.\r\n", (*vict)->abbrev.c_str()); return; } SetPolitics((*vict)->rent, kPoliticsNeutral); SetWait(ch, 1, false); - // уведомляем обе дружины + // for (d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { - SendMsgToChar(d->character.get(), "%sДружина %s заключила с вашей дружиной нейтралитет!%s\r\n", + SendMsgToChar(d->character.get(), "%s %s !%s\r\n", kColorWht, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { - SendMsgToChar(d->character.get(), "%sВаша дружина заключила с дружиной %s нейтралитет!%s\r\n", + SendMsgToChar(d->character.get(), "%s %s !%s\r\n", kColorWht, (*vict)->abbrev.c_str(), kColorNrm); } } } - if (!ch->IsFlagged(EPrf::kPolitMode)) // а то сам может не увидеть нафик + if (!ch->IsFlagged(EPrf::kPolitMode)) // { - SendMsgToChar(ch, "%sВаша дружина заключила с дружиной %s нейтралитет!%s\r\n", + SendMsgToChar(ch, "%s %s !%s\r\n", kColorWht, (*vict)->abbrev.c_str(), kColorNrm); } - } else if (CompareParam(buffer2, "война")) { + } else if (CompareParam(buffer2, "")) { if (CheckPolitics((*vict)->rent) == kPoliticsWar) { - SendMsgToChar(ch, "Ваша дружина уже воюет с дружиной %s.\r\n", (*vict)->abbrev.c_str()); + SendMsgToChar(ch, " %s.\r\n", (*vict)->abbrev.c_str()); return; } SetPolitics((*vict)->rent, kPoliticsWar); SetWait(ch, 1, false); - // тож самое + // for (d = descriptor_list; d; d = d->next) { if (d->character @@ -1736,13 +1736,13 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { SendMsgToChar(d->character.get(), - "%sДружина %s объявила вашей дружине войну!%s\r\n", + "%s %s !%s\r\n", kColorBoldRed, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { SendMsgToChar(d->character.get(), - "%sВаша дружина объявила дружине %s войну!%s\r\n", + "%s %s !%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); @@ -1752,29 +1752,29 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { if (!ch->IsFlagged(EPrf::kPolitMode)) { SendMsgToChar(ch, - "%sВаша дружина объявила дружине %s войну!%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); + "%s %s !%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); } - } else if (CompareParam(buffer2, "альянс")) { + } else if (CompareParam(buffer2, "")) { if (CheckPolitics((*vict)->rent) == kPoliticsAlliance) { - SendMsgToChar(ch, "Ваша дружина уже в альянсе с дружиной %s.\r\n", (*vict)->abbrev.c_str()); + SendMsgToChar(ch, " %s.\r\n", (*vict)->abbrev.c_str()); return; } SetWait(ch, 1, false); SetPolitics((*vict)->rent, kPoliticsAlliance); - // тож самое + // for (d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { SendMsgToChar(d->character.get(), - "%sДружина %s заключила с вашей дружиной альянс!%s\r\n", + "%s %s !%s\r\n", kColorGrn, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { SendMsgToChar(d->character.get(), - "%sВаша дружина заключила альянс с дружиной %s!%s\r\n", + "%s %s!%s\r\n", kColorGrn, (*vict)->abbrev.c_str(), kColorNrm); @@ -1783,26 +1783,26 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { } if (!ch->IsFlagged(EPrf::kPolitMode)) { - SendMsgToChar(ch, "%sВаша дружина заключила альянс с дружиной %s!%s\r\n", + SendMsgToChar(ch, "%s %s!%s\r\n", kColorGrn, (*vict)->abbrev.c_str(), kColorNrm); } } } const char *HCONTROL_FORMAT = - "Формат: hcontrol build \r\n" + ": hcontrol build \r\n" " hcontrol show\r\n" - " hcontrol destroy - удалить дружину\r\n" - " hcontrol outcast - исключить игрока из дружины\r\n" + " hcontrol destroy - \r\n" + " hcontrol outcast - \r\n" " hcontrol save\r\n" - " hcontrol title <аббревиатура для муж рода> <аббревиатура для жен рода>\r\n" - " hcontrol rank <старое звание муж рода> <звание для муж рода> <звание для жен рода>\r\n" - " hcontrol owner <имя нового воеводы>\r\n" - " hcontrol ingr \r\n" - " hcontrol exphitory <число месяцев>\r\n" - " hcontrol pk \r\n"; - -// * hcontrol title - изменение аббревиатуры клана в титуле персонажа. + " hcontrol title < > < >\r\n" + " hcontrol rank < > < > < >\r\n" + " hcontrol owner < >\r\n" + " hcontrol ingr \r\n" + " hcontrol exphitory < >\r\n" + " hcontrol pk \r\n"; + +// * hcontrol title - . void Clan::hcontrol_title(CharData *ch, std::string &text) { std::string buffer; @@ -1814,7 +1814,7 @@ void Clan::hcontrol_title(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } @@ -1838,10 +1838,10 @@ void Clan::hcontrol_title(CharData *ch, std::string &text) { } } - SendMsgToChar("Сделано.\r\n", ch); + SendMsgToChar(".\r\n", ch); } -// * hcontrol rank - изменение кланового звания персонажа в титуле. +// * hcontrol rank - . void Clan::hcontrol_rank(CharData *ch, std::string &text) { std::string buffer; @@ -1853,7 +1853,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } @@ -1866,7 +1866,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { return; } if (rank_male.size() > MAX_RANK_LENGHT || rank_female.size() > MAX_RANK_LENGHT) { - SendMsgToChar(ch, "Звание не должно быть длиннее %d символов.\r\n", MAX_RANK_LENGHT); + SendMsgToChar(ch, " %d .\r\n", MAX_RANK_LENGHT); return; } @@ -1883,7 +1883,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { } } catch (...) { - SendMsgToChar(ch, "Ошибка в званиях дружины.\r\n"); + SendMsgToChar(ch, " .\r\n"); } Clan::ClanSave(); @@ -1894,12 +1894,12 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { Clan::SetClanData(d->character.get()); } } - SendMsgToChar("Сделано.\r\n", ch); + SendMsgToChar(".\r\n", ch); } /** -* Распечатка списка кланов с историей экспы за указанное кол-во месяцев. -* \param text - число последних месяцев, если пустая строка - 0 (только текущий месяц). +* - . +* \param text - , - 0 ( ). */ void Clan::hcontrol_exphistory(CharData *ch, std::string &text) { if (!ch->IsFlagged(EPrf::kCoderinfo)) { @@ -1914,7 +1914,7 @@ void Clan::hcontrol_exphistory(CharData *ch, std::string &text) { month = std::stoi(text, nullptr, 10); } catch (const std::invalid_argument &) { - SendMsgToChar(ch, "Неверный формат (\"hcontrol exp <кол-во последних месяцев>\")."); + SendMsgToChar(ch, " (\"hcontrol exp <- >\")."); return; } } @@ -1935,7 +1935,7 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { return; } - // <клан> <комната> - buffer2, text + // <> <> - buffer2, text std::string buffer2; GetOneParam(text, buffer2); utils::Trim(text); @@ -1947,14 +1947,14 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { vnumum = std::stol(text, nullptr, 10); } catch (const std::invalid_argument &) { - SendMsgToChar(ch, "Неверный формат (\"hcontrol ingr <клан-рента> <комната хранилища>\")."); + SendMsgToChar(ch, " (\"hcontrol ingr <-> < >\")."); return; } } int room_rnum = GetRoomRnum(vnumum); if (room_rnum <= 0) { - SendMsgToChar(ch, "Комнаты %d не существует.", vnumum); + SendMsgToChar(ch, " %d .", vnumum); return; } @@ -1965,16 +1965,16 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { } } if (i == iend) { - SendMsgToChar(ch, "Клана %d не существует.", clan_vnum); + SendMsgToChar(ch, " %d .", clan_vnum); return; } if ((*i)->GetRent() / 100 != vnumum / 100) { - SendMsgToChar(ch, "Комната %d находится вне зоны замка %d.", vnumum, (*i)->GetRent()); + SendMsgToChar(ch, " %d %d.", vnumum, (*i)->GetRent()); return; } bool chest_moved = false; - // хран под ингры уже был + // if ((*i)->ingr_chest_active()) { for (auto chest : world[(*i)->get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { @@ -1994,120 +1994,120 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { if (chest) { PlaceObjToRoom(chest.get(), (*i)->get_ingr_chest_room_rnum()); } - SendMsgToChar("Хранилище установлено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { - SendMsgToChar("Хранилище перенесено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } -// создание дружины (hcontrol build) +// (hcontrol build) void Clan::HcontrolBuild(CharData *ch, std::string &buffer) { - // парсим все параметры, чтобы сразу проверить на ввод всех полей + // , std::string buffer2; - // рента + // GetOneParam(buffer, buffer2); int rent = atoi(buffer2.c_str()); - // рента вне замка + // GetOneParam(buffer, buffer2); int out_rent = atoi(buffer2.c_str()); - // охранник + // GetOneParam(buffer, buffer2); MobVnum guard = atoi(buffer2.c_str()); - // воевода + // std::string owner; GetOneParam(buffer, owner); - // аббревиатура + // std::string abbrev; GetOneParam(buffer, abbrev); - // название клана + // std::string name = buffer; - // тут проверяем наличие все этого дела + // if (name.empty()) { SendMsgToChar(HCONTROL_FORMAT, ch); return; } if (!GetRoomRnum(rent)) { - SendMsgToChar(ch, "Комнаты %d не существует.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } if (!GetRoomRnum(out_rent)) { - SendMsgToChar(ch, "Комнаты %d не существует.\r\n", out_rent); + SendMsgToChar(ch, " %d .\r\n", out_rent); return; } if (GetMobRnum(guard) < 0) { - SendMsgToChar(ch, "Моба %d не существует.\r\n", guard); + SendMsgToChar(ch, " %d .\r\n", guard); return; } long unique = 0; if (!(unique = GetUniqueByName(owner))) { - SendMsgToChar(ch, "Персонажа %s не существует.\r\n", owner.c_str()); + SendMsgToChar(ch, " %s .\r\n", owner.c_str()); return; } - // а тут - не занят ли параметр другим кланом + // - for (const auto &clan : Clan::ClanList) { if (clan->rent == rent) { - SendMsgToChar(ch, "Комната %d уже занята другой дружиной.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } if (clan->guard == guard) { - SendMsgToChar(ch, "Охранник %d уже занят другой дружиной.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } const auto it = clan->m_members.find(unique); if (it != clan->m_members.end()) { - SendMsgToChar(ch, "%s уже приписан к дружине %s.\r\n", owner.c_str(), clan->abbrev.c_str()); + SendMsgToChar(ch, "%s %s.\r\n", owner.c_str(), clan->abbrev.c_str()); return; } if (CompareParam(clan->abbrev, abbrev, 1)) { - SendMsgToChar(ch, "Аббревиатура '%s' уже занята другой дружиной.\r\n", abbrev.c_str()); + SendMsgToChar(ch, " '%s' .\r\n", abbrev.c_str()); return; } if (CompareParam(clan->name, name, 1)) { - SendMsgToChar(ch, "Имя '%s' уже занято другой дружиной.\r\n", name.c_str()); + SendMsgToChar(ch, " '%s' .\r\n", name.c_str()); return; } } - // собственно клан + // const auto tempClan = std::make_shared(); tempClan->rent = rent; tempClan->out_rent = out_rent; tempClan->chest_room = rent; tempClan->guard = guard; - // пишем воеводу + // native_text::capitalize_first(owner); tempClan->owner = owner; const auto tempMember = std::make_shared(); tempMember->name = owner; tempClan->m_members.set(unique, tempMember); - // названия + // tempClan->name = name; tempClan->builtOn = time(nullptr); tempClan->title_female = tempClan->title = tempClan->abbrev = abbrev; - // ранги + // const char - *ranks[] = {"воевода", "боярин", "десятник", "храбр", "кметь", "гридень", "муж", "вой", "отрок", "гость"}; - // женский род пока тоже самое, а то воплей будет... + *ranks[] = {"", "", "", "", "", "", "", "", "", ""}; + // , ... const char *ranks_female[] = - {"воевода", "боярин", "десятник", "храбр", "кметь", "гридень", "муж", "вой", "отрок", "гость"}; + {"", "", "", "", "", "", "", "", "", ""}; std::vector temp_ranks(ranks, ranks + 10); std::vector temp_ranks_female(ranks_female, ranks_female + 10); tempClan->ranks = temp_ranks; tempClan->bank = 1000; tempClan->ranks_female = temp_ranks_female; - // привилегии + // for (std::vector::const_iterator it = tempClan->ranks.begin(); it != tempClan->ranks.end(); ++it) { tempClan->privileges.push_back(std::bitset()); } - // воеводе проставим все привилегии + // for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { tempClan->privileges[0].set(i); } - // залоадим сразу хранилище + // const auto chest = world_objects.create_from_prototype_by_vnum(CLAN_CHEST_VNUM); if (chest) { PlaceObjToRoom(chest.get(), GetRoomRnum(tempClan->chest_room)); @@ -2117,17 +2117,17 @@ void Clan::HcontrolBuild(CharData *ch, std::string &buffer) { Clan::ClanSave(); Boards::Static::ClanInit(); - // уведомляем счастливых воеводу и имма + // DescriptorData *d = DescriptorByUid(unique); if (d) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), "Вы стали хозяином нового замка. Добро пожаловать!\r\n"); + SendMsgToChar(d->character.get(), " . !\r\n"); } - SendMsgToChar(ch, "Дружина '%s' создана!\r\n", abbrev.c_str()); + SendMsgToChar(ch, " '%s' !\r\n", abbrev.c_str()); } -// удаление дружины (hcontrol destroy) +// (hcontrol destroy) void Clan::HcontrolDestroy(CharData *ch, std::string &buffer) { int rent = atoi(buffer.c_str()); @@ -2139,14 +2139,14 @@ void Clan::HcontrolDestroy(CharData *ch, std::string &buffer) { } if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); + SendMsgToChar(ch, " %d .\r\n", rent); return; } char smallbuf[128]; - sprintf(smallbuf, "распущена_богами"); + sprintf(smallbuf, "_"); DestroyClan(*clan, smallbuf); - SendMsgToChar("Дружина распущена.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } void Clan::fix_clan_members_load_room(Clan::shared_ptr clan) { @@ -2184,7 +2184,7 @@ void Clan::fix_clan_members_load_room(Clan::shared_ptr clan) { delete cbuf; } - sprintf(buf, "CLAN: Роспуск, удаляю игрока %s [%s]", player_table[i].name().c_str(), clan->name.c_str()); + sprintf(buf, "CLAN: , %s [%s]", player_table[i].name().c_str(), clan->name.c_str()); log("%s", buf); } } @@ -2193,7 +2193,7 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { fix_clan_members_load_room(clan); const auto members = clan->m_members; // copy members - for (const auto &clanVictim : Clan::ClanList) { //для всех кланов выставляем нейтралитет (тупо удаляем) + for (const auto &clanVictim : Clan::ClanList) { // ( ) if (clan->rent != clanVictim->rent) clanVictim->SetPolitics(clan->rent, kPoliticsNeutral); } @@ -2220,7 +2220,7 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { break; } } - // пуржим ингры, если есть + // , clan->purge_ingr_chest(); clan->exp_history.fulldelete(); clan->last_exp.fulldelete(); @@ -2231,12 +2231,12 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { DescriptorData *d = DescriptorByUid(it.first); if (d) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), "Ваша дружина распущена. Желаем удачи!\r\n"); + SendMsgToChar(d->character.get(), " . !\r\n"); } Player p_vict; CharData *vict = &p_vict; if (LoadPlayerCharacter(it.second->name.c_str(), vict, ELoadCharFlags::kFindId) > -1) { - sprintf(smallbuf, "Исключен(а) из дружины '%s'. (распущена)", clan->get_abbrev().c_str()); + sprintf(smallbuf, "() '%s'. ()", clan->get_abbrev().c_str()); AddKarma(vict, smallbuf, reason); vict->save_char(); } @@ -2249,15 +2249,15 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { const char *CLAN_PKLIST_FORMAT[] = { - "Формат: пклист|дрлист (все)\r\n" - " пклист|дрлист имя (все)\r\n", - " пклист|дрлист добавить имя причина\r\n" - " пклист|дрлист удалить имя|все\r\n" + ": | ()\r\n" + " | ()\r\n", + " | \r\n" + " | |\r\n" }; /** -* Для клановых пкл/дрл - не показываются чары в состоянии дисконета, кроме находящихся в бд, -* т.е. они стоят где-то в мире полюбому, все остальные состояния считаются как онлайн. +* / - , , +* .. - , . */ bool check_online_state(long uid) { for (const auto &tch : character_list) { @@ -2272,7 +2272,7 @@ bool check_online_state(long uid) { return false; } -// * Распечатка пкл/дрл с учетом режима 'пкфортмат'. +// * / ''. void print_pkl(CharData *ch, std::ostringstream &stream, ClanPkList::const_iterator &it) { static char timeBuf[11]; @@ -2285,14 +2285,14 @@ void print_pkl(CharData *ch, std::ostringstream &stream, ClanPkList::const_itera } } -// кладем в сундук (при наличии привилегии) -// если предмет - деньги, то автоматом идут в клан-казну, контейнеры только пустые +// ( ) +// - , -, bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { const bool prohibited = ch->IsNpc() || !CLAN(ch) || GetRoomRnum(CLAN(ch)->chest_room) != ch->in_room || !CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHEST_PUT]; if (prohibited) { - SendMsgToChar("Не имеете таких правов!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return false; } @@ -2302,11 +2302,11 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); currencies::AddHand(*ch, currencies::kGold, gold); - SendMsgToChar(ch, "Вам это не положено! Вы вновь обрели %ld %s.\r\n", + SendMsgToChar(ch, " ! %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); return true; } - // здесь и далее: в случае переполнения - кладем сколько можем, остальное возвращаем чару + // : - , if ((CLAN(ch)->bank + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; CLAN(ch)->bank += over; @@ -2316,7 +2316,7 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); SendMsgToChar(ch, - "Вам удалось вложить в казну дружины только %ld %s.\r\n", + " %ld %s.\r\n", over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); return true; @@ -2325,24 +2325,24 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { CLAN(ch)->m_members.add_money(ch->get_uid(), gold); RemoveObjFromChar(obj); ExtractObjFromWorld(obj); - SendMsgToChar(ch, "Вы вложили в казну дружины %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); + SendMsgToChar(ch, " %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); } else if (obj->has_flag(EObjFlag::kNodrop) || obj->is_unrentable() || obj->has_flag(EObjFlag::kNamed) || obj->get_owner()) { - act("Неведомая сила помешала положить $o3 в $O3.", false, ch, obj, chest, kToChar); + act(" $o3 $O3.", false, ch, obj, chest, kToChar); } else if (obj->get_type() == EObjType::kContainer && obj->get_contains()) { - act("В $o5 что-то лежит.", false, ch, obj, nullptr, kToChar); + act(" $o5 - .", false, ch, obj, nullptr, kToChar); } else if (SetSystem::is_norent_set(ch, obj, true) && obj->has_flag(EObjFlag::kNotOneInClanChest)) { - snprintf(buf, kMaxStringLength, "%s - требуется две и более вещи из набора.\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); + snprintf(buf, kMaxStringLength, "%s - .\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); SendMsgToChar(utils::CAP(buf), ch); return false; } else { if ((chest->get_weight() + obj->get_weight()) > CLAN(ch)->ChestMaxWeight() || CLAN(ch)->chest_objcount == CLAN(ch)->ChestMaxObjects()) { - act("Вы попытались запихнуть $o3 в $O3, но не смогли - там просто нет места.", + act(" $o3 $O3, - .", false, ch, obj, @@ -2355,11 +2355,11 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { PlaceObjIntoObj(obj, chest); ObjSaveSync::add(ch->get_uid(), CLAN(ch)->GetRent(), ObjSaveSync::CLAN_SAVE); - CLAN(ch)->chest_log.add(fmt::format("{} сдал{} {}{}\r\n", + CLAN(ch)->chest_log.add(fmt::format("{} {} {}{}\r\n", ch->get_name(), grammar::SexEnding((ch)->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch)))); - // канал хранилища + // for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying @@ -2367,9 +2367,9 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { && CLAN(d->character) && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kTakeMode)) { - // Цвет повторяем после имени предмета: у именных вещей оно само цветное и - // закрывается сбросом (&n), из-за чего хвост фразы терял цвет. - SendMsgToChar(fmt::format("[Хранилище]: &R'{} сдал{} {}{}&R.'&n\r\n", + // : + // (&n), - . + SendMsgToChar(fmt::format("[]: &R'{} {} {}{}&R.'&n\r\n", GET_NAME(ch), grammar::SexEnding(ch->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch))), @@ -2378,7 +2378,7 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { } if (!ch->IsFlagged(EPrf::kDecayMode)) { - act("Вы положили $o3 в $O3.", false, ch, obj, chest, kToChar); + act(" $o3 $O3.", false, ch, obj, chest, kToChar); } CLAN(ch)->chest_objcount++; @@ -2388,13 +2388,13 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { return true; } -// берем из клан-сундука (при наличии привилегии) +// - ( ) bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || GetRoomRnum(CLAN(ch)->chest_room) != ch->in_room || !CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHEST_TAKE]) { - SendMsgToChar("Не имеете таких правов!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return false; } @@ -2403,12 +2403,12 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { ObjSaveSync::add(ch->get_uid(), CLAN(ch)->GetRent(), ObjSaveSync::CLAN_SAVE); if (obj->get_carried_by() == ch) { - std::string log_text = fmt::format("{} забрал{} {}{}\r\n", + std::string log_text = fmt::format("{} {} {}{}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch))); CLAN(ch)->chest_log.add(log_text); - // канал хранилища + // for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying @@ -2416,7 +2416,7 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { && CLAN(d->character) && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kTakeMode)) { - SendMsgToChar(fmt::format("[Хранилище]: &R'{} забрал{} {}{}&R.'&n\r\n", + SendMsgToChar(fmt::format("[]: &R'{} {} {}{}&R.'&n\r\n", GET_NAME(ch), grammar::SexEnding(ch->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(d->character))), @@ -2425,7 +2425,7 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { } if (!ch->IsFlagged(EPrf::kTakeMode)) { - act("Вы взяли $o3 из $O1.", false, ch, obj, chest, kToChar); + act(" $o3 $O1.", false, ch, obj, chest, kToChar); } CLAN(ch)->chest_objcount--; GlobalObjects::chest_saver().mark_dirty(CLAN(ch).get()); @@ -2437,8 +2437,8 @@ void Clan::save_chest() { log("Save obj: %s", this->abbrev.c_str()); ObjSaveSync::check(this->GetRent(), ObjSaveSync::CLAN_SAVE); - // Имя каталога дружины строится из байтов KOI8-R, как и до миграции: под UTF-8 байтовый - // AtoL резал русскую букву пополам, и добро дружины уезжало в каталог с другим именем + // KOI8-R, : UTF-8 + // AtoL , // (issue #3681). std::string buffer = this->abbrev; CreateFileName(buffer); @@ -2464,22 +2464,22 @@ void Clan::save_chest() { } } -// сохраняем все сундуки в файлы -// пользует write_one_object (мне кажется это разуменее, чем плодить свои форматы везде и потом -// заниматься с ними сексом при изменении параметров на шмотках) +// +// write_one_object ( , +// ) void Clan::SaveChestAll() { GlobalObjects::chest_saver().run(); } -// чтение файлов клановых сундуков -// пользует read_one_object_new для чтения шмоток плееров в ренте +// +// read_one_object_new void Clan::ChestLoad() { ObjData *temp, *obj_next; - // TODO: при сильном желании тут можно пробегать все зоны замков или вообще все зоны/предметы и пуржить все chest - // предметы и их содержимое, на случай релоада кланов и изменения комнаты с хранилищем (чтобы в маде не пуржить руками) + // TODO: / chest + // , ( ) - // на случай релоада - чистим перед этим все что было в сундуках + // - for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { for (auto chest : world[GetRoomRnum((*clan)->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { @@ -2504,7 +2504,7 @@ void Clan::ChestLoad() { CreateFileName(buffer); std::string filename = LIB_CLANS + buffer + "/" + buffer + ".obj"; - //лоадим сундук. в зонах его лоадить не нужно. + // . . const auto chest = world_objects.create_from_prototype_by_vnum(CLAN_CHEST_VNUM); if (chest) { PlaceObjToRoom(chest.get(), GetRoomRnum((*clan)->chest_room)); @@ -2550,7 +2550,7 @@ void Clan::ChestLoad() { } if (!NamedStuff::check_named(nullptr, - obj.get()))//Если объект есть в списке именных то ему нечего делать в хранилище + obj.get()))// { PlaceObjIntoObj(obj.get(), chest.get()); } else { @@ -2561,14 +2561,14 @@ void Clan::ChestLoad() { } } -// смотрим чего в сундуках и берем из казны за ренту +// void Clan::ChestUpdate() { double i, cost; for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { cost = (*clan)->ChestTax() + (*clan)->ingr_chest_tax(); cost += (*clan)->calculate_clan_tax(); - // расчет и снимание за ренту (целой части по возможности) + // ( ) cost = (cost * kChestUpdatePeriod) / (60 * 24); (*clan)->bankBuffer += cost; @@ -2577,8 +2577,8 @@ void Clan::ChestUpdate() { (*clan)->bank -= static_cast(i); (*clan)->bankBuffer -= i; } - // при нулевом счете все шмотки в сундуке пуржим - // TODO: а тут придется опять искать, ибо выше сундук уже похерен, надо фиксить + // + // TODO: , , if ((*clan)->bank < 0) { (*clan)->bank = 0; ObjData *temp, *obj_next; @@ -2592,14 +2592,14 @@ void Clan::ChestUpdate() { break; } } - // пуржим ингры, если есть + // , (*clan)->purge_ingr_chest(); GlobalObjects::chest_saver().mark_dirty(clan->get()); } } } -// * Запись сообщения дружины в файл и поле клана. +// * . void Clan::write_mod(const std::string &arg) { std::string abbrev = this->get_abbrev(); CreateFileName(abbrev); @@ -2616,7 +2616,7 @@ void Clan::write_mod(const std::string &arg) { mod_text = arg; } -// * Распечатка сообщения дружины чару при входе. +// * . bool Clan::print_mod(CharData *ch) const { if (!mod_text.empty()) { SendMsgToChar(ch, "\r\n%s%s%s\r\n", @@ -2627,7 +2627,7 @@ bool Clan::print_mod(CharData *ch) const { return false; } -// * Загрузка сообщения дружины. +// * . void Clan::load_mod() { std::string abbrev = this->get_file_abbrev(); std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".mod"; @@ -2643,8 +2643,8 @@ void Clan::load_mod() { out.str().swap(mod_text); } -// казна дружины... команды теже самые с приставкой 'казна' в начале -// смотреть/вкладывать могут все, снимать по привилегии, висит на стандартных банкирах +// ... '' +// / , , bool Clan::BankManage(CharData *ch, char *arg) { if (ch->IsNpc() || !CLAN(ch) || GetRealLevel(ch) >= kLvlImmortal) return false; @@ -2652,13 +2652,13 @@ bool Clan::BankManage(CharData *ch, char *arg) { std::string buffer = arg, buffer2; GetOneParam(buffer, buffer2); - if (CompareParam(buffer2, "баланс") || CompareParam(buffer2, "balance")) { + if (CompareParam(buffer2, "") || CompareParam(buffer2, "balance")) { SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kClanBalance)), fmt::arg("amount", CLAN(ch)->bank), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(CLAN(ch)->bank, grammar::ECase::kNom).c_str())) + "\r\n", ch); return true; - } else if (CompareParam(buffer2, "вложить") || CompareParam(buffer2, "deposit")) { + } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "deposit")) { GetOneParam(buffer, buffer2); long gold = 0; try { @@ -2675,7 +2675,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { catch (const std::invalid_argument &) { SendMsgToChar(specials::BankMsg(specials::EBankMsg::kClanDepositFormat) + "\r\n", ch); } - // на случай переполнения казны + // if ((CLAN(ch)->bank + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; CLAN(ch)->bank += over; @@ -2685,7 +2685,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { fmt::arg("amount", over), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, nullptr, nullptr, kToRoom); - std::string log_text = fmt::format("{} вложил%s в казну {} {}\r\n", + std::string log_text = fmt::format("{} %s {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2696,14 +2696,14 @@ bool Clan::BankManage(CharData *ch, char *arg) { CLAN(ch)->m_members.add_money(ch->get_uid(), gold); SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kDeposited)), fmt::arg("amount", gold), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, 0, nullptr, kToRoom); - std::string log_text = fmt::format("{} вложил%s в казну {} {}\r\n", + std::string log_text = fmt::format("{} %s {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); return true; - } else if (CompareParam(buffer2, "получить") || CompareParam(buffer2, "withdraw")) { + } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "withdraw")) { if (!CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_BANK]) { SendMsgToChar(specials::BankMsg(specials::EBankMsg::kClanNoWithdraw) + "\r\n", ch); return true; @@ -2720,7 +2720,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { return true; } - // на случай переполнения персонажа + // if ((currencies::GetHand(*ch, currencies::kGold) + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; currencies::AddHand(*ch, currencies::kGold, over); @@ -2730,7 +2730,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { fmt::arg("amount", over), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, 0, nullptr, kToRoom); - std::string log_text = fmt::format("{} получил%s из казны {} {}\r\n", + std::string log_text = fmt::format("{} %s {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2741,7 +2741,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { currencies::AddHand(*ch, currencies::kGold, gold); SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kWithdrawn)), fmt::arg("amount", gold), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, nullptr, nullptr, kToRoom); - std::string log_text = fmt::format("{} получил%s из казны {} {}\r\n", + std::string log_text = fmt::format("{} %s {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2761,30 +2761,30 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // есть вариант, что за время в олц в клане изменят кол-во званий + // , - if (d->clan_olc->clan->privileges.size() != d->clan_olc->privileges.size()) { SendMsgToChar( - "Во время редактирования привилегий в вашей дружине было изменено количество званий.\r\n" - "Во избежание несоответствий зайдите в меню еще раз.\r\n" - "Редактирование отменено.\r\n", + " .\r\n" + " .\r\n" + " .\r\n", d->character.get()); d->clan_olc.reset(); d->state = EConState::kPlaying; return; } - SendMsgToChar("Вы желаете сохранить изменения? Y(Д)/N(Н) : ", d->character.get()); + SendMsgToChar(" ? Y()/N() : ", d->character.get()); d->clan_olc->mode = CLAN_SAVE_MENU; return; default: if (!*arg || !num) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } - // тут парсим циферки уже (меню начинается с 1 + ранг ниже себя) + // ( 1 + ) unsigned choise = num + CLAN_MEMBER(d->character)->rank_num; if (choise >= d->clan_olc->clan->ranks.size()) { unsigned i = choise - static_cast(d->clan_olc->clan->ranks.size()); @@ -2812,14 +2812,14 @@ void Clan::Manage(DescriptorData *d, const char *arg) { return; } else if (i == 5 && !CLAN_MEMBER(d->character)->rank_num) { if (!ingr_chest_active()) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); } else { d->clan_olc->clan->disable_ingr_chest(d->character.get()); } d->clan_olc->clan->MainMenu(d); return; } else { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } @@ -2827,7 +2827,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { } if (choise >= d->clan_olc->clan->ranks.size()) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } @@ -2843,25 +2843,25 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // выход в общее меню + // d->clan_olc->rank = 0; d->clan_olc->mode = CLAN_MAIN_MENU; d->clan_olc->clan->MainMenu(d); return; default: if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->PrivilegeMenu(d, d->clan_olc->rank); return; } if (num > CLAN_PRIVILEGES_NUM || num <= 0) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->PrivilegeMenu(d, d->clan_olc->rank); return; } - // мы выдаем только доступные привилегии в меню и нормально парсим их тут + // unsigned parse_num; for (parse_num = 0; parse_num <= CLAN_PRIVILEGES_NUM; ++parse_num) { if (d->clan_olc->privileges[CLAN_MEMBER(d->character)->rank_num][parse_num]) { @@ -2892,7 +2892,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { d->clan_olc.reset(); // Clan::ClanSave(); d->state = EConState::kPlaying; - SendMsgToChar("Изменения сохранены.\r\n", d->character.get()); + SendMsgToChar(" .\r\n", d->character.get()); return; case 'n': @@ -2900,11 +2900,11 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kEn: case rus::kEnUpper: d->clan_olc.reset(); d->state = EConState::kPlaying; - SendMsgToChar("Редактирование отменено.\r\n", d->character.get()); + SendMsgToChar(" .\r\n", d->character.get()); return; default: - SendMsgToChar("Неверный выбор!\r\nВы желаете сохранить изменения? Y(Д)/N(Н) : ", + SendMsgToChar(" !\r\n ? Y()/N() : ", d->character.get()); d->clan_olc->mode = CLAN_SAVE_MENU; return; @@ -2918,7 +2918,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // выход в общее меню с изменением всех званий + // for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (d->clan_olc->all_ranks[i]) { unsigned j = CLAN_MEMBER(d->character)->rank_num + 1; @@ -2936,13 +2936,13 @@ void Clan::Manage(DescriptorData *d, const char *arg) { default: if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 0); return; } if (num > CLAN_PRIVILEGES_NUM) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 0); return; } @@ -2972,7 +2972,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // выход в общее меню с изменением всех званий + // for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (d->clan_olc->all_ranks[i]) { unsigned j = CLAN_MEMBER(d->character)->rank_num + 1; @@ -2991,13 +2991,13 @@ void Clan::Manage(DescriptorData *d, const char *arg) { default: if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 1); return; } if (num > CLAN_PRIVILEGES_NUM) { - SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar(" !\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 1); return; } @@ -3027,9 +3027,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { void Clan::MainMenu(DescriptorData *d) { std::ostringstream buffer; - buffer << "Раздел добавления и удаления привилегий.\r\n" - << "Выберите редактируемое звание или действие:\r\n"; - // звания ниже своего + buffer << " .\r\n" + << " :\r\n"; + // int rank = CLAN_MEMBER(d->character)->rank_num + 1; int num = 0; for (std::vector::const_iterator it = d->clan_olc->clan->ranks.begin() + rank; @@ -3037,46 +3037,46 @@ void Clan::MainMenu(DescriptorData *d) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") " << (*it) << "\r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << "добавить всем\r\n"; + << ") " << " \r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << "убрать у всех\r\n"; + << ") " << " \r\n"; if (!CLAN_MEMBER(d->character)->rank_num) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << "Выборка из хранилища по параметрам предметов\r\n" - << " + просмотр хранилища вне замка в качестве бонуса (1000 кун в день) "; + << ") " << " \r\n" + << " + (1000 ) "; if (this->storehouse) - buffer << "(отключить)\r\n"; + buffer << "()\r\n"; else - buffer << "(включить)\r\n"; + buffer << "()\r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << "Моментально обновлять очки дружины в топе (при отключении обновление раз в 6 часов) "; + << ") " << " ( 6 ) "; if (this->exp_info) - buffer << "(отключить)\r\n"; + buffer << "()\r\n"; else - buffer << "(включить)\r\n"; + buffer << "()\r\n"; - // хранилище ингров (включить/переместить) + // (/) buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") "; if (ingr_chest_active()) { - buffer << "Переместить в данную комнату хранилище для ингредиентов\r\n"; + buffer << " \r\n"; } else { - buffer << "Установить в данной комнате хранилище для ингредиентов (1000 кун/день)\r\n"; + buffer << " (1000 /)\r\n"; } - // хранилище ингров (выключить) + // () if (ingr_chest_active()) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") " - << "Отключить хранилище для ингредиентов" << "\r\n"; + << " " << "\r\n"; } else { buffer << kColorBoldBlk << std::setw(2) << ++num - << ") " << "Отключить хранилище для ингредиентов" + << ") " << " " << kColorNrm << "\r\n"; } } - buffer << kColorGrn << " В(Q)" << kColorNrm - << ") Выход\r\n" << "Ваш выбор:"; + buffer << kColorGrn << " (Q)" << kColorNrm + << ") \r\n" << " :"; SendMsgToChar(buffer.str(), d->character.get()); d->clan_olc->mode = CLAN_MAIN_MENU; @@ -3088,12 +3088,12 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { if (d->character) { d->clan_olc.reset(); d->state = EConState::kPlaying; - SendMsgToChar(d->character.get(), "Случилось что-то страшное, сообщите Богам!"); + SendMsgToChar(d->character.get(), " - , !"); } return; } std::ostringstream buffer; - buffer << "Список привилегий для звания '" << kColorBoldRed + buffer << " '" << kColorBoldRed << d->clan_olc->clan->ranks[num] << kColorNrm << "':\r\n"; int count = 0; @@ -3104,9 +3104,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_INFO]) - buffer << "[x] информация о дружине (клан информация)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] информация о дружине (клан информация)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_ADD: @@ -3114,9 +3114,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_ADD]) - buffer << "[x] принятие в дружину (клан принять)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] принятие в дружину (клан принять)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_REMOVE: @@ -3124,9 +3124,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_REMOVE]) - buffer << "[x] изгнание из дружины (клан изгнать)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] изгнание из дружины (клан изгнать)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_PRIVILEGES: @@ -3134,9 +3134,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_PRIVILEGES]) - buffer << "[x] редактирование привилегий (клан привилегии)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] редактирование привилегий (клан привилегии)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_CHANNEL: @@ -3144,9 +3144,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHANNEL]) - buffer << "[x] клан-канал (гдругам)\r\n"; + buffer << "[x] - ()\r\n"; else - buffer << "[ ] клан-канал (гдругам)\r\n"; + buffer << "[ ] - ()\r\n"; } break; case MAY_CLAN_POLITICS: @@ -3154,9 +3154,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_POLITICS]) - buffer << "[x] изменение политики дружины (политика)\r\n"; + buffer << "[x] ()\r\n"; else - buffer << "[ ] изменение политики дружины (политика)\r\n"; + buffer << "[ ] ()\r\n"; } break; case MAY_CLAN_NEWS: @@ -3164,9 +3164,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_NEWS]) - buffer << "[x] добавление новостей дружины (дрновости)\r\n"; + buffer << "[x] ()\r\n"; else - buffer << "[ ] добавление новостей дружины (дрновости)\r\n"; + buffer << "[ ] ()\r\n"; } break; case MAY_CLAN_PKLIST: @@ -3174,9 +3174,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_PKLIST]) - buffer << "[x] добавление в пк-лист (пклист)\r\n"; + buffer << "[x] - ()\r\n"; else - buffer << "[ ] добавление в пк-лист (пклист)\r\n"; + buffer << "[ ] - ()\r\n"; } break; case MAY_CLAN_CHEST_PUT: @@ -3184,9 +3184,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHEST_PUT]) - buffer << "[x] класть вещи в хранилище\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] класть вещи в хранилище\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_CHEST_TAKE: @@ -3194,9 +3194,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHEST_TAKE]) - buffer << "[x] брать вещи из хранилища\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] брать вещи из хранилища\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_BANK: @@ -3204,9 +3204,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_BANK]) - buffer << "[x] брать из казны дружины\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] брать из казны дружины\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_EXIT: @@ -3214,9 +3214,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_EXIT]) - buffer << "[x] свободный выход из дружины\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] свободный выход из дружины\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_MOD: @@ -3224,7 +3224,7 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_MOD] ? "[x]" : "[ ]") - << " написание сообщения дружины\r\n"; + << " \r\n"; } break; case MAY_CLAN_TAX: @@ -3232,7 +3232,7 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_TAX] ? "[x]" : "[ ]") - << " установка налога для ратников\r\n"; + << " \r\n"; } break; case MAY_CLAN_BOARD: @@ -3240,27 +3240,27 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_BOARD] ? "[x]" : "[ ]") - << " сообщения в дрвече\r\n"; + << " \r\n"; } break; } // case } - buffer << kColorGrn << " В(Q)" << kColorNrm - << ") Выход\r\n" << "Ваш выбор:"; + buffer << kColorGrn << " (Q)" << kColorNrm + << ") \r\n" << " :"; SendMsgToChar(buffer.str(), d->character.get()); d->clan_olc->mode = CLAN_PRIVILEGE_MENU; } -// меню добавления/удаления привилегий у всех званий, решил не нагружать все в одну функцию -// flag 0 - добавление, 1 - удаление +// / , +// flag 0 - , 1 - void Clan::AllMenu(DescriptorData *d, unsigned flag) { std::ostringstream buffer; if (flag == 0) - buffer << "Выберите привилегии, которые вы хотите " << kColorBoldRed - << "добавить всем" << kColorNrm << " званиям:\r\n"; + buffer << " , " << kColorBoldRed + << " " << kColorNrm << " :\r\n"; else - buffer << "Выберите привилегии, которые вы хотите " << kColorBoldRed - << "убрать у всех" << kColorNrm << " званий:\r\n"; + buffer << " , " << kColorBoldRed + << " " << kColorNrm << " :\r\n"; int count = 0; for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { @@ -3270,9 +3270,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_INFO]) - buffer << "[x] информация о дружине (клан информация)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] информация о дружине (клан информация)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_ADD: @@ -3280,9 +3280,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_ADD]) - buffer << "[x] принятие в дружину (клан принять)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] принятие в дружину (клан принять)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_REMOVE: @@ -3290,9 +3290,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_REMOVE]) - buffer << "[x] изгнание из дружины (клан изгнать)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] изгнание из дружины (клан изгнать)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_PRIVILEGES: @@ -3300,9 +3300,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_PRIVILEGES]) - buffer << "[x] редактирование привилегий (клан привилегии)\r\n"; + buffer << "[x] ( )\r\n"; else - buffer << "[ ] редактирование привилегий (клан привилегии)\r\n"; + buffer << "[ ] ( )\r\n"; } break; case MAY_CLAN_CHANNEL: @@ -3310,9 +3310,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHANNEL]) - buffer << "[x] клан-канал (гдругам)\r\n"; + buffer << "[x] - ()\r\n"; else - buffer << "[ ] клан-канал (гдругам)\r\n"; + buffer << "[ ] - ()\r\n"; } break; case MAY_CLAN_POLITICS: @@ -3320,9 +3320,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_POLITICS]) - buffer << "[x] изменение политики дружины (политика)\r\n"; + buffer << "[x] ()\r\n"; else - buffer << "[ ] изменение политики дружины (политика)\r\n"; + buffer << "[ ] ()\r\n"; } break; case MAY_CLAN_NEWS: @@ -3330,9 +3330,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_NEWS]) - buffer << "[x] добавление новостей дружины (дрновости)\r\n"; + buffer << "[x] ()\r\n"; else - buffer << "[ ] добавление новостей дружины (дрновости)\r\n"; + buffer << "[ ] ()\r\n"; } break; case MAY_CLAN_PKLIST: @@ -3340,9 +3340,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_PKLIST]) - buffer << "[x] добавление в пк-лист (пклист)\r\n"; + buffer << "[x] - ()\r\n"; else - buffer << "[ ] добавление в пк-лист (пклист)\r\n"; + buffer << "[ ] - ()\r\n"; } break; case MAY_CLAN_CHEST_PUT: @@ -3350,9 +3350,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHEST_PUT]) - buffer << "[x] класть вещи в хранилище\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] класть вещи в хранилище\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_CHEST_TAKE: @@ -3360,9 +3360,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHEST_TAKE]) - buffer << "[x] брать вещи из хранилища\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] брать вещи из хранилища\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_BANK: @@ -3370,9 +3370,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_BANK]) - buffer << "[x] брать из казны дружины\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] брать из казны дружины\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_EXIT: @@ -3380,9 +3380,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_EXIT]) - buffer << "[x] свободный выход из дружины\r\n"; + buffer << "[x] \r\n"; else - buffer << "[ ] свободный выход из дружины\r\n"; + buffer << "[ ] \r\n"; } break; case MAY_CLAN_MOD: @@ -3390,7 +3390,7 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_MOD] ? "[x]" : "[ ]") - << " написание сообщения дружины\r\n"; + << " \r\n"; } break; case MAY_CLAN_TAX: @@ -3398,7 +3398,7 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_TAX] ? "[x]" : "[ ]") - << " установка налога для ратников\r\n"; + << " \r\n"; } break; case MAY_CLAN_BOARD: @@ -3406,13 +3406,13 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_BOARD] ? "[x]" : "[ ]") - << " сообщения в дрвече\r\n"; + << " \r\n"; } break; } // case } - buffer << kColorGrn << " В(Q)" << kColorNrm - << ") Применить\r\n" << "Ваш выбор:"; + buffer << kColorGrn << " (Q)" << kColorNrm + << ") \r\n" << " :"; SendMsgToChar(buffer.str(), d->character.get()); if (flag == 0) d->clan_olc->mode = CLAN_ADDALL_MENU; @@ -3427,7 +3427,7 @@ void Clan::add_offline_member(const std::string &name, int uid, int rank) { this->m_members.set(uid, tmp_member); } -// игрок ранг +// void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { if (ch->get_name_str().empty()) { log("SYSERROR: zero player name (uid = %ld) (%s:%d %s)", ch->get_uid(), @@ -3446,7 +3446,7 @@ void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { && this->GetRent() == CLAN(d->character)->GetRent() && ch != d->character.get()) { SendMsgToChar(d->character.get(), - "%s%s приписан%s к вашей дружине, статус - '%s'.%s\r\n", + "%s%s %s , - '%s'.%s\r\n", kColorWht, GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 6), @@ -3455,14 +3455,14 @@ void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { } } - SendMsgToChar(ch, "%sВас приписали к дружине '%s', статус - '%s'.%s\r\n", + SendMsgToChar(ch, "%s '%s', - '%s'.%s\r\n", kColorWht, this->name.c_str(), (this->ranks[rank]).c_str(), kColorNrm); - sprintf(buf, "Принят в дружину '%s', статус - '%s'", this->name.c_str(), (this->ranks[rank]).c_str()); + sprintf(buf, " '%s', - '%s'", this->name.c_str(), (this->ranks[rank]).c_str()); AddKarma(ch, buf, invite_name.c_str()); return; } -// передача воеводства +// void Clan::HouseOwner(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -3470,21 +3470,21 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { DescriptorData *d = DescriptorByUid(unique); if (buffer2.empty()) - SendMsgToChar("Укажите имя персонажа.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else if (!unique) - SendMsgToChar("Неизвестный персонаж.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else if (unique == ch->get_uid()) - SendMsgToChar("Сменить себя на самого себя? Вы бредите.\r\n", ch); + SendMsgToChar(" ? .\r\n", ch); else if (!d || !sight::CanSee(ch, d->character)) - SendMsgToChar("Этого персонажа нет в игре!\r\n", ch); + SendMsgToChar(" !\r\n", ch); else if (CLAN(d->character) && CLAN(ch) != CLAN(d->character)) - SendMsgToChar("Вы не можете передать свои права члену другой дружины.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else { native_text::capitalize_first(buffer2); - // воевода идет рангом ниже + // this->m_members.set_rank(ch->get_uid(), 1); Clan::SetClanData(ch); - // ставим нового воеводу (если он был в клане - меняем только ранг) + // ( - ) if (CLAN(d->character)) { this->m_members.set_rank(d->character->get_uid(), 0); Clan::SetClanData(d->character.get()); @@ -3492,16 +3492,16 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { this->ClanAddMember(d->character.get(), 0, ch->get_name()); } this->owner = buffer2; - SendMsgToChar(ch, "Поздравляем, вы передали свои полномочия %s!\r\n", GET_PAD(d->character, 2)); + SendMsgToChar(ch, ", %s!\r\n", GET_PAD(d->character, 2)); if (IsMale(ch)) sprintf(buf, - "&RВнимание!!!&n %s ушел на пенсию и добровольно передал руководство дружины %s игроку %s.\r\n", + "&R!!!&n %s %s %s.\r\n", GET_NAME(ch), CLAN(d->character)->GetAbbrev(), GET_PAD(d->character, 2)); else sprintf(buf, - "&RВнимание!!!&n %s ушла на пенсию и добровольно передала руководство дружины %s игроку %s.\r\n", + "&R!!!&n %s %s %s.\r\n", GET_NAME(ch), CLAN(d->character)->GetAbbrev(), GET_PAD(d->character, 2)); @@ -3510,9 +3510,9 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { } /** -* hcontrol owner vnum имя - передача воеводы клана внум игроку имя -* Новый воевода не может быть мембером другого клана, старый исключается из клана. -* Оба могут быть оффлайн во время выполнения команды иммом. +* hcontrol owner vnum - +* , . +* . */ void Clan::hcon_owner(CharData *ch, std::string &text) { std::string buffer; @@ -3526,7 +3526,7 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", vnum); + SendMsgToChar(ch, " %d .\r\n", vnum); return; } @@ -3535,7 +3535,7 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { long member_uid = GetUniqueByName(name); if (!member_uid) { - SendMsgToChar("Неизвестный персонаж.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } @@ -3545,48 +3545,48 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { const auto it = tmp_clan->m_members.find(member_uid); if (it != tmp_clan->m_members.end()) { if (vnum != tmp_clan->GetRent()) { - SendMsgToChar(ch, "%s состоит в другой дружине.\r\n", name.c_str()); + SendMsgToChar(ch, "%s .\r\n", name.c_str()); return; } else if (!it->second->rank_num) { - SendMsgToChar(ch, "%s и так является воеводой этой дружины.\r\n", name.c_str()); + SendMsgToChar(ch, "%s .\r\n", name.c_str()); return; } } } - // убираем старого воеводу из клана + // for (const auto &it : (*clan)->m_members) { if (!it.second->rank_num) { const auto member_uid = it.first; - // ахтунг, удаляется элемент дерева, по которому мы и идем в цикле + // , , char tmpstr[kMaxInputLength]; - sprintf(tmpstr, "смена воеводы"); + sprintf(tmpstr, " "); (*clan)->remove_member(member_uid, tmpstr); break; } } - // вписываем нового + // if ((*clan)->m_members.find(member_uid) != (*clan)->m_members.end()) { - // уже был в клане + // (*clan)->m_members.set_rank(member_uid, 0); } else { (*clan)->add_offline_member(name, member_uid, 0); } - // новый воевода онлайн + // DescriptorData *d = DescriptorByUid(member_uid); if (d && d->character) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), "%sВы стали новым воеводой дружины %s. Желаем удачи!%s\r\n", + SendMsgToChar(d->character.get(), "%s %s. !%s\r\n", kColorBoldGrn, (*clan)->get_abbrev().c_str(), kColorNrm); } if ((*clan)->m_members.size() > 0) { - // оповещение + // for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && CLAN(d->character) && CLAN(d->character)->GetRent() == (*clan)->GetRent()) { SendMsgToChar(d->character.get(), - "%sОсуществлена принудительная смена воеводы вашей дружины: %s -> %s.%s\r\n", + "%s : %s -> %s.%s\r\n", kColorBoldGrn, (*clan)->owner.c_str(), name.c_str(), @@ -3594,11 +3594,11 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { } } } else { - (*clan)->builtOn = time(0); //не ставит текущую дату + (*clan)->builtOn = time(0); // } (*clan)->owner = name; Clan::ClanSave(); - SendMsgToChar("Сделано.\r\n", ch); + SendMsgToChar(".\r\n", ch); } void Clan::CheckPkList(CharData *ch) { @@ -3606,16 +3606,16 @@ void Clan::CheckPkList(CharData *ch) { return; ClanPkList::iterator it; for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { - // пкл + // if ((it = (*clan)->pkList.find(ch->get_uid())) != (*clan)->pkList.end()) SendMsgToChar(ch, - "Находитесь в списке врагов дружины '%s', добавивший: %s.\r\n", + " '%s', : %s.\r\n", (*clan)->name.c_str(), it->second->authorName.c_str()); - // дрл + // if ((it = (*clan)->frList.find(ch->get_uid())) != (*clan)->frList.end()) SendMsgToChar(ch, - "Находитесь в списке друзей дружины '%s', добавивший: %s.\r\n", + " '%s', : %s.\r\n", (*clan)->name.c_str(), it->second->authorName.c_str()); } @@ -3626,7 +3626,7 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { GetOneParam(buffer, buffer2); bool all{false}, name{false}; - // параметр сортировки + // enum ESortParam { SORT_STAT_BY_EXP, SORT_STAT_BY_CLANEXP, @@ -3637,39 +3637,39 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { ESortParam sortParameter; long long lSortParam; - // т.к. в кои8-р русские буквы не попорядку - const char *pSortAlph = "яюэьыъщшчцхфутсрпонмлкизжёедгвба"; - // первая буква имени + // .. 8- + const char *pSortAlph = "֣"; + // - // для избежания путаницы с именами фильтр начинается со знака "!" - // формат команды: - // клан стат [!опыт/!заработанным/!последнему/!имя] [имя/все] + // "!" + // : + // [!/!/!/!] [/] sortParameter = SORT_STAT_BY_EXP; if (buffer2.length() > 1) { - if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "опыту"))) // опыту дружине + if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) // sortParameter = SORT_STAT_BY_CLANEXP; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "заработанным"))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) sortParameter = SORT_STAT_BY_MONEY; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "последнему"))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) sortParameter = SORT_STAT_BY_LOGON; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "имя"))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) sortParameter = SORT_STAT_BY_NAME; - // берем следующий параметр + // if (buffer2[0] == '!') GetOneParam(buffer, buffer2); } std::ostringstream out; out << kColorWht; - if (CompareParam(buffer2, "очистить") || CompareParam(buffer2, "удалить")) { + if (CompareParam(buffer2, "") || CompareParam(buffer2, "")) { if (CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("У вас нет прав удалять статистику.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - // можно почистить тока деньги для удобства сбора с мемберов налога + // GetOneParam(buffer, buffer2); bool money = false; - if (CompareParam(buffer2, "деньги") || CompareParam(buffer2, "money")) + if (CompareParam(buffer2, "") || CompareParam(buffer2, "money")) money = true; for (const auto &it : m_members) { @@ -3682,41 +3682,41 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } if (money) { - SendMsgToChar("Статистика доходов вашей дружины очищена.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { - SendMsgToChar("Статистика вашей дружины полностью очищена.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return; - } else if (CompareParam(buffer2, "все")) { + } else if (CompareParam(buffer2, "")) { all = true; - out << "Статистика вашей дружины "; + out << " "; } else if (!buffer2.empty()) { name = true; - out << "Статистика вашей дружины (поиск по имени '" << buffer2 << "') "; + out << " ( '" << buffer2 << "') "; } else { - out << "Статистика вашей дружины (находящиеся онлайн) "; + out << " ( ) "; } - // вывод режима сортировки - out << "(сортировка по: "; + // + out << "( : "; switch (sortParameter) { - case SORT_STAT_BY_EXP: out << "рейтинговым очкам"; + case SORT_STAT_BY_EXP: out << " "; break; - case SORT_STAT_BY_CLANEXP: out << "опыту дружине"; + case SORT_STAT_BY_CLANEXP: out << " "; break; - case SORT_STAT_BY_MONEY: out << "заработанным кунам"; + case SORT_STAT_BY_MONEY: out << " "; break; - case SORT_STAT_BY_LOGON: out << "последнему заходу в игру"; + case SORT_STAT_BY_LOGON: out << " "; break; - case SORT_STAT_BY_NAME: out << "первой букве имени"; + case SORT_STAT_BY_NAME: out << " "; break; - // этого быть не должно - default: out << "чему БОГ пошлет"; + // + default: out << " "; } out << "):" << "\r\n"; - // multimap ибо могут быть совпадения + // multimap std::multimap > temp_list; for (const auto &it : m_members) { it.second->level = 0; @@ -3740,7 +3740,7 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { if (tmp_time <= 0) tmp_time = time(nullptr); strftime(timeBuf, sizeof(timeBuf), "%d-%m-%Y", localtime(&tmp_time)); - // сортировка по... + // ... switch (sortParameter) { case SORT_STAT_BY_EXP: lSortParam = it.second->exp; break; @@ -3751,9 +3751,9 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { case SORT_STAT_BY_LOGON: lSortParam = GetLastlogonByUnique(it.first); break; case SORT_STAT_BY_NAME: { - // Индекс первой БУКВЫ имени в алфавите. Под UTF-8 буква занимает два байта, и - // strpbrk по одному байту находил бы что попало, а то и вовсе ничего - // (issue #3681). Идём по алфавиту символ за символом. + // . UTF-8 , + // strpbrk , + // (issue #3681). ģ . char first_letter[8] = {0}; native_text::copy_lower_char(it.second->name.c_str(), first_letter); lSortParam = -1; @@ -3765,12 +3765,12 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } } if (lSortParam < 0) { - // не русская буква -- пусть уедет в конец, но в стабильном порядке + // -- , lSortParam = static_cast(native_text::first_char_code(first_letter)); } break; } - // на всякий случай + // default: lSortParam = it.second->exp; } @@ -3778,8 +3778,8 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } table_wrapper::Table table; - table << table_wrapper::kHeader << "Имя" << "Ур" << "Прв" << "Класс" << "Рейт. очков" - << "Опыта дружины" << "Внесено кун" << "Был в игре" << table_wrapper::kEndRow; + table << table_wrapper::kHeader << "" << "" << "" << "" << ". " + << " " << " " << " " << table_wrapper::kEndRow; std::string lvl_str; for (auto it = temp_list.rbegin(); it != temp_list.rend(); ++it) { table << it->second.second->name; @@ -3803,17 +3803,17 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { page_string(ch->desc, out.str()); } -// напоминалка про кончину денег в казне дружины +// void Clan::ChestInvoice() { for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { int cost = (*clan)->ChestTax() + (*clan)->ingr_chest_tax(); cost += (*clan)->calculate_clan_tax(); if (!cost) { - continue; // чем черт не шутит + continue; // } - // опаньки + // if ((*clan)->bank >= cost) { continue; } @@ -3824,7 +3824,7 @@ void Clan::ChestInvoice() { && CLAN(d->character) && CLAN(d->character) == *clan) { SendMsgToChar(d->character.get(), - "[Хранилище]: %s'Напоминаю, что средств в казне дружины хватит менее, чем на сутки!'%s\r\n", + "[]: %s', , !'%s\r\n", kColorBoldRed, kColorNrm); } @@ -3838,7 +3838,7 @@ int Clan::ChestTax() { int count = 0; for (auto chest : world[GetRoomRnum(this->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { - // перебираем шмот + // for (temp = chest->get_contains(); temp; temp = temp->get_next_content()) { cost += temp->get_rent_on(); ++count; @@ -3853,12 +3853,12 @@ int Clan::ChestTax() { } /** -* Вместо спешиала теперь просто перехватываем осмотр контейнеров на случай клан-сундука. -* Смотреть могут ес-сно только соклановцы. -* \todo Вынести из класса. Да и вообще там чистить давно пора. -* \param obj - контейнер -* \param ch - смотрящий -* \return 0 - это не клан-сундук, 1 - это был он +* -. +* - . +* \todo . . +* \param obj - +* \param ch - +* \return 0 - -, 1 - */ bool Clan::ChestShow(ObjData *obj, CharData *ch) { if (!ch->desc || !Clan::is_clan_chest(obj)) { @@ -3866,29 +3866,29 @@ bool Clan::ChestShow(ObjData *obj, CharData *ch) { } if (CLAN(ch) && GetRoomRnum(CLAN(ch)->chest_room) == obj->get_in_room()) { - SendMsgToChar("Хранилище вашей дружины:\r\n", ch); + SendMsgToChar(" :\r\n", ch); int cost = CLAN(ch)->ChestTax(); SendMsgToChar(ch, - "Всего вещей: %d, Рента в день: %d %s\r\n\r\n", + " : %d, : %d %s\r\n\r\n", CLAN(ch)->chest_objcount, cost, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str()); sight::list_obj_to_char(obj->get_contains(), ch, 1, 3); } else { - SendMsgToChar("Не на что тут глазеть, пусто, вот те крест.\r\n", - ch); // засланым казачкам показываем хер, а не хранилище + SendMsgToChar(" , , .\r\n", + ch); // , } return true; } -// +/- клан-экспы +// +/- - void Clan::SetClanExp(CharData *ch, int add) { - // шоб не читили + // if (GetRealLevel(ch) >= kLvlImmortal) { return; } - // обнулять не надо минуса + // CLAN_MEMBER(ch)->clan_exp += add; this->clan_exp += add; @@ -3905,7 +3905,7 @@ void Clan::SetClanExp(CharData *ch, int add) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->rent) { SendMsgToChar(d->character.get(), - "&GВаш замок достиг нового, %d уровня! Поздравляем!&n\r\n", + "&G , %d ! !&n\r\n", this->clan_level); } } @@ -3918,7 +3918,7 @@ void Clan::SetClanExp(CharData *ch, int add) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->rent) { SendMsgToChar(d->character.get(), - "%sВаш замок потерял уровень! Теперь он %d уровня! Поздравляем!%s\r\n", + "%s ! %d ! !%s\r\n", kColorBoldRed, this->clan_level, kColorNrm); } @@ -3926,9 +3926,9 @@ void Clan::SetClanExp(CharData *ch, int add) { } } -// добавление экспы для топа кланов и мемберу в зачетку +// void Clan::AddTopExp(CharData *ch, int add_exp) { - // шоб не читили + // if (GetRealLevel(ch) >= kLvlImmortal) return; @@ -3936,16 +3936,16 @@ void Clan::AddTopExp(CharData *ch, int add_exp) { if (CLAN_MEMBER(ch)->exp < 0) CLAN_MEMBER(ch)->exp = 0; - // в буффер или сразу в топ + // if (this->exp_info) { this->exp += add_exp; if (this->exp < 0) this->exp = 0; } else - this->exp_buf += add_exp; // тут обнулять не надо + this->exp_buf += add_exp; // } -// синхронизация клановой экспы для топ с буффером, если есть режим запрета показа в ран-тайме +// , - void Clan::SyncTopExp() { for (auto clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) if (!(*clan)->exp_info) { @@ -3956,56 +3956,56 @@ void Clan::SyncTopExp() { } } -// установка режима оповещения об изменениях в хранилище +// void SetChestMode(CharData *ch, std::string &buffer) { if (ch->IsNpc()) return; if (!CLAN(ch)) { - SendMsgToChar("Для начала обзаведитесь дружиной.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - if (CompareParam(buffer, "нет")) { + if (CompareParam(buffer, "")) { ch->UnsetFlag(EPrf::kDecayMode); ch->UnsetFlag(EPrf::kTakeMode); - SendMsgToChar("Ладушки.\r\n", ch); - } else if (CompareParam(buffer, "рассыпание")) { + SendMsgToChar(".\r\n", ch); + } else if (CompareParam(buffer, "")) { ch->SetFlag(EPrf::kDecayMode); ch->UnsetFlag(EPrf::kTakeMode); - SendMsgToChar("Ладушки.\r\n", ch); - } else if (CompareParam(buffer, "изменение")) { + SendMsgToChar(".\r\n", ch); + } else if (CompareParam(buffer, "")) { ch->UnsetFlag(EPrf::kDecayMode); ch->SetFlag(EPrf::kTakeMode); - SendMsgToChar("Ладушки.\r\n", ch); - } else if (CompareParam(buffer, "полный")) { + SendMsgToChar(".\r\n", ch); + } else if (CompareParam(buffer, "")) { ch->SetFlag(EPrf::kDecayMode); ch->SetFlag(EPrf::kTakeMode); - SendMsgToChar("Ладушки.\r\n", ch); + SendMsgToChar(".\r\n", ch); } else { - SendMsgToChar("Задается режим оповещения об изменениях в хранилище дружины.\r\n" - "Формат команды: режим хранилище <нет|рассыпание|изменение|полный>\r\n" - " нет - выключение канала хранилища\r\n" - " рассыпание - получать только сообщения о рассыпании вещей\r\n" - " изменение - получать только сообщения о взятии/добавлении вещей\r\n" - " полный - получать оба вида сообщений\r\n", ch); + SendMsgToChar(" .\r\n" + " : <|||>\r\n" + " - \r\n" + " - \r\n" + " - / \r\n" + " - \r\n", ch); return; } } -// шоб не засорять в режиме, а выдать строку сразу +// , std::string GetChestMode(CharData *ch) { if (ch->IsFlagged(EPrf::kDecayMode)) { if (ch->IsFlagged(EPrf::kTakeMode)) - return "полный"; + return ""; else - return "рассыпание"; + return ""; } else if (ch->IsFlagged(EPrf::kTakeMode)) - return "изменение"; + return ""; else - return "выкл"; + return ""; } -// неужто реально глючит? +// ? int Clan::GetRent() { return this->rent; } @@ -4014,7 +4014,7 @@ int Clan::GetOutRent() { return this->out_rent; } -// * Удаление чара из клана, клан берется не через поля чара, а ищем по всем кланам +// * , , void Clan::remove_from_clan(long unique) { for (const auto &clan : Clan::ClanList) { const auto it = clan->m_members.find(unique); @@ -4073,8 +4073,8 @@ bool ClanSystem::is_ingr_chest(ObjData *obj) { } /** -* Оповещение соклановцев о входе/выходе друг друга. -* \param enter 1 - вход чара, 0 - выход. +* / . +* \param enter 1 - , 0 - . */ void Clan::clan_invoice(CharData *ch, bool enter) { if (ch->IsNpc() || !CLAN(ch)) { @@ -4088,19 +4088,19 @@ void Clan::clan_invoice(CharData *ch, bool enter) { && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kClanmembersMode)) { if (enter) { - SendMsgToChar(d->character.get(), "%sДружинни%s %s вош%s в мир.%s\r\n", - kColorBoldBlk, IsMale(ch) ? "к" : "ца", GET_NAME(ch), + SendMsgToChar(d->character.get(), "%s%s %s %s .%s\r\n", + kColorBoldBlk, IsMale(ch) ? "" : "", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 5), kColorNrm); } else { - SendMsgToChar(d->character.get(), "%sДружинни%s %s покинул%s мир.%s\r\n", - kColorBoldBlk, IsMale(ch) ? "к" : "ца", GET_NAME(ch), + SendMsgToChar(d->character.get(), "%s%s %s %s .%s\r\n", + kColorBoldBlk, IsMale(ch) ? "" : "", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), kColorNrm); } } } } -// иммское где для кланхранилищ +// std::string Clan::print_imm_where_obj(const ObjData *obj) { std::string str; for (Clan::shared_ptr ptr_clan : Clan::ClanList) { @@ -4108,7 +4108,7 @@ std::string Clan::print_imm_where_obj(const ObjData *obj) { if (Clan::is_clan_chest(chest)) { for (ObjData *chest_content = chest->get_contains(); chest_content; chest_content = chest_content->get_next_content()) { if (obj->get_id() == chest_content->get_id()) { - str = fmt::format("наход{}ся в хранилище дружины '{}'.\r\n", + str = fmt::format("{} '{}'.\r\n", grammar::ObjPluralVerbEnding(((chest_content))->get_sex()), ptr_clan->GetAbbrev()); return str; @@ -4139,13 +4139,13 @@ int Clan::print_spell_locate_object(CharData *ch, int count, std::string name) { continue; } - sprintf(buf, "%s наход%sся в хранилище дружины '%s'.", + sprintf(buf, "%s %s '%s'.", temp->get_short_description().c_str(), grammar::ObjPluralVerbEnding((temp)->get_sex()), (*clan)->GetAbbrev()); // CAP(buf); if (privilege::IsGrGod(ch)) { - sprintf(buf2, " Vnum предмета: %d", GET_OBJ_VNUM(temp)); + sprintf(buf2, " Vnum : %d", GET_OBJ_VNUM(temp)); strcat(buf, buf2); } strcat(buf, "\r\n"); @@ -4197,7 +4197,7 @@ void Clan::add_remember(std::string text, int flag) { break; default: - log("SYSERROR: мы не должны были сюда попасть, flag: %d, func: %s", + log("SYSERROR: , flag: %d, func: %s", flag, __func__); break; } @@ -4228,12 +4228,12 @@ std::string Clan::get_remember(unsigned int num, int flag) const { break; } default: - log("SYSERROR: мы не должны были сюда попасть, flag: %d, func: %s", + log("SYSERROR: , flag: %d, func: %s", flag, __func__); break; } if (buffer.empty()) { - buffer = "Вам нечего вспомнить.\r\n"; + buffer = " .\r\n"; } return buffer; } @@ -4255,7 +4255,7 @@ void Clan::init_ingr_chest() { return; } - // на случай релоада + // for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { ObjData *obj_next; @@ -4277,7 +4277,7 @@ void Clan::init_ingr_chest() { log(" IngrChest load error '%d'! (%s %s %d)", GetRent(), __FILE__, __func__, __LINE__); return; } - //лоадим в комнату сам хран + // PlaceObjToRoom(chest.get(), get_ingr_chest_room_rnum()); FILE *fl = fopen(filename.c_str(), "r+b"); @@ -4335,13 +4335,13 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || CLAN(ch)->GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar("Не имеете таких правов!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return false; } if (obj->get_type() != EObjType::kMagicComponent && obj->get_type() != EObjType::kCraftMaterial) { - SendMsgToChar(ch, "%s - Хранилище ингредиентов не предназначено для предметов данного типа.\r\n", + SendMsgToChar(ch, "%s - .\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); if (obj->get_type() == EObjType::kMoney) { @@ -4349,7 +4349,7 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); currencies::AddHand(*ch, currencies::kGold, howmany); - SendMsgToChar(ch, "Вы вновь обрели %d %s.\r\n", howmany, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(howmany, grammar::ECase::kNom).c_str()); + SendMsgToChar(ch, " %d %s.\r\n", howmany, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(howmany, grammar::ECase::kNom).c_str()); } } else if (obj->has_flag(EObjFlag::kNodrop) || obj->has_flag(EObjFlag::kZonedecay) @@ -4357,10 +4357,10 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { || obj->has_flag(EObjFlag::kNorent) || obj->get_rent_off() < 0 || obj->get_rnum() <= kNothing) { - act("Неведомая сила помешала положить $o3 в $O3.", false, ch, obj, chest, kToChar); + act(" $o3 $O3.", false, ch, obj, chest, kToChar); } else { if (CLAN(ch)->ingr_chest_objcount_ >= CLAN(ch)->ingr_chest_max_objects()) { - act("Вы попытались запихнуть $o3 в $O3, но не смогли - там просто нет места.", + act(" $o3 $O3, - .", false, ch, obj, @@ -4371,7 +4371,7 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { dungeons::SwapOriginalObject(obj); RemoveObjFromChar(obj); PlaceObjIntoObj(obj, chest); - act("Вы положили $o3 в $O3.", false, ch, obj, chest, kToChar); + act(" $o3 $O3.", false, ch, obj, chest, kToChar); CLAN(ch)->ingr_chest_objcount_++; GlobalObjects::ingr_chest_saver().mark_dirty(CLAN(ch).get()); } @@ -4381,14 +4381,14 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { bool Clan::take_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || CLAN(ch)->GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar("Не имеете таких правов!\r\n", ch); + SendMsgToChar(" !\r\n", ch); return false; } RemoveObjFromObj(obj); PlaceObjToInventory(obj, ch); if (obj->get_carried_by() == ch) { - act("Вы взяли $o3 из $O1.", false, ch, obj, chest, kToChar); + act(" $o3 $O1.", false, ch, obj, chest, kToChar); CLAN(ch)->ingr_chest_objcount_--; GlobalObjects::ingr_chest_saver().mark_dirty(CLAN(ch).get()); } @@ -4403,20 +4403,20 @@ bool ClanSystem::show_ingr_chest(ObjData *obj, CharData *ch) { if (CLAN(ch) && CLAN(ch)->ingr_chest_active() && CLAN(ch)->GetRent() / 100 == GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar("Хранилище ингредиентов вашей дружины:\r\n", ch); + SendMsgToChar(" :\r\n", ch); int cost = CLAN(ch)->ingr_chest_tax(); - SendMsgToChar(ch, "Всего вещей: %d/%d, Рента в день: %d %s\r\n\r\n", + SendMsgToChar(ch, " : %d/%d, : %d %s\r\n\r\n", CLAN(ch)->get_ingr_chest_objcount(), CLAN(ch)->ingr_chest_max_objects(), cost, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str()); sight::list_obj_to_char(obj->get_contains(), ch, 1, 4); } else { - SendMsgToChar("Не на что тут глазеть, пусто, вот те крест.\r\n", ch); + SendMsgToChar(" , , .\r\n", ch); } return true; } -// * Расчет суточной ренты хранилища ингров. +// * . int Clan::ingr_chest_tax() { if (!ingr_chest_active()) { return false; @@ -4439,7 +4439,7 @@ int Clan::ingr_chest_tax() { return cost; } -// * Очистка хранилища ингров при нулевом клан-банке. +// * -. void Clan::purge_ingr_chest() { if (!ingr_chest_active()) { return; @@ -4469,9 +4469,9 @@ int Clan::calculate_clan_tax() const { } bool Clan::ingr_chest_active() const { - // Распущенная дружина (без членов) не должна считаться владельцем - // сундука для ингров: платить налог, отображать его в olc, etc. - // См. issue #3191. + // ( ) + // : , olc, etc. + // . issue #3191. if (m_members.empty()) { return false; } @@ -4480,12 +4480,12 @@ bool Clan::ingr_chest_active() const { void Clan::set_ingr_chest(CharData *ch) { if (GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar("Данная комната находится вне зоны вашего замка.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } bool chest_moved = false; - // хран под ингры уже был + // if (ingr_chest_active()) { for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { @@ -4505,22 +4505,22 @@ void Clan::set_ingr_chest(CharData *ch) { if (chest) { PlaceObjToRoom(chest.get(), get_ingr_chest_room_rnum()); } - SendMsgToChar("Хранилище установлено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { - SendMsgToChar("Хранилище перенесено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } void Clan::disable_ingr_chest(CharData *ch) { if (!ingr_chest_active()) { - SendMsgToChar("У вас и так нет хранилища для ингредиентов.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { if (chest->get_contains()) { - SendMsgToChar("Во избежание недоразумений отключить можно только пустое хранилище.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } ExtractObjFromWorld(chest); @@ -4528,7 +4528,7 @@ void Clan::disable_ingr_chest(CharData *ch) { } } ingr_chest_room_rnum_ = 0; - SendMsgToChar("Хранилище отключено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } int Clan::ingr_chest_max_objects() { @@ -4545,34 +4545,34 @@ void save_chest_log() { } } -// * Генерация справки 'сайтыдружин'. +// * ''. void init_xhelp() { std::stringstream out; - out << " В данном разделе приведены адреса сайтов, принадлежащим той или иной дружине.\r\n" - "Как правило, на подобных сайтах вы можете ознакомиться с уставом дружины,\r\n" - "узнать условия вступления в дружину, а также получить довольно много информации\r\n" - "о политике дружины, ее составе, важных событиях, участие в которых принимали ее\r\n" - "ратники и многое другое.\r\n\r\n" - " Список сайтов дружин:\r\n\r\n"; + out << " , .\r\n" + " , ,\r\n" + " , \r\n" + " , , , \r\n" + " .\r\n\r\n" + " :\r\n\r\n"; for (const auto &i : Clan::ClanList) { out << " $COLORW" << fmt::format("{:<7}", i->GetAbbrev()) << "$COLORn -- $COLORC" - << (i->get_web_url().empty() ? "$COLORW[ НЕТ ИНФОРМАЦИИ ]" : i->get_web_url()) + << (i->get_web_url().empty() ? "$COLORW[ ]" : i->get_web_url()) << "$COLORn\r\n"; } - out << "\r\n Официальный сайт мада МПМ Былины:$COLORc www.mud.ru$COLORn\r\n" - << " Сайт истории мада МПМ Былины:$COLORc mudhistory.nm.ru$COLORn\r\n" - << "\r\nСм. также:$COLORC ДРУЖИНЫ $COLORn\r\n"; + out << "\r\n :$COLORc www.mud.ru$COLORn\r\n" + << " :$COLORc mudhistory.nm.ru$COLORn\r\n" + << "\r\n. :$COLORC $COLORn\r\n"; - HelpSystem::add_dynamic("САЙТЫДРУЖИН", out.str()); + HelpSystem::add_dynamic("", out.str()); HelpSystem::add_dynamic("CLANSITES", out.str()); HelpSystem::add_dynamic("INTERNETLINKS", out.str()); } const char *GOLD_TAX_FORMAT = - "Формат команды: клан налог <число от 0 до 25>\r\n" - "Устанавливает процент автоматических отчислений в казну дружины.\r\n"; + " : < 0 25>\r\n" + " .\r\n"; void tax_manage(CharData *ch, std::string &buffer) { if (!CLAN(ch)) return; @@ -4583,7 +4583,7 @@ void tax_manage(CharData *ch, std::string &buffer) { int tax = std::stoi(buffer, nullptr, 10); if (tax <= MAX_GOLD_TAX_PCT) { CLAN(ch)->set_gold_tax_pct(tax); - SendMsgToChar(ch, "Налог для ратников дружины установлен в %d%%\r\n", tax); + SendMsgToChar(ch, " %d%%\r\n", tax); } else { SendMsgToChar(GOLD_TAX_FORMAT, ch); } @@ -4592,7 +4592,7 @@ void tax_manage(CharData *ch, std::string &buffer) { SendMsgToChar(GOLD_TAX_FORMAT, ch); } } else { - SendMsgToChar(ch, "Текущий налог для ратников дружины: %ld%%\r\n%s", + SendMsgToChar(ch, " : %ld%%\r\n%s", CLAN(ch)->get_gold_tax_pct(), GOLD_TAX_FORMAT); } } @@ -4607,27 +4607,27 @@ long do_gold_tax(CharData *ch, long gold) { const long tax = (gold * CLAN(ch)->get_gold_tax_pct()) / 100; if (tax <= 0) return false; - // TODO: мб вынести как в desc_count для версии с окончаниями? + // TODO: desc_count ? if ((tax % 100 >= 11 && tax % 100 <= 14) || tax % 10 >= 5 || tax % 10 == 0) { SendMsgToChar(ch, - "%ld %s было немедленно отправлено в казну вашей дружины.\r\n", + "%ld %s .\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } else if (tax % 10 == 1) { SendMsgToChar(ch, - "%ld %s была немедленно отправлена в казну вашей дружины.\r\n", + "%ld %s .\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } else { SendMsgToChar(ch, - "%ld %s были немедленно отправлены в казну вашей дружины.\r\n", + "%ld %s .\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } - // 1 куну за транзакцию, если сумма налога позволяет + // 1 , const long real_tax = tax > 1 ? tax - 1 : tax; CLAN(ch)->set_bank(CLAN(ch)->get_bank() + real_tax); CLAN_MEMBER(ch)->money += real_tax; - // возврат полного налога, снятого с чара + // , return tax; } return false; @@ -4637,7 +4637,7 @@ long do_gold_tax(CharData *ch, long gold) { //////////////////////////////////////////////////////////////////////////////// -// клан сайт <текст> +// <> void Clan::house_web_url(CharData *ch, const std::string &buffer) { const unsigned MAX_URL_LENGTH = 40; std::istringstream tmp(buffer); @@ -4646,17 +4646,17 @@ void Clan::house_web_url(CharData *ch, const std::string &buffer) { if (url.size() > MAX_URL_LENGTH) { url = url.substr(0, MAX_URL_LENGTH); - SendMsgToChar(ch, "Строка была обрезана до %u символов.\r\n", MAX_URL_LENGTH); + SendMsgToChar(ch, " %u .\r\n", MAX_URL_LENGTH); } if (url.empty()) { - SendMsgToChar("Адрес сайта вашей дружины удален.\r\n" - "Обновление справки 'сайтыдружин' состоится в течении минуты.\r\n", ch); + SendMsgToChar(" .\r\n" + " '' .\r\n", ch); this->web_url_.clear(); } else { this->web_url_ = url; - SendMsgToChar("Адрес сайта вашей дружины установлен.\r\n" - "Обновление справки 'сайтыдружин' состоится в течении минуты.\r\n", ch); + SendMsgToChar(" .\r\n" + " '' .\r\n", ch); snprintf(buf, sizeof(buf), "%s sets new clan website: %s", GET_NAME(ch), url.c_str()); mudlog(buf, LGH, kLvlImmortal, SYSLOG, true); @@ -4665,8 +4665,8 @@ void Clan::house_web_url(CharData *ch, const std::string &buffer) { HelpSystem::need_update = true; } -// для использования с кланами (в "клан лог", сообщениях хранилища и т.д.): -// возвращает клан-метку с ведущим пробелом +// ( " ", ..): +// - std::string clan_get_custom_label(ObjData *obj, Clan::shared_ptr clan) { if (obj->get_custom_label() && !obj->get_custom_label()->text_label.empty() @@ -4739,8 +4739,8 @@ long Clan::get_bank() const { return bank; } -// проверка, находится ли левый чар в замке, -// если да, то выпинываем его оттуда +// , , +// , void ClanSystem::check_player_in_house() { for (auto d = descriptor_list; d; d = d->next) { if (d->character @@ -4748,17 +4748,17 @@ void ClanSystem::check_player_in_house() { const auto clan = Clan::GetClanByRoom(d->character->in_room); if (clan) { char_from_room(d->character); - act("$n был$g выдворен$a за пределы замка!", true, d->character.get(), 0, 0, kToRoom); - SendMsgToChar("Вы были выдворены за пределы замка!\r\n", d->character.get()); + act("$n $g $a !", true, d->character.get(), 0, 0, kToRoom); + SendMsgToChar(" !\r\n", d->character.get()); char_to_room(d->character, GetRoomRnum(clan->GetOutRent())); sight::look_at_room(d->character.get(), GetRoomRnum(clan->GetOutRent())); - act("$n свалил$u с небес, выкрикивая какие-то ругательства!", true, d->character.get(), 0, 0, kToRoom); + act("$n $u , - !", true, d->character.get(), 0, 0, kToRoom); } } } } -// показывает, является ли чар союзником такой-то дружине +// , - bool ClanSystem::is_alliance(CharData *ch, char *clan_abbr) { std::string abbrev = clan_abbr; if (!CLAN(ch)) { @@ -4787,7 +4787,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; std::string buffer = argument; std::ostringstream buffer3; - buffer3 << "Дружины, находящиеся в состоянии войны:\r\n"; + buffer3 << ", :\r\n"; if (!buffer.empty()) { Clan::ClanListType::const_iterator clan1; for (clan1 = Clan::ClanList.begin(); clan1 != Clan::ClanList.end(); ++clan1) { @@ -4797,7 +4797,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan1 == Clan::ClanList.end() || (*clan1)->m_members.size() == 0) { - SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); + SendMsgToChar(" \r\n", ch); return; } @@ -4808,7 +4808,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if ((*clan1)->CheckPolitics((*clan2)->rent) == kPoliticsWar) { - buffer3 << " " << (*clan1)->abbrev << " против " << (*clan2)->abbrev << "\r\n"; + buffer3 << " " << (*clan1)->abbrev << " " << (*clan2)->abbrev << "\r\n"; } } } else { @@ -4819,7 +4819,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan1->CheckPolitics(clan2->rent) == kPoliticsWar) { - buffer3 << " " << clan1->abbrev << " против " << clan2->abbrev << "\r\n"; + buffer3 << " " << clan1->abbrev << " " << clan2->abbrev << "\r\n"; } } } @@ -4829,16 +4829,16 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } -// обработка клан-канала и канала союзников, как игрока, так и имма -// клановые БОГи ниже 34 не могут говорить другим дружинам, и им и остальным спокойнее -// для канала союзников нужен обоюдный альянс дружин +// - , , +// 34 , +// void DoClanChannel(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (ch->IsNpc()) return; std::string buffer = argument; - // большой неклановый или 34 клановый БОГ говорит какой-то дружине + // 34 - if (privilege::IsImpl(ch) || (privilege::IsGrGod(ch) && !CLAN(ch))) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -4851,29 +4851,29 @@ void DoClanChannel(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } if (clan == Clan::ClanList.end()) { - if (!CLAN(ch)) // неклановый 34 ошибся аббревиатурой - SendMsgToChar("Дружина с такой аббревиатурой не найдена.\r\n", ch); - else // клановый 34 ошибиться не может, идет в его клан-канал + if (!CLAN(ch)) // 34 + SendMsgToChar(" .\r\n", ch); + else // 34 , - { - buffer = argument; // финт ушами + buffer = argument; // CLAN(ch)->CharToChannel(ch, buffer, subcmd); } return; } (*clan)->GodToChannel(ch, buffer, subcmd); - // остальные говорят только в свою дружину + // } else { if (!CLAN(ch)) { - SendMsgToChar("Вы не принадлежите ни к одной дружине.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - // ограничения на клан-канал не канают на любое звание, если это БОГ + // - , if (!privilege::IsImmortal(ch) && (!(CLAN(ch))->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHANNEL] || ch->IsFlagged(EPlrFlag::kDumbed))) { - SendMsgToChar("Вы не можете пользоваться каналом дружины.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } @@ -4887,16 +4887,16 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } std::string buffer = argument; if (buffer.empty()) { - // сортировка кланов по экспе + // std::multimap sort_clan; for (const auto &clan : Clan::ClanList) { sort_clan.insert(std::make_pair(clan->exp, clan)); } std::ostringstream out; - // \todo Тут нужно использовать table_wrapper::Table а не формат. + // \todo table_wrapper::Table . std::string_view clanTopFormat{" {:5} {:6} {:<30} {:14}{:14} {:9}\r\n"}; - out << "В игре зарегистрированы следующие дружины:\r\n" - << " # Название Всего опыта За 30 дней Человек\r\n\r\n"; + out << " :\r\n" + << " # 30 \r\n\r\n"; int count = 1; for (const auto &it : reverse(sort_clan)) { if (it.second->m_members.size() == 0) { @@ -4926,22 +4926,22 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan == Clan::ClanList.end()) { - if (CompareParam(buffer, "все")) { + if (CompareParam(buffer, "")) { all = true; } else { - SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); + SendMsgToChar(" \r\n", ch); return; } } - // спам-контроль применяем только если запросили полный список или не свою дружину + // - if (all || !ch->player_specials->clan || !CompareParam(ch->player_specials->clan->GetAbbrev(), (*clan)->abbrev)) { if (PerformWhoSpamcontrol(ch, kWhoListclan)) { return; } } - // строится список членов дружины или всех дружин (по флагу all) + // ( all) std::vector temp_list; for (auto d = descriptor_list; d; d = d->next) { if (d->character @@ -4955,14 +4955,14 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } - // до кучи сортировка по рангам + // std::sort(temp_list.begin(), temp_list.end(), SortRank()); std::ostringstream buffer2; - buffer2 << "В игре зарегистрированы следующие дружины:\r\n" << " # Глава Название\r\n\r\n"; + buffer2 << " :\r\n" << " # \r\n\r\n"; std::string_view clanFormat{" {:5} {:6} {:15} {}\r\n"}; std::string_view memberFormat{" {:>10} {}{}{} {}{}{}\r\n"}; - // если искали конкретную дружину - выводим ее + // - if (!all) { buffer2 << fmt::format(fmt::runtime(clanFormat), 1, (*clan)->abbrev, (*clan)->owner, (*clan)->name); for (const auto &it : temp_list) { @@ -4970,11 +4970,11 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { : (*clan)->ranks_female[CLAN_MEMBER(it)->rank_num]), GetPkNameColor(it), (it)->GetNameWithTitleOrRace(), kColorNrm, kColorBoldRed, - (it->IsFlagged(EPlrFlag::kKiller) ? "(ДУШЕГУБ)" : ""), + (it->IsFlagged(EPlrFlag::kKiller) ? "()" : ""), kColorNrm); } } - // просто выводим все дружины и всех членов (без параметра 'все' в списке будут только дружины) + // ( '' ) else { int count = 1; for (auto clan_i = Clan::ClanList.begin(); clan_i != Clan::ClanList.end(); ++clan_i) { @@ -4989,7 +4989,7 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { buffer2 << fmt::format(fmt::runtime(memberFormat), (*clan_i)->ranks[CLAN_MEMBER(it)->rank_num], GetPkNameColor(it), it->GetNameWithTitleOrRace(), kColorNrm, kColorBoldRed, - (it->IsFlagged(EPlrFlag::kKiller) ? "(ДУШЕГУБ)" : ""), + (it->IsFlagged(EPlrFlag::kKiller) ? "()" : ""), kColorNrm); } } @@ -4997,13 +4997,13 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } - buffer2 << "\r\nВсего игроков - " << temp_list.size() << "\r\n"; + buffer2 << "\r\n - " << temp_list.size() << "\r\n"; SendMsgToChar(buffer2.str(), ch); } void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } std::string buffer = argument, buffer2; @@ -5012,23 +5012,23 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { //std::string frmt{"%s [%s] :: %s\r\n%s\r\n\r\n"}; if (buffer2.empty()) { - // выводим список тех, кто онлайн + // , SendMsgToChar(ch, - "%sОтображаются только находящиеся в игре персонажи:%s\r\n\r\n", + "%s :%s\r\n\r\n", kColorWht, kColorNrm); ClanPkList::const_iterator it; - // вобщем чтобы словить чаров, находящихся в лд - придется гонять по чарактер-листу + // , - - for (const auto &tch : character_list) { if (tch->IsNpc()) continue; - // пкл + // if (!subcmd) { it = CLAN(ch)->pkList.find(tch->get_uid()); if (it != CLAN(ch)->pkList.end()) print_pkl(ch, info, it); } - // дрл + // else { it = CLAN(ch)->frList.find(tch->get_uid()); if (it != CLAN(ch)->frList.end()) @@ -5036,55 +5036,55 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } } if (info.str().empty()) - info << "Записи в выбранном списке отсутствуют.\r\n"; + info << " .\r\n"; page_string(ch->desc, info.str()); - } else if (CompareParam(buffer2, "все") || CompareParam(buffer2, "all")) { - // выводим весь список - SendMsgToChar(ch, "%sСписок отображается полностью:%s\r\n\r\n", kColorWht, kColorNrm); - // пкл + } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "all")) { + // + SendMsgToChar(ch, "%s :%s\r\n\r\n", kColorWht, kColorNrm); + // if (!subcmd) for (ClanPkList::const_iterator it = CLAN(ch)->pkList.begin(); it != CLAN(ch)->pkList.end(); ++it) print_pkl(ch, info, it); - // дрл + // else for (ClanPkList::const_iterator it = CLAN(ch)->frList.begin(); it != CLAN(ch)->frList.end(); ++it) print_pkl(ch, info, it); if (info.str().empty()) - info << "Записи в выбранном списке отсутствуют.\r\n"; + info << " .\r\n"; page_string(ch->desc, info.str()); - } else if ((CompareParam(buffer2, "добавить") || CompareParam(buffer2, "add")) + } else if ((CompareParam(buffer2, "") || CompareParam(buffer2, "add")) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { - // добавляем нового + // GetOneParam(buffer, buffer2); if (buffer2.empty()) { - SendMsgToChar("Кого добавить то?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } long unique = GetUniqueByName(buffer2, 1); if (!unique) { - SendMsgToChar("Интересующий вас персонаж не найден.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (unique < 0) { - SendMsgToChar("Не дело это, Богов добавлять куда не надо....\r\n", ch); + SendMsgToChar(" , ....\r\n", ch); return; } const auto it = CLAN(ch)->m_members.find(unique); if (it != CLAN(ch)->m_members.end()) { - SendMsgToChar("Давайте не будем засорять список всяким бредом?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } if (buffer.empty()) { - SendMsgToChar("Потрудитесь прокомментировать, за что вы его так.\r\n", ch); + SendMsgToChar(" , .\r\n", ch); return; } - // тож надо проверять + // ClanPkList::iterator it2; if (!subcmd) { it2 = CLAN(ch)->pkList.find(unique); @@ -5094,20 +5094,20 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if ((!subcmd && it2 != CLAN(ch)->pkList.end()) || (subcmd && it2 != CLAN(ch)->frList.end())) { - // уид тут саавсем не обязательно, что валидный + // , const auto rank_it = CLAN(ch)->m_members.find(it2->second->author); if (rank_it != CLAN(ch)->m_members.end() && rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { if (!subcmd) { - SendMsgToChar("Ваша жертва уже добавлена в список врагов старшим по званию.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { - SendMsgToChar("Персонаж уже добавлен в список друзей старшим по званию.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return; } } - // собственно пишем новую жертву/друга + // / ClanPkPtr tempRecord(new ClanPk); tempRecord->author = ch->get_uid(); tempRecord->authorName = GET_NAME(ch); @@ -5124,13 +5124,13 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (d && d->character->IsFlagged(EPrf::kPklMode)) { if (!subcmd) { SendMsgToChar(d->character.get(), - "%sДружина '%s' добавила вас в список своих врагов!%s\r\n", + "%s '%s' !%s\r\n", kColorBoldRed, CLAN(ch)->name.c_str(), kColorNrm); } else { SendMsgToChar(d->character.get(), - "%sДружина '%s' добавила вас в список своих друзей!%s\r\n", + "%s '%s' !%s\r\n", kColorGrn, CLAN(ch)->name.c_str(), kColorNrm); @@ -5138,32 +5138,32 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SetWait(ch, 1, false); } - SendMsgToChar("Ладушки, добавили.\r\n", ch); - } else if ((CompareParam(buffer2, "удалить") || CompareParam(buffer2, "delete")) + SendMsgToChar(", .\r\n", ch); + } else if ((CompareParam(buffer2, "") || CompareParam(buffer2, "delete")) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { - // удаление записи + // GetOneParam(buffer, buffer2); - if (CompareParam(buffer2, "все", 1) || CompareParam(buffer2, "all", 1)) { + if (CompareParam(buffer2, "", 1) || CompareParam(buffer2, "all", 1)) { if (CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Полная очистка списка доступна только воеводе.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - // пкл + // if (!subcmd) CLAN(ch)->pkList.erase(CLAN(ch)->pkList.begin(), CLAN(ch)->pkList.end()); - // дрл + // else CLAN(ch)->frList.erase(CLAN(ch)->frList.begin(), CLAN(ch)->frList.end()); - SendMsgToChar("Список очищен.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } long unique = GetUniqueByName(buffer2, true); if (unique <= 0) { - SendMsgToChar("Интересующий вас персонаж не найден.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } - // пкл, раздельно они мне больше нравятся, чем по пять раз subcmd проверять + // , , subcmd bool removed = false; if (!subcmd) { ClanPkList::iterator it; @@ -5172,13 +5172,13 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { const auto pk_rank_it = CLAN(ch)->m_members.find(it->second->author); if (pk_rank_it != CLAN(ch)->m_members.end() && pk_rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Ваша жертва была добавлена старшим по званию.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } CLAN(ch)->pkList.erase(it); removed = true; } - // дрл + // } else { ClanPkList::iterator it; it = CLAN(ch)->frList.find(unique); @@ -5186,7 +5186,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { const auto fr_rank_it = CLAN(ch)->m_members.find(it->second->author); if (fr_rank_it != CLAN(ch)->m_members.end() && fr_rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar("Персонаж был добавлен старшим по званию.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } CLAN(ch)->frList.erase(it); @@ -5195,19 +5195,19 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } if (removed) { - SendMsgToChar("Запись удалена.\r\n", ch); + SendMsgToChar(" .\r\n", ch); DescriptorData *d; if ((d = DescriptorByUid(unique)) && d->character->IsFlagged(EPrf::kPklMode)) { if (!subcmd) { SendMsgToChar(d->character.get(), - "%sДружина '%s' удалила вас из списка своих врагов!%s\r\n", + "%s '%s' !%s\r\n", kColorGrn, CLAN(ch)->name.c_str(), kColorNrm); } else { SendMsgToChar(d->character.get(), - "%sДружина '%s' удалила вас из списка своих друзей!%s\r\n", + "%s '%s' !%s\r\n", kColorBoldRed, CLAN(ch)->name.c_str(), kColorNrm); @@ -5215,22 +5215,22 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SetWait(ch, 1, false); } } else { - SendMsgToChar("Запись не найдена.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } else { utils::Trim(buffer); bool online = 1; - if (CompareParam(buffer, "all") || CompareParam(buffer, "все")) + if (CompareParam(buffer, "all") || CompareParam(buffer, "")) online = 0; if (online) SendMsgToChar(ch, - "%sОтображаются только находящиеся в игре персонажи:%s\r\n\r\n", + "%s :%s\r\n\r\n", kColorWht, kColorNrm); else - SendMsgToChar(ch, "%sСписок отображается полностью:%s\r\n\r\n", kColorWht, kColorNrm); + SendMsgToChar(ch, "%s :%s\r\n\r\n", kColorWht, kColorNrm); std::ostringstream out; @@ -5252,7 +5252,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (!out.str().empty()) page_string(ch->desc, out.str()); else { - SendMsgToChar("По вашему запросу никого не найдено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); if (CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { buffer = CLAN_PKLIST_FORMAT[0]; buffer += CLAN_PKLIST_FORMAT[1]; @@ -5263,7 +5263,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } } -// обработка клановых привилегий (команда house) +// ( house) void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; @@ -5271,31 +5271,31 @@ void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { std::string buffer = argument, buffer2; GetOneParam(buffer, buffer2); - // если игрок неклановый, то есть вариант с приглашением + // , if (!CLAN(ch)) { - if (CompareParam(buffer2, "согласен") && ch->desc->clan_invite) + if (CompareParam(buffer2, "") && ch->desc->clan_invite) ch->desc->clan_invite->clan->ClanAddMember(ch, ch->desc->clan_invite->rank, ch->desc->clan_invite->invite_name); - else if (CompareParam(buffer2, "отказать") && ch->desc->clan_invite) { + else if (CompareParam(buffer2, "") && ch->desc->clan_invite) { ch->desc->clan_invite.reset(); - SendMsgToChar("Приглашение дружины отклонено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } else - SendMsgToChar("Данная команда доступна только высоко привилегированным членам дружин,\r\n" - "а если вас пригласили вступить в оную, то так и пишите 'клан согласен' или 'клан отказать'.\r\n" - "Никто не сможет послать вам новое приглашение до тех пор, пока вы не разберетесь с этим.\r\n", + SendMsgToChar(" ,\r\n" + " , ' ' ' '.\r\n" + " , .\r\n", ch); return; } - if (CompareParam(buffer2, "информация") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_INFO]) + if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_INFO]) CLAN(ch)->HouseInfo(ch); - else if (CompareParam(buffer2, "принять") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_ADD]) + else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_ADD]) CLAN(ch)->HouseAdd(ch, buffer); - else if (CompareParam(buffer2, "изгнать") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_REMOVE]) + else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_REMOVE]) CLAN(ch)->HouseRemove(ch, buffer); - else if (CompareParam(buffer2, "привилегии") + else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PRIVILEGES]) { std::shared_ptr temp_clan_olc(new ClanOLC); temp_clan_olc->clan = CLAN(ch); @@ -5303,59 +5303,59 @@ void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { ch->desc->clan_olc = temp_clan_olc; ch->desc->state = EConState::kClanedit; CLAN(ch)->MainMenu(ch->desc); - } else if (CompareParam(buffer2, "воевода") && !CLAN_MEMBER(ch)->rank_num) + } else if (CompareParam(buffer2, "") && !CLAN_MEMBER(ch)->rank_num) CLAN(ch)->HouseOwner(ch, buffer); - else if (CompareParam(buffer2, "статистика")) + else if (CompareParam(buffer2, "")) CLAN(ch)->HouseStat(ch, buffer); - else if (CompareParam(buffer2, "покинуть", true) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_EXIT]) + else if (CompareParam(buffer2, "", true) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_EXIT]) CLAN(ch)->HouseLeave(ch); - else if (CompareParam(buffer2, "налог") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_TAX]) + else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_TAX]) tax_manage(ch, buffer); - else if (CompareParam(buffer2, "сообщение") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_MOD]) { + else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_MOD]) { prepare_write_mod(ch, buffer); - } else if (CompareParam(buffer2, "пк")) { + } else if (CompareParam(buffer2, "")) { CLAN(ch)->pk_log.print(ch); - } else if (CompareParam(buffer2, "лог")) { + } else if (CompareParam(buffer2, "")) { CLAN(ch)->chest_log.print(ch, buffer); - } else if (CompareParam(buffer2, "сайт") && !CLAN_MEMBER(ch)->rank_num) { + } else if (CompareParam(buffer2, "") && !CLAN_MEMBER(ch)->rank_num) { CLAN(ch)->house_web_url(ch, buffer); } else { - // обработка списка доступных команд по званию персонажа - buffer = "Доступные вам привилегии дружины:\r\n"; + // + buffer = " :\r\n"; for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) if (CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][i]) buffer += HOUSE_FORMAT[i]; - // воевода до кучи может сам сменить у дружины воеводу + // if (!CLAN_MEMBER(ch)->rank_num) { - buffer += " клан воевода (имя)\r\n"; - buffer += " клан сайт (адрес сайта вашей дружины для 'справка сайтыдружин')\r\n"; + buffer += " ()\r\n"; + buffer += " ( ' ')\r\n"; } if (CLAN(ch)->storehouse) - buffer += " хранилище <фильтры>\r\n"; - buffer += " клан статистика "; + buffer += " <>\r\n"; + buffer += " "; if (!CLAN_MEMBER(ch)->rank_num) - buffer += " <имя|все|очистить|очистить деньги>\r\n"; + buffer += " <||| >\r\n"; else - buffer += " <имя|все>\r\n"; + buffer += " <|>\r\n"; if (!CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_POLITICS]) { - buffer += " политика (только просмотр)\r\n"; + buffer += " ( )\r\n"; } - buffer += " клан пк (список последних сражений)\r\n"; - buffer += " клан лог <без параметров|строка поиска>\r\n"; + buffer += " ( )\r\n"; + buffer += " < | >\r\n"; SendMsgToChar(buffer, ch); } } -// ктодружина (список соклановцев, находящихся онлайн) +// ( , ) void DoWhoClan(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } std::ostringstream buffer; - buffer << fmt::format(" Ваша дружина: {}{}{}.\r\n", kColorBoldRed, CLAN(ch)->abbrev, kColorNrm); - buffer << fmt::format(" {}Сейчас в игре Ваши соратники:{}\r\n\r\n", kColorWht, kColorNrm); + buffer << fmt::format(" : {}{}{}.\r\n", kColorBoldRed, CLAN(ch)->abbrev, kColorNrm); + buffer << fmt::format(" {} :{}\r\n\r\n", kColorWht, kColorNrm); DescriptorData *d; int num = 0; @@ -5364,13 +5364,13 @@ void DoWhoClan(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { buffer << " " << d->character->race_or_title().c_str() << "\r\n"; ++num; } - buffer << "\r\n Всего: " << num << ".\r\n"; + buffer << "\r\n : " << num << ".\r\n"; SendMsgToChar(buffer.str(), ch); } void DoShowPolitics(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } @@ -5382,8 +5382,8 @@ void DoShowPolitics(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int p1 = 0, p2 = 0; std::ostringstream buffer2; - buffer2 << "Отношения Вашей дружины с другими дружинами:\r\n" << - "Название Отношение Вашей дружины Отношение к вашей дружине\r\n"; + buffer2 << " :\r\n" << + " \r\n"; for (const auto &clanVictim : Clan::ClanList) { if ((clanVictim == CLAN(ch)) || ((*clanVictim).m_members.size() == 0)) { @@ -5408,7 +5408,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } std::string buffer = argument; std::ostringstream buffer3; - buffer3 << "Дружины, находящиеся в состоянии союза:\r\n"; + buffer3 << ", :\r\n"; if (!buffer.empty()) { Clan::ClanListType::const_iterator clan1; @@ -5419,7 +5419,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if (clan1 == Clan::ClanList.end() || (*clan1)->m_members.size() == 0) { - SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); + SendMsgToChar(" \r\n", ch); return; } @@ -5430,7 +5430,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if ((*clan1)->CheckPolitics((*clan2)->rent) == kPoliticsAlliance) { - buffer3 << " " << (*clan1)->abbrev << " помогает " << (*clan2)->abbrev << "\r\n"; + buffer3 << " " << (*clan1)->abbrev << " " << (*clan2)->abbrev << "\r\n"; } } } else { @@ -5441,7 +5441,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if (clan1->CheckPolitics(clan2->rent) == kPoliticsAlliance) { - buffer3 << " " << clan1->abbrev << " помогает " << clan2->abbrev << "\r\n"; + buffer3 << " " << clan1->abbrev << " " << clan2->abbrev << "\r\n"; } } } @@ -5451,19 +5451,19 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } -// вобщем это копи-паст из биржи + флаги +// - + void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("Чаво?\r\n", ch); + SendMsgToChar("?\r\n", ch); return; } if (!CLAN(ch)->storehouse) { - SendMsgToChar("Ваш воевода зажал денег и отключил эту возможность! :(\r\n", ch); + SendMsgToChar(" ! :(\r\n", ch); return; } char *stufina = one_argument(argument, arg); - if (!str_cmp(arg, "все") || !str_cmp(arg, "all")) { + if (!str_cmp(arg, "") || !str_cmp(arg, "all")) { for (auto chest : world[GetRoomRnum(CLAN(ch)->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { Clan::ChestShow(chest, ch); @@ -5471,9 +5471,9 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - if (utils::IsAbbr(arg, "характеристики") || utils::IsAbbr(arg, "identify") || utils::IsAbbr(arg, "опознать")) { + if (utils::IsAbbr(arg, "") || utils::IsAbbr(arg, "identify") || utils::IsAbbr(arg, "")) { if ((currencies::GetBank(*ch, currencies::kGold) < kChestIdentPay) && (GetRealLevel(ch) < kLvlImplementator)) { - SendMsgToChar("У вас недостаточно денег в банке для такого исследования.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } @@ -5481,11 +5481,11 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (Clan::is_clan_chest(chest)) { ObjData *tmp_obj = get_obj_in_list_vis(ch, stufina, chest->get_contains()); if (tmp_obj) { - SendMsgToChar(ch, "Характеристики предмета: %s\r\n", stufina); + SendMsgToChar(ch, " : %s\r\n", stufina); MortShowObjValues(tmp_obj, ch, 200); currencies::RemoveBank(*ch, currencies::kGold, kChestIdentPay); SendMsgToChar(ch, - "%sЗа информацию о предмете с вашего банковского счета сняли %d %s%s\r\n", + "%s %d %s%s\r\n", kColorBoldGrn, kChestIdentPay, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(kChestIdentPay, grammar::ECase::kNom).c_str(), @@ -5494,7 +5494,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - sprintf(buf1, "Ничего похожего на %s в хранилище ненайдено! Будьте внимательнее.\r\n", stufina); + sprintf(buf1, " %s ! .\r\n", stufina); SendMsgToChar(buf1, ch); return; } @@ -5504,7 +5504,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!filter.parse_filter(ch, filter, argument)) { return; } - SendMsgToChar(ch, "Выборка: %s\r\n", filter.print().c_str()); + SendMsgToChar(ch, ": %s\r\n", filter.print().c_str()); SetWait(ch, 1, false); std::string out; @@ -5522,7 +5522,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!out.empty()) page_string(ch->desc, out); else - SendMsgToChar("Ничего не найдено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -5530,12 +5530,12 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int cnt = 0, gold_total = 0; if (!CLAN(ch)) { - SendMsgToChar("Сначала вступите в какой-нибудь клан.\r\n", ch); + SendMsgToChar(" - .\r\n", ch); return; } if (GET_ROOM_VNUM(ch->in_room) != CLAN(ch)->GetRent()) { - SendMsgToChar("Получить клановую экипировку вы можете только в центре вашего замка.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } @@ -5578,8 +5578,8 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (!cnt) { - act("$n открыл$g крышку сундука со стандартной экипировкой\r\n", false, ch, nullptr, nullptr, kToRoom); - act("Вы открыли крышку сундука со стандартной экипировкой\r\n", false, ch, nullptr, nullptr, kToChar); + act("$n $g \r\n", false, ch, nullptr, nullptr, kToRoom); + act(" \r\n", false, ch, nullptr, nullptr, kToChar); } int gold = obj->get_cost(); @@ -5588,27 +5588,27 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { currencies::RemoveHand(*ch, currencies::kGold, gold); gold_total += gold; } else { - SendMsgToChar(ch, "Кончились денюжки!\r\n"); + SendMsgToChar(ch, " !\r\n"); break; } PlaceObjToInventory(obj.get(), ch); cnt++; - sprintf(buf, "$n взял$g %s из сундука", obj->get_PName(grammar::ECase::kNom).c_str()); - sprintf(buf2, "Вы взяли %s из сундука", obj->get_PName(grammar::ECase::kNom).c_str()); + sprintf(buf, "$n $g %s ", obj->get_PName(grammar::ECase::kNom).c_str()); + sprintf(buf2, " %s ", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, ch, 0, 0, kToRoom); act(buf2, false, ch, 0, 0, kToChar); } if (cnt) { - sprintf(buf2, "\r\nЭкипировка обошлась вам в %d %s.", gold_total, + sprintf(buf2, "\r\n %d %s.", gold_total, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold_total, grammar::ECase::kNom).c_str()); - act("\r\n$n закрыл$g крышку сундука", false, ch, 0, 0, kToRoom); + act("\r\n$n $g ", false, ch, 0, 0, kToRoom); act(buf2, false, ch, 0, 0, kToChar); } else { - act("\r\n$n порыл$u в сундуке со стандартной экипировкой, но ничего не наш$y", false, ch, 0, 0, kToRoom); - act("\r\nВы порылись в сундуке со стандартной экипировкой, но не нашли ничего подходящего", + act("\r\n$n $u , $y", false, ch, 0, 0, kToRoom); + act("\r\n , ", false, ch, 0, @@ -5619,7 +5619,7 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SetWait(ch, 1, false); } -// божественный hcontrol +// hcontrol void DoHcontrol(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; diff --git a/src/gameplay/communication/boards/boards_changelog_loaders.cpp b/src/gameplay/communication/boards/boards_changelog_loaders.cpp index fccd26846..3f13c0a45 100644 --- a/src/gameplay/communication/boards/boards_changelog_loaders.cpp +++ b/src/gameplay/communication/boards/boards_changelog_loaders.cpp @@ -24,7 +24,7 @@ void ChangeLogLoaderImplementation::add_message(const std::string &author, const std::string &desc, time_t parsed_time) { const auto message = std::make_shared(); - // имя автора + // std::string author_copy = author; const std::size_t e_pos = author_copy.find('<'); const std::size_t s_pos = author_copy.find(' '); @@ -39,7 +39,7 @@ void ChangeLogLoaderImplementation::add_message(const std::string &author, utils::Trim(desc_copy); message->text = desc_copy; - // из текста первая строка в заголовок + // std::string subj(message->text.begin(), std::find(message->text.begin(), message->text.end(), '\n')); /* if (subj.size() > 40) { @@ -204,8 +204,8 @@ bool GitChangeLogLoader::GitCommitReader::next_line(std::string &buffer, std::is if (!std::getline(is, buffer)) { return false; } - // Changelog лежит на диске в KOI8-R и читается потоком; границу кодировки проходим здесь - // (issue #3681). Под KOI8-R это тождество. + // Changelog KOI8-R ; + // (issue #3681). KOI8-R . buffer = native_text::from_disk_line(buffer.c_str()); ++line; diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index a1834d206..3fb85bdd7 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -41,21 +41,21 @@ extern int invalid_anti_class(CharData *ch, const ObjData *obj); namespace Parcel { void parcel_log(const char *format, ...) __attribute__((format(printf, 1, 2))); -const int KEEP_TIMER = 60 * 24 * 3; // 3 суток ждет на почте (в минутах) -const int SEND_COST = 100; // в любом случае снимается за посылку шмотки -const int RESERVED_COST_COEFF = 3; // цена ренты за 3 дня -const int MAX_SLOTS = 25; // сколько шмоток может находиться в отправке от одного игрока -const int RETURNED_TIMER = -1; // при развороте посылки идет двойной таймер шмоток без капания ренты +const int KEEP_TIMER = 60 * 24 * 3; // 3 ( ) +const int SEND_COST = 100; // +const int RESERVED_COST_COEFF = 3; // 3 +const int MAX_SLOTS = 25; // +const int RETURNED_TIMER = -1; // const char *FILE_NAME = LIB_USERDATA"parcel.db"; -// для возврата посылки отправителю +// const bool RETURN_WITH_MONEY = 1; const bool RETURN_NO_MONEY = 0; -// доставленные с ребута посылки +// static int was_sended = 0; -// для групповой отсылки шмоток (чтобы не спамить на каждую) +// ( ) static std::string send_buffer; static int send_cost_buffer = 0; static int send_reserved_buffer = 0; @@ -64,10 +64,10 @@ class Node { public: Node(int money, const ObjData::shared_ptr &obj) : money_(money), timer_(0), obj_(obj) {}; Node() : money_(0), timer_(0), obj_(nullptr) {}; - int money_; // резервированные средства - int timer_; // сколько минут шмотка уже ждет получателя (при значении выще KEEP_TIMER возвращается отправителю) + int money_; // + int timer_; // ( KEEP_TIMER ) ObjData::shared_ptr - obj_; // шмотка (здесь же берется таймер, при уходе в ноль - пурж и возврат оставшегося резерва) + obj_; // ( , - ) }; class LoadNode { @@ -77,13 +77,13 @@ class LoadNode { long target; }; -typedef std::map > SenderListType; -typedef std::map ParcelListType; +typedef std::map > SenderListType; +typedef std::map ParcelListType; -ParcelListType parcel_list; // список посылок -SenderListType return_list; // временный список на возврат +ParcelListType parcel_list; // +SenderListType return_list; // -// * Отдельный лог для отладки. +// * . void parcel_log(const char *format, ...) { const auto filename = runtime_config.log_dir() + "/parcel.log"; static FILE *file = 0; @@ -106,18 +106,18 @@ void parcel_log(const char *format, ...) { fflush(file); } -// * Уведомление чара (если он онлайна) о новой посылке. +// * ( ) . void invoice(long uid) { DescriptorData *d = DescriptorByUid(uid); if (d) { if (!has_parcel(d->character.get())) { - SendMsgToChar(d->character.get(), "%sВам пришла посылка, зайдите на почту и распишитесь!%s\r\n", + SendMsgToChar(d->character.get(), "%s , !%s\r\n", kColorWht, kColorNrm); } } } -// * Добавление шмотки в список посылок. +// * . void add_parcel(long target, long sender, const Node &tmp_node) { invoice(target); ParcelListType::iterator it = parcel_list.find(target); @@ -139,7 +139,7 @@ void add_parcel(long target, long sender, const Node &tmp_node) { } } -// * Сколько всего предметов уже посылается данным персонажем (для ограничения). +// * ( ). int total_sended(CharData *ch) { int sended = 0; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -153,23 +153,23 @@ int total_sended(CharData *ch) { return sended; } -// * Проверка возможности отправить шмотку почтой. +// * . bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { if (obj->has_flag(EObjFlag::kNodrop) || obj->is_unrentable() || obj->has_flag(EObjFlag::kDecay) || obj->get_owner()) { - snprintf(buf, kMaxStringLength, "$n сказал$g вам : '%s - мы не отправляем такие вещи!'\r\n", + snprintf(buf, kMaxStringLength, "$n $g : '%s - !'\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; } else if (obj->get_type() == EObjType::kContainer && obj->get_contains()) { - snprintf(buf, kMaxStringLength, "$n сказал$g вам : 'В %s что-то лежит.'\r\n", obj->get_PName(grammar::ECase::kPre).c_str()); + snprintf(buf, kMaxStringLength, "$n $g : ' %s - .'\r\n", obj->get_PName(grammar::ECase::kPre).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; } else if (SetSystem::is_big_set(obj)) { - snprintf(buf, kMaxStringLength, "$n сказал$g вам : '%s является частью большого набора предметов.'\r\n", + snprintf(buf, kMaxStringLength, "$n $g : '%s .'\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; @@ -181,17 +181,17 @@ bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { if (invalid_anti_class(&t_vict, obj)) { switch ((&t_vict)->get_sex()) { case EGender::kMale: - act("$n сказал$g вам : 'Знаю я такого добра молодца - эта вещь явно на него не налезет.'\r\n", + act("$n $g : ' - .'\r\n", false, mailman, 0, ch, kToVict); break; case EGender::kFemale: - act("$n сказал$g вам : 'Знаю я такую красну девицу - эта вещь явно на нее не налезет.'\r\n", + act("$n $g : ' - .'\r\n", false, mailman, 0, ch, kToVict); break; default: - act("$n сказал$g вам : 'Знаю я сие чудо бесполое - эта вещь явно на него не налезет.'\r\n", + act("$n $g : ' - .'\r\n", false, mailman, 0, ch, kToVict); } return 0; @@ -199,7 +199,7 @@ bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { return 1; } -// получаем все объекты, которые были отправлены чару +// , std::vector get_objs(long char_uid) { std::vector buf_vector; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -213,10 +213,10 @@ std::vector get_objs(long char_uid) { return buf_vector; } -// * Отправка предмета (снятие/резервирование денег, вывод из списка предметов). +// * (/ , ). void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { if (!ch || !mailman || !vict_uid || !obj) { - log("Parcel: нулевой входной параметр: %d, %d, %d, %d (%s %s %d)", + log("Parcel: : %d, %d, %d, %d (%s %s %d)", ch ? 1 : 0, mailman ? 1 : 0, vict_uid ? 1 : 0, obj ? 1 : 0, __FILE__, __func__, __LINE__); return; } @@ -227,30 +227,30 @@ void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { const int total_cost = reserved_cost + SEND_COST; if (currencies::GetTotal(*ch, currencies::kGold) < total_cost) { - act("$n сказал$g вам : 'Да у тебя ведь нет столько денег!'", false, mailman, 0, ch, kToVict); + act("$n $g : ' !'", false, mailman, 0, ch, kToVict); return; } if (total_sended(ch) >= MAX_SLOTS) { - act("$n сказал$g вам : 'Ты уже и так отправил кучу вещей! Подожди, пока их получат адресаты!'", + act("$n $g : ' ! , !'", false, mailman, 0, ch, kToVict); return; } std::string name = GetNameByUnique(vict_uid); if (name.empty()) { - act("$n сказал$g вам : 'Ошибка в имени получателя, сообщите Богам!'", false, mailman, 0, ch, kToVict); + act("$n $g : ' , !'", false, mailman, 0, ch, kToVict); return; } if (SetSystem::is_norent_set(ch, obj) && SetSystem::is_norent_set(GET_OBJ_VNUM(obj), get_objs(ch->get_uid()))) { - snprintf(buf, kMaxStringLength, "%s - требуется две и более вещи из набора.\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); + snprintf(buf, kMaxStringLength, "%s - .\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); SendMsgToChar(utils::CAP(buf), ch); return; } name_convert(name); if (send_buffer.empty()) - send_buffer += "Адресат: " + name + ", отправлено:\r\n"; + send_buffer += ": " + name + ", :\r\n"; snprintf(buf, sizeof(buf), "%s%s%s\r\n", kColorWht, obj->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); send_buffer += buf; @@ -269,16 +269,16 @@ void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { check_auction(nullptr, obj); } -// * Отправка предмета, дергается из спешиала почты ('отправить имя предмет)'. +// * , (' )'. void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (ch->IsNpc()) return; if (ch->get_uid() == vict_uid) { - act("$n сказал$g вам : 'Не загружай понапрасну почту!'", false, mailman, 0, ch, kToVict); + act("$n $g : ' !'", false, mailman, 0, ch, kToVict); return; } if (NORENTABLE(ch)) { - act("$n сказал$g вам : 'Да у тебя руки по локоть в крови, проваливай!'", false, mailman, 0, ch, kToVict); + act("$n $g : ' , !'", false, mailman, 0, ch, kToVict); return; } @@ -290,17 +290,17 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (is_number(tmp_arg)) { int amount = atoi(tmp_arg); - if (!strn_cmp("coin", tmp_arg2, 4) || !strn_cmp("кун", tmp_arg2, 5) || !str_cmp("денег", tmp_arg2)) { - act("$n сказал$g вам : 'Для перевода денег воспользуйтесь услугами банка.'", + if (!strn_cmp("coin", tmp_arg2, 4) || !strn_cmp("", tmp_arg2, 5) || !str_cmp("", tmp_arg2)) { + act("$n $g : ' .'", false, mailman, 0, ch, kToVict); return; - } else if (!str_cmp("все", tmp_arg2) || !str_cmp("all", tmp_arg2)) { + } else if (!str_cmp("", tmp_arg2) || !str_cmp("all", tmp_arg2)) { if (!ch->carrying) { - SendMsgToChar("У вас ведь ничего нет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } for (obj = ch->carrying; obj && amount; obj = next_obj) { @@ -309,9 +309,9 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { send_object(ch, mailman, vict_uid, obj); } } else if (!*tmp_arg2) { - SendMsgToChar(ch, "Чего %d вы хотите отправить?\r\n", amount); + SendMsgToChar(ch, " %d ?\r\n", amount); } else if (!(obj = get_obj_in_list_vis(ch, tmp_arg2, ch->carrying))) { - SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg2); + SendMsgToChar(ch, " '%s'.\r\n", tmp_arg2); } else { while (obj && amount--) { next_obj = get_obj_in_list_vis(ch, tmp_arg2, obj->get_next_content()); @@ -323,17 +323,17 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { int dotmode = find_all_dots(tmp_arg); if (dotmode == kFindIndiv) { if (!(obj = get_obj_in_list_vis(ch, tmp_arg, ch->carrying))) { - SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg); + SendMsgToChar(ch, " '%s'.\r\n", tmp_arg); return; } send_object(ch, mailman, vict_uid, obj); } else { if (dotmode == kFindAlldot && !*tmp_arg) { - SendMsgToChar("Отправить \"все\" какого типа предметов?\r\n", ch); + SendMsgToChar(" \"\" ?\r\n", ch); return; } if (!ch->carrying) { - SendMsgToChar("У вас ведь ничего нет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { bool has_items = false; for (obj = ch->carrying; obj; obj = next_obj) { @@ -346,13 +346,13 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { } } if (!has_items) - SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg); + SendMsgToChar(ch, " '%s'.\r\n", tmp_arg); } } } if (!send_buffer.empty()) { - snprintf(buf, sizeof(buf), "с вас удержано %d %s и еще %d %s зарезервировано на 3 дня хранения.\r\n", + snprintf(buf, sizeof(buf), " %d %s %d %s 3 .\r\n", send_cost_buffer, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(send_cost_buffer, grammar::ECase::kNom).c_str(), send_reserved_buffer, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(send_reserved_buffer, grammar::ECase::kNom).c_str()); send_buffer += buf; @@ -364,10 +364,10 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { } } -// * Дергается из спешиала почты ('почта'). Распечатка отправленных посылок, которые еще не доставлены. +// * (''). , . void print_sending_stuff(CharData *ch) { std::stringstream out; - out << "\r\nВаши текущие посылки:"; + out << "\r\n :"; bool print = false; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { SenderListType::const_iterator it2 = it->second.find(ch->get_uid()); @@ -375,7 +375,7 @@ void print_sending_stuff(CharData *ch) { print = true; std::string name = GetNameByUnique(it->first); name_convert(name); - out << "\r\nАдресат: " << name << ", отправлено:\r\n" << kColorWht; + out << "\r\n: " << name << ", :\r\n" << kColorWht; int money = 0; for (std::list::const_iterator it3 = it2->second.begin(); it3 != it2->second.end(); ++it3) { @@ -383,14 +383,14 @@ void print_sending_stuff(CharData *ch) { money += it3->money_; } out << kColorNrm - << money << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str() << " зарезервировано на 3 дня хранения.\r\n"; + << money << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str() << " 3 .\r\n"; } } if (print) SendMsgToChar(out.str(), ch); } -// * Для учитывания предметов на почте в локейте. +// * . std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { for (auto i : parcel_list) { for (auto k : i.second) { @@ -404,7 +404,7 @@ std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { continue; } if (obj->get_id() == o.obj_->get_id()) { - return fmt::format("{} наход{}ся у почтового голубя в инвентаре.\r\n", o.obj_->get_short_description().c_str(), grammar::ObjPluralVerbEnding((o.obj_)->get_sex())); + return fmt::format("{} {} .\r\n", o.obj_->get_short_description().c_str(), grammar::ObjPluralVerbEnding((o.obj_)->get_sex())); } } } @@ -412,7 +412,7 @@ std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { return {}; } -// * Есть ли на чара какие-нить посылки. +// * - . bool has_parcel(CharData *ch) { ParcelListType::const_iterator it = parcel_list.find(ch->get_uid()); if (it != parcel_list.end()) @@ -421,7 +421,7 @@ bool has_parcel(CharData *ch) { return false; } -// * Возврат зарезервированных денег отправителю. +// * . void return_money(std::string const &name, int money, bool add) { if (money <= 0) { log("WARNING: money=%d (%s %s %d)", money, __FILE__, __func__, __LINE__); @@ -432,11 +432,11 @@ void return_money(std::string const &name, int money, bool add) { if ((vict = get_player_of_name(name.c_str()))) { if (add) { currencies::AddBank(*vict, currencies::kGold, money); - SendMsgToChar(vict, "%sВы получили %d %s банковским переводом от почтовой службы%s.\r\n", + SendMsgToChar(vict, "%s %d %s %s.\r\n", kColorWht, money, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str(), kColorNrm); } } else { - vict = new Player; // TODO: переделать на стек + vict = new Player; // TODO: if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) < 0) { delete vict; return; @@ -447,41 +447,41 @@ void return_money(std::string const &name, int money, bool add) { } } -// * Экстра-описание на самой посылке при получении. +// * - . void fill_ex_desc(CharData *ch, ObjData *obj, std::string sender) { size_t size = std::max(strlen(GET_NAME(ch)), sender.size()); std::stringstream out; out.setf(std::ios_base::left); - out << " Разваливающийся на глазах ящик с явными признаками\r\n" - "неуклюжего взлома - царская служба безопасности бдит...\r\n" - "На табличке сбоку видны надписи:\r\n\r\n"; + out << " \r\n" + " - ...\r\n" + " :\r\n\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; - out << "| Отправитель: " << fmt::format("{:>{}}", sender, size) - << " |\r\n| Получатель: " << fmt::format("{:>{}}", GET_NAME(ch), size) << " |\r\n"; + out << "| : " << fmt::format("{:>{}}", sender, size) + << " |\r\n| : " << fmt::format("{:>{}}", GET_NAME(ch), size) << " |\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; - obj->set_ex_description("посылка бандероль пакет ящик parcel box case chest", out.str().c_str()); + obj->set_ex_description(" parcel box case chest", out.str().c_str()); } -// * Расчет стоимости ренты за предмет, пока он лежал на почте. +// * , . int calculate_timer_cost(std::list::iterator const &it) { return static_cast((get_object_low_rent(it->obj_.get()) / (24.0 * 60.0)) * it->timer_); } -// * Генерим сам контейнер посылку. +// * . ObjData *create_parcel() { const auto obj = world_objects.create_blank(); - obj->set_aliases("посылка бандероль пакет ящик parcel box case chest"); - obj->set_short_description("посылка"); - obj->set_description("Кто-то забыл здесь свою посылку."); - obj->set_PName(grammar::ECase::kNom, "посылка"); - obj->set_PName(grammar::ECase::kGen, "посылки"); - obj->set_PName(grammar::ECase::kDat, "посылке"); - obj->set_PName(grammar::ECase::kAcc, "посылку"); - obj->set_PName(grammar::ECase::kIns, "посылкой"); - obj->set_PName(grammar::ECase::kPre, "посылке"); + obj->set_aliases(" parcel box case chest"); + obj->set_short_description(""); + obj->set_description("- ."); + obj->set_PName(grammar::ECase::kNom, ""); + obj->set_PName(grammar::ECase::kGen, ""); + obj->set_PName(grammar::ECase::kDat, ""); + obj->set_PName(grammar::ECase::kAcc, ""); + obj->set_PName(grammar::ECase::kIns, ""); + obj->set_PName(grammar::ECase::kPre, ""); obj->set_sex(EGender::kFemale); obj->set_type(EObjType::kContainer); obj->set_wear_flags(to_underlying(EWearFlag::kTake)); @@ -496,13 +496,13 @@ ObjData *create_parcel() { return obj.get(); } -// * Получение посылки на почте, дергается из спешиала почты. ('получить'). +// * , . (''). void receive(CharData *ch, CharData *mailman) { if (((ch->in_room == r_helled_start_room) || (ch->in_room == r_named_start_room) || (ch->in_room == r_unreg_start_room)) && has_parcel(ch)) { - act("$n сказал$g вам : 'А посылку-то не получишь, сюда не доставляем.'", false, mailman, 0, ch, kToVict); + act("$n $g : ' - , .'", false, mailman, 0, ch, kToVict); return; } @@ -518,15 +518,15 @@ void receive(CharData *ch, CharData *mailman) { int money = 0; for (std::list::iterator it3 = it2->second.begin(); it3 != it2->second.end(); ++it3) { money += it3->money_ - calculate_timer_cost(it3); - // добавляем в глоб.список и кладем в посылку + // . PlaceObjIntoObj(it3->obj_.get(), obj); } return_money(name, money, RETURN_WITH_MONEY); PlaceObjToInventory(obj, ch); - snprintf(buf, kMaxStringLength, "$n дал$g вам посылку (отправитель %s).", name.c_str()); + snprintf(buf, kMaxStringLength, "$n $g ( %s).", name.c_str()); act(buf, false, mailman, 0, ch, kToVict); - act("$N дал$G $n2 посылку.", false, ch, 0, mailman, kToRoom); + act("$N $G $n2 .", false, ch, 0, mailman, kToRoom); ++was_sended; } ObjSaveSync::add(ch->get_uid(), ch->get_uid(), ObjSaveSync::PARCEL_SAVE); @@ -534,17 +534,17 @@ void receive(CharData *ch, CharData *mailman) { } } -// * Отправка сообщения через письмо, с уведомлением чару, если тот онлайн. +// * , , . void create_mail(int to_uid, int from_uid, char *text) { mail::add(to_uid, from_uid, text); const DescriptorData *i = DescriptorByUid(to_uid); if (i) { - SendMsgToChar(i->character.get(), "%sВам пришло письмо, зайдите на почту и распишитесь!%s\r\n", + SendMsgToChar(i->character.get(), "%s , !%s\r\n", kColorWht, kColorNrm); } } -// * Формирование временного списка возвращенных предметов (из основного удалены). +// * ( ). void prepare_return(const long uid, const std::list::iterator &it) { Node tmp_node(0, it->obj_); tmp_node.timer_ = RETURNED_TIMER; @@ -559,7 +559,7 @@ void prepare_return(const long uid, const std::list::iterator &it) { } } -// * Возврат предметов из временного списка (перекидывание их в основной список посылок). +// * ( ). void return_parcel() { for (SenderListType::iterator it = return_list.begin(); it != return_list.end(); ++it) { for (std::list::iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { @@ -571,19 +571,19 @@ void return_parcel() { return_list.clear(); } -// * Дикей предмета на почте и уведомление об этом отправителя и получателя через письма. +// * . void extract_parcel(int sender_uid, int target_uid, const std::list::iterator &it) { - snprintf(buf, kMaxStringLength, "С прискорбием сообщаем вам: %s рассыпал%s в прах.\r\n", + snprintf(buf, kMaxStringLength, " : %s %s .\r\n", it->obj_->get_short_description().c_str(), grammar::ObjSexEnding((it->obj_)->get_sex(), 2)); char *tmp = str_dup(buf); - // -1 в качестве ид отправителя при получении подставит в имя почтовую службу + // -1 create_mail(sender_uid, -1, tmp); create_mail(target_uid, -1, tmp); free(tmp); - // возврат оставшихся зарезервированных кун отправителю (у развернутых уже ноль) + // ( ) if (it->money_ && it->timer_ != RETURNED_TIMER) { int money_return = it->money_ - calculate_timer_cost(it); std::string name = GetNameByUnique(sender_uid); @@ -593,31 +593,31 @@ void extract_parcel(int sender_uid, int target_uid, const std::list::itera ExtractObjFromWorld(it->obj_.get()); } -// * Генерация письма о возврате посылки. +// * . void return_invoice(int uid, ObjData *obj) { - snprintf(buf, kMaxStringLength, "Посылка возвращена отправителю: %s.\r\n", + snprintf(buf, kMaxStringLength, " : %s.\r\n", obj->get_short_description().c_str()); char *tmp = str_dup(buf); create_mail(uid, -1, tmp); free(tmp); } -// * Чтение записи, включающей инфу почты и сам предмет после нее. +// * , . LoadNode parcel_read_one_object(char **data, int *error) { LoadNode tmp_node; *error = 1; - // Станем на начало предмета (#) + // (#) for (; **data != '#'; (*data)++) if (!**data || **data == '$') return tmp_node; - // Пропустим # + // # (*data)++; char buffer[kMaxStringLength]; *error = 2; - // отправитель + // if (!get_buf_line(data, buffer)) return tmp_node; *error = 3; @@ -625,7 +625,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 4; - // получатель + // if (!get_buf_line(data, buffer)) return tmp_node; *error = 5; @@ -633,7 +633,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 6; - // зарезервированная сумма + // if (!get_buf_line(data, buffer)) return tmp_node; *error = 7; @@ -641,7 +641,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 8; - // таймер ожидания на почте + // if (!get_buf_line(data, buffer)) return tmp_node; *error = 9; @@ -657,7 +657,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; } -// * Загрузка предметов и тех.информации (при ребуте). +// * . ( ). void load() { FILE *fl; if (!(fl = fopen(FILE_NAME, "r"))) { @@ -696,13 +696,13 @@ void load() { return; } add_parcel(node.target, node.sender, node.obj_node); - // из глобального списка изымаем + // } free(readdata); } -// * Сохранение предметов и тех.информации (при апдейте таймеров). +// * . ( ). void save() { log("Save obj: parcel"); ObjSaveSync::check(0, ObjSaveSync::PARCEL_SAVE); @@ -719,7 +719,7 @@ void save() { } out << "$\n$\n"; - // скидываем в файл + // std::ofstream file(FILE_NAME); if (!file.is_open()) { log("SYSERR: error opening file: %s! (%s %s %d)", FILE_NAME, __FILE__, __func__, __LINE__); @@ -731,7 +731,7 @@ void save() { return; } -// * Обновление таймеров у предметов + таймеров ожидания на почте. Пурж/возврат по надобности. +// * + . / . void update_timers() { for (ParcelListType::iterator it = parcel_list.begin(); it != parcel_list.end(); /* empty */) { for (SenderListType::iterator it2 = it->second.begin(); it2 != it->second.end(); /* empty */) { @@ -739,19 +739,19 @@ void update_timers() { for (std::list::iterator it3 = it2->second.begin(); it3 != it2->second.end(); it3 = tmp_it) { tmp_it = it3; ++tmp_it; - // После ObjDecayManager::process_tick объекты с истёкшим - // дедлайном удаляются из индексов decay_manager, но shared_ptr - // здесь продолжает их держать. ObjData::get_timer() в этом - // случае возвращает прото-таймер (обычно > 0), и проверка - // timer == 0 такие объекты пропускает -- симптоматика ровно - // как в #3239 для депо. + // ObjDecayManager::process_tick ԣ + // decay_manager, shared_ptr + // . ObjData::get_timer() + // - ( > 0), + // timer == 0 -- + // #3239 . const bool expired_by_decay = !world_objects.decay_manager().contains(it3->obj_.get()); if (expired_by_decay || it3->obj_->get_timer() == 0) { extract_parcel(it2->first, it->first, it3); it2->second.erase(it3); } else { if (it3->timer_ == RETURNED_TIMER) { - // шмотка уже развернута отправителю, рента не капает, но таймер идет два раза + // , , if (it3->obj_->get_timer() > 1) it3->obj_->dec_timer(); if (it3->obj_->get_timer() == 0) { @@ -763,10 +763,10 @@ void update_timers() { if (it3->timer_ >= KEEP_TIMER) { return_invoice(it->first, it3->obj_.get()); prepare_return(it2->first, it3); - // тут надо штатно уменьшить счетчики у плеера + // std::string name = GetNameByUnique(it2->first); return_money(name, it3->money_, RETURN_NO_MONEY); - // и удалить запись (она уйдет в разворот позже в return_parcel) + // ( return_parcel) it2->second.erase(it3); } } @@ -786,7 +786,7 @@ void update_timers() { save(); } -// * Иммский 'show stats' интересу и статистики ради. +// * 'show stats' . void show_stats(CharData *ch) { int targets = 0, returned = 0, objs = 0, reserved_money = 0; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -800,7 +800,7 @@ void show_stats(CharData *ch) { } } } - SendMsgToChar(ch, " Почта: предметов в ожидании %d, доставлено с ребута %d\r\n", objs, was_sended); + SendMsgToChar(ch, " : %d, %d\r\n", objs, was_sended); } int delete_obj(int vnum) { @@ -818,7 +818,7 @@ int delete_obj(int vnum) { return num; } -// * Иммское 'где' для учета предметов на почте. +// * '' . bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { bool found = false; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -829,7 +829,7 @@ bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { std::string sender = GetNameByUnique(it2->first); found = true; - SendMsgToChar(fmt::format("{:2}. [{:6}] {:<25} - наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", + SendMsgToChar(fmt::format("{:2}. [{:6}] {:<25} - {} (: {}, : {}).\r\n", num++, GET_OBJ_VNUM(it3->obj_.get()), it3->obj_->get_short_description(), @@ -855,7 +855,7 @@ std::string FindParcelObj(const ObjData *obj) { native_text::capitalize_first(target); native_text::capitalize_first(sender); - str = fmt::format("наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", + str = fmt::format("{} (: {}, : {}).\r\n", grammar::ObjPluralVerbEnding((it3->obj_)->get_sex()), sender.c_str(), target.c_str()); @@ -867,7 +867,7 @@ std::string FindParcelObj(const ObjData *obj) { return ""; } -// * Обновление полей объектов при изменении их прототипа через олц. +// * . void olc_update_from_proto(int robj_num, ObjData *olc_proto) { for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { for (SenderListType::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { @@ -880,7 +880,7 @@ void olc_update_from_proto(int robj_num, ObjData *olc_proto) { } } -// * Поиск цели для каста локейта. +// * . ObjData *locate_object(const char *str) { for (ParcelListType::const_iterator i = parcel_list.begin(); i != parcel_list.end(); ++i) { for (SenderListType::const_iterator k = i->second.begin(); k != i->second.end(); ++k) { @@ -894,7 +894,7 @@ ObjData *locate_object(const char *str) { return nullptr; } -// * Возврат всех ждущих посылок их отправителю. +// * . void bring_back(CharData *ch, CharData *mailman) { int money = 0; bool empty = true; @@ -906,15 +906,15 @@ void bring_back(CharData *ch, CharData *mailman) { } empty = false; ObjData *obj = create_parcel(); - fill_ex_desc(ch, obj, std::string("Отдел возвратов")); + fill_ex_desc(ch, obj, std::string(" ")); for (std::list::iterator l = k->second.begin(); l != k->second.end(); ++l) { money += l->money_ - calculate_timer_cost(l); PlaceObjIntoObj(l->obj_.get(), obj); } PlaceObjToInventory(obj, ch); - snprintf(buf, kMaxStringLength, "$n дал$g вам посылку."); + snprintf(buf, kMaxStringLength, "$n $g ."); act(buf, false, mailman, 0, ch, kToVict); - act("$N дал$G $n2 посылку.", false, ch, 0, mailman, kToRoom); + act("$N $G $n2 .", false, ch, 0, mailman, kToRoom); i->second.erase(k); if (i->second.empty()) { @@ -924,13 +924,13 @@ void bring_back(CharData *ch, CharData *mailman) { } } if (!empty && money > 0) { - act("$n сказал$g вам : 'За экстренный возврат посылок с вас удержана половина зарезервированных кун.'", + act("$n $g : ' .'", false, mailman, 0, ch, kToVict); std::string name = GET_NAME(ch); return_money(name, money / 2, RETURN_WITH_MONEY); ObjSaveSync::add(ch->get_uid(), ch->get_uid(), ObjSaveSync::PARCEL_SAVE); } else if (empty) { - act("$n сказал$g вам : 'У нас нет ни одной вашей посылки!'", false, mailman, 0, ch, kToVict); + act("$n $g : ' !'", false, mailman, 0, ch, kToVict); } } diff --git a/src/gameplay/crafting/item_creation.cpp b/src/gameplay/crafting/item_creation.cpp index 6c8bfe51c..246f3c0b3 100644 --- a/src/gameplay/crafting/item_creation.cpp +++ b/src/gameplay/crafting/item_creation.cpp @@ -58,99 +58,99 @@ struct create_item_type created_item[] = {310, 0x3A, 4, 10, {{COAL_PROTO, 0, 0}}, ESkill::kReforging, WEAR_TAKE_HEAD}, {312, 0, 4, 40, {{WOOD_PROTO, TETIVA_PROTO, 0}}, ESkill::kCreateBow, WEAR_TAKE_BOTHS} }; -const char *create_item_name[] = {"шелепуга", - "меч", - "сабля", - "нож", - "топор", - "плеть", - "дубина", - "кольчуга", - "наручи", - "поножи", - "шлем", - "лук", +const char *create_item_name[] = {"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", "\n" }; const struct make_skill_type make_skills[] = { - {"смастерить предмет", "предметы", ESkill::kMakeStaff}, - {"смастерить лук", "луки", ESkill::kMakeBow}, - {"выковать оружие", "оружие", ESkill::kMakeWeapon}, - {"выковать доспех", "доспех", ESkill::kMakeArmor}, - {"сшить одежду", "одежда", ESkill::kMakeWear}, - {"смастерить диковину", "артеф.", ESkill::kMakeJewel}, - {"смастерить оберег", "оберег", ESkill::kMakeAmulet}, -// { "сварить отвар","варево", ESkill::kMakePotion }, - {"\n", "\n", ESkill::kUndefined} // Терминатор + {" ", "", ESkill::kMakeStaff}, + {" ", "", ESkill::kMakeBow}, + {" ", "", ESkill::kMakeWeapon}, + {" ", "", ESkill::kMakeArmor}, + {" ", "", ESkill::kMakeWear}, + {" ", ".", ESkill::kMakeJewel}, + {" ", "", ESkill::kMakeAmulet}, +// { " ","", ESkill::kMakePotion }, + {"\n", "\n", ESkill::kUndefined} // }; const char *create_weapon_quality[] = {"RESERVED", "RESERVED", "RESERVED", "RESERVED", "RESERVED", - "невообразимо ужасного качества", // <3 // - "ужасного качества", // 3 // - "более чем жуткого качества", - "жуткого качества", // 4 // - "более чем отвратительного качества", - "отвратительного качества", // 5 // - "более чем плохого качества", - "плохого качества", // 6 // - "хуже чем среднего качества", - "среднего качества", // 7 // - "лучше чем среднего качества", - "неплохого качества", // 8 // - "более чем неплохого качества", - "хорошего качества", // 9 // - "более чем хорошего качества", - "превосходного качества", // 10 // - "более чем превосходного качества", - "великолепного качества", // 11 // - "более чем великолепного качества", - "качества, достойного учеников мастера", // 12 // - "лучшего качества, чем делают ученики мастера", - "качества, достойного мастера", // 13 // - "лучшего качества, чем делают мастера", - "качества, достойного великого мастера", // 14 // - "лучшего качества, чем делают великие мастера", - "качества, достойного руки сына боярского", // 15 // - "качества, более чем достойного руки сына боярского", - "качества, достойного руки боярина", // 16 // - "качества, более чем достойного руки боярина", - "качества, достойного руки сына воеводы", // 17 // - "качества, более чем достойного руки сына воеводы", - "качества, достойного руки воеводы", // 18 // - "качества, более чем достойного руки воеводы", - "качества, достойного руки княжича", // 19 // - "качества, более чем достойного руки княжича", - "качества, достойного руки князя", // 20 // - "качества, более чем достойного руки князя", - "качества, достойного руки героя", // 21 // - "качества, более чем достойного руки героя", - "качества, достойного руки великого героя", // 22 // - "качества, более чем достойного руки великого героя", - "качества, которое знали только древние титаны", // 23 // - "качества, лучшего чем то, которое знали древние титаны", - "качества, достойного младших богов", // 24 // - "качества, более чем достойного младших богов", - "качества, достойного богов", // 25 // - "качества, более чем достойного богов", - "качества, которого достигают немногие", // 26 - "качества, которого достигают немногие", - "качества, которого достигают немногие", // 27 - "качества, которого достигают немногие", - "качества, достойного старших богов", // >= 28 + " ", // <3 // + " ", // 3 // + " ", + " ", // 4 // + " ", + " ", // 5 // + " ", + " ", // 6 // + " ", + " ", // 7 // + " ", + " ", // 8 // + " ", + " ", // 9 // + " ", + " ", // 10 // + " ", + " ", // 11 // + " ", + ", ", // 12 // + " , ", + ", ", // 13 // + " , ", + ", ", // 14 // + " , ", + ", ", // 15 // + ", ", + ", ", // 16 // + ", ", + ", ", // 17 // + ", ", + ", ", // 18 // + ", ", + ", ", // 19 // + ", ", + ", ", // 20 // + ", ", + ", ", // 21 // + ", ", + ", ", // 22 // + ", ", + ", ", // 23 // + ", , ", + ", ", // 24 // + ", ", + ", ", // 25 // + ", ", + ", ", // 26 + ", ", + ", ", // 27 + ", ", + ", ", // >= 28 "\n" }; MakeReceptList make_recepts; -// Функция вывода в поток // +// // CharData *&operator<<(CharData *&ch, const string &p) { SendMsgToChar(p, ch); return ch; } -// Целочисленный атрибут DataNode; def при отсутствии/некорректном значении. +// DataNode; def / . static int RecipeAttrInt(parser_wrapper::DataNode &node, const char *key, int def) { const char *v = node.GetValue(key); if (!v || !*v) { @@ -163,7 +163,7 @@ static int RecipeAttrInt(parser_wrapper::DataNode &node, const char *key, int de } } -// Чтение cfg/craft/item_creation.xml через cfg_manager (boot + reload makeitems). +// cfg/craft/item_creation.xml cfg_manager (boot + reload makeitems). void ItemCreationLoader::Load(parser_wrapper::DataNode data) { make_recepts.clear(); for (auto &rnode : data.Children("recipe")) { @@ -209,7 +209,7 @@ void ItemCreationLoader::Reload(parser_wrapper::DataNode data) { Load(std::move(data)); } -// Наименьший свободный (неиспользуемый) vnum рецепта, начиная с 1. +// () vnum , 1. static int FirstFreeRecipeVnum() { std::set used; for (size_t i = 0; i < make_recepts.size(); ++i) { @@ -238,7 +238,7 @@ std::vector ItemCreationLoader::ListElements() con } cfg_manager::ValidationResult ItemCreationLoader::Validate(parser_wrapper::DataNode &doc) const { - // Проверяем валидность умений и УНИКАЛЬНОСТЬ vnum'ов рецептов. + // vnum' . std::set seen; for (auto &node : doc.Children()) { if (std::string(node.GetName()) != "recipe") { @@ -269,9 +269,9 @@ cfg_manager::ValidationResult ItemCreationLoader::Validate(parser_wrapper::DataN parser_wrapper::DataNode ItemCreationLoader::FindElementNode(parser_wrapper::DataNode root, const std::string &id) const { - // Рецепт опознается по целочисленному vnum (не по `id`-атрибуту). Идем по всем детям с проверкой - // имени тега (а не Children("recipe")): узел, скопированный из отфильтрованного диапазона, унес бы - // фильтр и сломал бы свой собственный Children() ниже. + // vnum ( `id`-). + // ( Children("recipe")): , , + // Children() . for (auto &child : root.Children()) { if (std::string(child.GetName()) == "recipe" && id == child.GetValue("vnum")) { return child; @@ -284,12 +284,12 @@ std::string ItemCreationLoader::CanonicalElementId(const std::string &id) const if (id.empty()) { return ""; } - // "new"/"auto"/"next" -- создать рецепт с первым свободным vnum. + // "new"/"auto"/"next" -- vnum. if (id == "new" || id == "auto" || id == "next") { return std::to_string(FirstFreeRecipeVnum()); } - // Иначе ключом может быть только неотрицательное целое (свободный vnum для нового рецепта или - // существующий -- последний обрабатывается через FindElementNode как редактирование). + // ( vnum + // -- FindElementNode ). for (const char c : id) { if (c < '0' || c > '9') { return ""; @@ -312,17 +312,17 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ char tmpbuf[kMaxInputLength]; MakeRecept *trec; if (make_recepts.size() == 0) { - SendMsgToChar("Рецепты в этом мире не определены.", ch); + SendMsgToChar(" .", ch); return; } - // Выдаем список рецептов всех рецептов как в магазине. - tmpstr = "### Б Умение Предмет Составляющие: \r\n"; + // . + tmpstr = "### : \r\n"; tmpstr += "-------------------------------------------------------------------------------------------------------------------------------------------------------\r\n"; for (size_t i = 0; i < make_recepts.size(); i++) { int j = 0; - skill_name = "Нет"; - obj_name = "Нет"; + skill_name = ""; + obj_name = ""; trec = make_recepts[i]; auto obj = GetObjectPrototype(trec->obj_proto); if (obj) { @@ -344,12 +344,12 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ if (obj) { obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, native_text::char_offset(obj->get_PName(grammar::ECase::kNom), 34))); } else { - obj_name = "Нет"; + obj_name = ""; } strcpy(tmpbuf, fmt::format(" {:<35}({:5})", obj_name, trec->parts[j].proto).c_str()); if (j > 0) { if (j % 2 == 0) { - // разбиваем строчки если ингров больше 2; + // 2; tmpstr += "\r\n"; tmpstr += " ->"; } @@ -364,18 +364,18 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ page_string(ch->desc, tmpstr); return; } -// Создание любого предмета из компонента. +// . void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { - // Тут творим предмет. - // Если прислали без параметра то выводим список всех рецептов - // доступных для изготовления персонажу из его ингров - // Мастерить можно лук, посох , и диковину(аналог артефакта) - // суб команда make - // Выковать можно клинок и доспех (щит) умения разные. название одно - // Сварить отвар - // Сшить одежду + // . + // + // + // , , ( ) + // make + // () . + // + // if ((subcmd == MAKE_WEAR) && (!GetSkill(ch, ESkill::kMakeWear))) { - SendMsgToChar("Вас этому никто не научил.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } string tmpstr; @@ -384,23 +384,23 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { char tmpbuf[kMaxInputLength]; //int used_skill = subcmd; argument = one_argument(argument, tmpbuf); - // Разбираем в зависимости от того что набрали ... список объектов + // ... switch (subcmd) { case (MAKE_POTION): - // Варим отвар. - tmpstr = "Вы можете сварить:\r\n"; + // . + tmpstr = " :\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakePotion); break; case (MAKE_WEAR): - // Шьем одежку. - tmpstr = "Вы можете сшить:\r\n"; + // . + tmpstr = " :\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeWear); break; - case (MAKE_METALL): tmpstr = "Вы можете выковать:\r\n"; + case (MAKE_METALL): tmpstr = " :\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeWeapon); make_recepts.can_make(ch, &canlist, ESkill::kMakeArmor); break; - case (MAKE_CRAFT): tmpstr = "Вы можете смастерить:\r\n"; + case (MAKE_CRAFT): tmpstr = " :\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeStaff); make_recepts.can_make(ch, &canlist, ESkill::kMakeBow); make_recepts.can_make(ch, &canlist, ESkill::kMakeJewel); @@ -408,12 +408,12 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { break; } if (canlist.size() == 0) { - // Чар не может сделать ничего. - SendMsgToChar("Вы ничего не можете сделать.\r\n", ch); + // . + SendMsgToChar(" .\r\n", ch); return; } if (!*tmpbuf) { - // Выводим тут список предметов которые можем сделать. + // . for (size_t i = 0; i < canlist.size(); i++) { auto tobj = GetObjectPrototype(canlist[i]->obj_proto); if (!tobj) @@ -424,7 +424,7 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SendMsgToChar(tmpstr.c_str(), ch); return; } - // Адресуемся по списку либо по номеру, либо по названию с номером. + // , . tmpstr = string(tmpbuf); size_t i = atoi(tmpbuf); if ((i > 0) && (i <= canlist.size()) @@ -433,7 +433,7 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } else { trec = canlist.get_by_name(tmpstr); if (trec == nullptr) { - tmpstr = "Похоже, у вас творческий кризис.\r\n"; + tmpstr = ", .\r\n"; SendMsgToChar(tmpstr.c_str(), ch); return; } @@ -461,23 +461,23 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { weight = MIN(obj->get_weight() - 2, obj->get_weight() * prob / percent); } if (weight < created_item[obj_type].min_weight) { - SendMsgToChar("У вас не хватило материала.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else if (prob * 5 < percent) { - SendMsgToChar("У вас ничего не получилось.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { const auto tobj = world_objects.create_from_prototype_by_vnum(created_item[obj_type].obj_vnum); if (!tobj) { - SendMsgToChar("Образец был невозвратимо утерян.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { tobj->set_weight(MIN(weight, created_item[obj_type].max_weight)); tobj->set_cost(2 * obj->get_cost() / 3); tobj->set_owner(ch->get_uid()); tobj->set_extra_flag(EObjFlag::kTransformed); - // ковка объектов со слотами. - // для 5+ мортов имеем шанс сковать стаф с 3 слотами: базовый 2% и по 0.5% за морт - // для 2 слотов базовый шанс 5%, 1% за каждый морт - // для 1 слота базово 20% и 4% за каждый морт - // Карачун. Поправлено. Расчет не через морты а через скил. + // . + // 5+ 3 : 2% 0.5% + // 2 5%, 1% + // 1 20% 4% + // . . . if (skill == ESkill::kReforging) { if (GetSkill(ch, skill) >= 105 && number(1, 100) <= 2 + (GetSkill(ch, skill) - 105) / 10) { tobj->set_extra_flag(EObjFlag::kHasThreeSlots); @@ -494,21 +494,21 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { case 3: case 4: case 11: { - // Карачун. Таймер должен зависить от таймера прототипа. - // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) - // В минимуме один день реала, в максимуме таймер из прототипа + // . . + // MAX(<>, <>/100*< >-< 0 25% >) + // , const int timer_value = tobj->get_timer() / 100 * GetSkill(ch, skill) - number(0, tobj->get_timer() / 100 * 25); const int timer = MAX(ObjData::ONE_DAY, timer_value); tobj->set_timer(timer); - sprintf(buf, "Ваше изделие продержится примерно %d дней\n", tobj->get_timer() / 24 / 60); + sprintf(buf, " %d \n", tobj->get_timer() / 24 / 60); act(buf, false, ch, tobj.get(), 0, kToChar); tobj->set_material(obj->get_material()); - // Карачун. Так логичнее. - // было tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); - // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) - // при расчете числа умножены на 100, перед приравниванием делятся на 100. Для не потерять десятые. + // . . + // tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); + // MAX(<>, <>/100*< >-< 0 25% >) + // 100, 100. . tobj->set_maximum_durability( MAX(20000, 35000 / 100 * GetSkill(ch, skill) - number(0, 35000 / 100 * 25)) / 100); tobj->set_current_durability(tobj->get_maximum_durability()); @@ -522,24 +522,24 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { sdice += tobj->get_weight() / 10; tobj->set_val(1, ndice); tobj->set_val(2, sdice); - // tobj->set_wear_flags(created_item[obj_type].wear); пусть wear флаги будут из прототипа + // tobj->set_wear_flags(created_item[obj_type].wear); wear if (skill != ESkill::kCreateBow) { if (tobj->get_weight() < 14 && percent * 4 > prob) { tobj->set_wear_flag(EWearFlag::kHold); } - to_room = "$n выковал$g $o3."; + to_room = "$n $g $o3."; average = (((float) sdice + 1) * (float) ndice / 2.0); if (average < 3.0) { - sprintf(txtbuff, "Вы выковали $o3 %s.", create_weapon_quality[(int) (2.5 * 2)]); + sprintf(txtbuff, " $o3 %s.", create_weapon_quality[(int) (2.5 * 2)]); } else if (average <= 27.5) { - sprintf(txtbuff, "Вы выковали $o3 %s.", create_weapon_quality[(int) (average * 2)]); + sprintf(txtbuff, " $o3 %s.", create_weapon_quality[(int) (average * 2)]); } else { - sprintf(txtbuff, "Вы выковали $o3 %s!", create_weapon_quality[56]); + sprintf(txtbuff, " $o3 %s!", create_weapon_quality[56]); } to_char = (char *) txtbuff; } else { - to_room = "$n смастерил$g $o3."; - to_char = "Вы смастерили $o3."; + to_room = "$n $g $o3."; + to_char = " $o3."; } break; } @@ -557,28 +557,28 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { tobj->set_extra_flag(EObjFlag::kDecay); tobj->set_extra_flag(EObjFlag::kNosell); tobj->set_wear_flags(created_item[obj_type].wear); - to_room = "$n создал$g $o3."; - to_char = "Вы создали $o3."; + to_room = "$n $g $o3."; + to_char = " $o3."; break; case 7: // smith armor case 8: case 9: case 10: { - // Карачун. Таймер должен зависить от таймера прототипа. - // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) - // В минимуме один день реала, в максимуме таймер из прототипа + // . . + // MAX(<>, <>/100*< >-< 0 25% >) + // , const int timer_value = tobj->get_timer() / 100 * GetSkill(ch, skill) - number(0, tobj->get_timer() / 100 * 25); const int timer = MAX(ObjData::ONE_DAY, timer_value); tobj->set_timer(timer); - sprintf(buf, "Ваше изделие продержится примерно %d дней\n", tobj->get_timer() / 24 / 60); + sprintf(buf, " %d \n", tobj->get_timer() / 24 / 60); act(buf, false, ch, tobj.get(), 0, kToChar); tobj->set_material(obj->get_material()); - // Карачун. Так логичнее. - // было tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); - // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) - // при расчете числа умножены на 100, перед приравниванием делятся на 100. Для не потерять десятые. + // . . + // tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); + // MAX(<>, <>/100*< >-< 0 25% >) + // 100, 100. . tobj->set_maximum_durability( MAX(20000, 10000 / 100 * GetSkill(ch, skill) - number(0, 15000 / 100 * 25)) / 100); tobj->set_current_durability(tobj->get_maximum_durability()); @@ -591,8 +591,8 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { tobj->set_val(0, ndice); tobj->set_val(1, sdice); tobj->set_wear_flags(created_item[obj_type].wear); - to_room = "$n выковал$g $o3."; - to_char = "Вы выковали $o3."; + to_room = "$n $g $o3."; + to_char = " $o3."; break; } } // switch @@ -606,11 +606,11 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { } if (ch->GetCarryingQuantity() >= CAN_CARRY_N(ch)) { - SendMsgToChar("Вы не сможете унести столько предметов.\r\n", ch); + SendMsgToChar(" .\r\n", ch); PlaceObjToRoom(tobj.get(), ch->in_room); CheckObjDecay(tobj.get()); } else if (ch->GetCarryingWeight() + tobj->get_weight() > CAN_CARRY_W(ch)) { - SendMsgToChar("Вы не сможете унести такой вес.\r\n", ch); + SendMsgToChar(" .\r\n", ch); PlaceObjToRoom(tobj.get(), ch->in_room); CheckObjDecay(tobj.get()); } else { @@ -640,16 +640,16 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (ch->IsNpc() || !GetSkill(ch, skill_id)) { - SendMsgToChar("Вас этому никто не научил.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } argument = one_argument(argument, arg1); if (!*arg1) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar("Во что вы хотите перековать?\r\n", ch); + case ESkill::kReforging: SendMsgToChar(" ?\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar("Что вы хотите смастерить?\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar(" ?\r\n", ch); break; default: break; } @@ -658,9 +658,9 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) obj_type = search_block(arg1, create_item_name, false); if (-1 == obj_type) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar("Перековать можно в :\r\n", ch); + case ESkill::kReforging: SendMsgToChar(" :\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar("Смастерить можно :\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar(" :\r\n", ch); break; default: break; } @@ -674,9 +674,9 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (created_item[obj_type].skill != skill_id) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar("Данный предмет выковать нельзя.\r\n", ch); + case ESkill::kReforging: SendMsgToChar(" .\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar("Данный предмет смастерить нельзя.\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar(" .\r\n", ch); break; default: break; } @@ -685,16 +685,16 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) for (i = 0; i < MAX_PROTO; proto[i++] = nullptr); argument = one_argument(argument, arg2); if (!*arg2) { - SendMsgToChar("Вам нечего перековывать.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (!(obj = get_obj_in_list_vis(ch, arg2, ch->carrying))) { - sprintf(buf, "У Вас нет '%s'.\r\n", arg2); + sprintf(buf, " '%s'.\r\n", arg2); SendMsgToChar(buf, ch); return; } if (obj->get_contains()) { - act("В $o5 что-то лежит.", false, ch, obj, 0, kToChar); + act(" $o5 - .", false, ch, obj, 0, kToChar); return; } if (obj->get_type() == EObjType::kMagicIngredient) { @@ -711,31 +711,31 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (i >= MAX_PROTO) { if (found) { - act("Похоже, вы уже что-то используете вместо $o1.", false, ch, obj, 0, kToChar); + act(", - $o1.", false, ch, obj, 0, kToChar); } else { - act("Похоже, $o не подойдет для этого.", false, ch, obj, 0, kToChar); + act(", $o .", false, ch, obj, 0, kToChar); } return; } } else { if (created_item[obj_type].material_bits && !IS_SET(created_item[obj_type].material_bits, (1 << obj->get_material()))) { - act("$o сделан$G из неподходящего материала.", false, ch, obj, 0, kToChar); + act("$o $G .", false, ch, obj, 0, kToChar); return; } } switch (skill_id) { case ESkill::kReforging: - // Проверяем повторно из чего сделан объект - // Чтобы не было абъюза с перековкой из угля. + // + // . if (created_item[obj_type].material_bits && !IS_SET(created_item[obj_type].material_bits, (1 << obj->get_material()))) { - act("$o сделан$G из неподходящего материала.", false, ch, obj, 0, kToChar); + act("$o $G .", false, ch, obj, 0, kToChar); return; } if (!privilege::IsImmortal(ch)) { if (!ROOM_FLAGGED(ch->in_room, ERoomFlag::kForge)) { - SendMsgToChar("Вам нужно попасть в кузницу для этого.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } for (coal = ch->carrying; coal; coal = coal->get_next_content()) { @@ -756,10 +756,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) && !proto[i]) { rnum = GetObjRnum(created_item[obj_type].proto[i]); if (rnum < 0) { - act("У вас нет необходимого ингредиента.", false, ch, 0, 0, kToChar); + act(" .", false, ch, 0, 0, kToChar); } else { const ObjData obj(*obj_proto[rnum]); - act("У вас не хватает $o1 для этого.", false, ch, &obj, 0, kToChar); + act(" $o1 .", false, ch, &obj, 0, kToChar); } found = false; } @@ -795,10 +795,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) && !proto[i]) { rnum = GetObjRnum(created_item[obj_type].proto[i]); if (rnum < 0) { - act("У вас нет необходимого ингредиента.", false, ch, 0, 0, kToChar); + act(" .", false, ch, 0, 0, kToChar); } else { const ObjData obj(*obj_proto[rnum]); - act("У вас не хватает $o1 для этого.", false, ch, &obj, 0, kToChar); + act(" $o1 .", false, ch, &obj, 0, kToChar); } found = false; } @@ -820,10 +820,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } // ***************************** MakeReceptList::MakeReceptList() { - // Инициализация класса листа . + // . } MakeReceptList::~MakeReceptList() { - // Разрушение списка. + // . clear(); } void MakeReceptList::add(MakeRecept *recept) { @@ -838,7 +838,7 @@ bool by_skill(MakeRecept *const &lhs, MakeRecept *const &rhs) { return ((lhs->skill) > (rhs->skill)); } void MakeReceptList::sort() { - // Сделать сортировку по умениям. + // . recepts.sort(by_skill); return; } @@ -846,7 +846,7 @@ size_t MakeReceptList::size() { return recepts.size(); } void MakeReceptList::clear() { - // Очищаем список + // list::iterator p; p = recepts.begin(); while (p != recepts.end()) { @@ -869,19 +869,19 @@ MakeRecept *MakeReceptList::operator[](size_t i) { return (nullptr); } MakeRecept *MakeReceptList::get_by_name(string &rname) { - // Ищем по списку рецепт с таким именем. - // Ищем по списку рецепты которые чар может изготовить. + // . + // . list::iterator p = recepts.begin(); int k = 1; // count // split string by '.' character and convert first part into number (store to k) size_t i = rname.find("."); if (std::string::npos != i) // TODO: Check me. { - // Строка не найдена. + // . if (0 < i) { k = atoi(rname.substr(0, i).c_str()); if (k <= 0) { - return nullptr; // Если ввели -3.xxx + return nullptr; // -3.xxx } } rname = rname.substr(i + 1); @@ -908,7 +908,7 @@ MakeRecept *MakeReceptList::get_by_name(string &rname) { return nullptr; } MakeReceptList *MakeReceptList::can_make(CharData *ch, MakeReceptList *canlist, ESkill use_skill) { - // Ищем по списку рецепты которые чар может изготовить. + // . list::iterator p; p = recepts.begin(); while (p != recepts.end()) { @@ -922,7 +922,7 @@ MakeReceptList *MakeReceptList::can_make(CharData *ch, MakeReceptList *canlist, return (canlist); } ObjData *get_obj_in_list_ingr(int num, - ObjData *list) //Ингридиентом является или сам прототип с VNUM или альтернатива с VALUE 1 равным внум прототипа + ObjData *list) // VNUM VALUE 1 { ObjData *i; for (i = list; i; i = i->get_next_content()) { @@ -939,7 +939,7 @@ ObjData *get_obj_in_list_ingr(int num, return nullptr; } MakeRecept::MakeRecept() : skill(ESkill::kUndefined) { - locked = true; // по умолчанию рецепт залочен. + locked = true; // . vnum = 0; obj_proto = 0; for (int i = 0; i < MAX_PARTS; i++) { @@ -952,16 +952,16 @@ int MakeRecept::can_make(CharData *ch) { int i; ObjData *ingrobj = nullptr; // char tmpbuf[kMaxInputLength]; - // Сделать проверку на поле locked + // locked if (!ch) return (false); if (locked) return (false); - // Сделать проверку наличия скилла у игрока. + // . if (ch->IsNpc() || !GetSkill(ch, skill)) { return (false); } - // Делаем проверку может ли чар сделать предмет такого типа + // if (skill == ESkill::kMakeStaff) { return 0; } @@ -971,17 +971,17 @@ int MakeRecept::can_make(CharData *ch) { } if (GetObjRnum(parts[i].proto) < 0) return (false); - //SendMsgToChar("Образец был невозвратимо утерян.\r\n",ch); //леший знает чего тут надо писать + //SendMsgToChar(" .\r\n",ch); // if (!(ingrobj = get_obj_in_list_ingr(parts[i].proto, ch->carrying))) { - //sprintf(tmpbuf,"Для '%d' у вас нет '%d'.\r\n",obj_proto,parts[i].proto); + //sprintf(tmpbuf," '%d' '%d'.\r\n",obj_proto,parts[i].proto); //SendMsgToChar(tmpbuf,ch); return (false); } int ingr_lev = get_ingr_lev(ingrobj); - // Если чар ниже уровня ингридиента то он не может делать рецепты с его - // участием. + // + // . if (!privilege::IsImpl(ch) && (ingr_lev > (GetRealLevel(ch) + 2 * remort::GetRealRemort(ch)))) { - SendMsgToChar("Вы слишком малого уровня и вам что-то не подходит для шитья.\r\n", ch); + SendMsgToChar(" - .\r\n", ch); return (false); } } @@ -989,12 +989,12 @@ int MakeRecept::can_make(CharData *ch) { } int MakeRecept::get_ingr_lev(ObjData *ingrobj) { - // Получаем уровень ингредиента ... + // ... if (ingrobj->get_type() == EObjType::kMagicIngredient) { - // Получаем уровень игредиента до 128 + // 128 return (GET_OBJ_VAL(ingrobj, 0) >> 8); } else if (ingrobj->get_type() == EObjType::kMagicComponent) { - // У ингров типа 26 совпадает уровень и сила. + // 26 . return GET_OBJ_VAL(ingrobj, IM_POWER_SLOT); } else if (ingrobj->get_type() == EObjType::kCraftMaterial) { return GET_OBJ_VAL(ingrobj, 0); @@ -1004,7 +1004,7 @@ int MakeRecept::get_ingr_lev(ObjData *ingrobj) { } int MakeRecept::get_ingr_pow(ObjData *ingrobj) { - // Получаем силу ингредиента ... + // ... if (ingrobj->get_type() == EObjType::kMagicIngredient || ingrobj->get_type() == EObjType::kCraftMaterial) { return GET_OBJ_VAL(ingrobj, 2); @@ -1047,7 +1047,7 @@ void MakeRecept::make_value_wear(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_ obj->set_val(0, ((GetRealInt(ch) * GetRealInt(ch) / 10 + GetSkill(ch, ESkill::kMakeWear)) / 100 + (GET_OBJ_VAL(ingrs[0], 3) + 1)) * wearkoeff - / 100); //АС=((инта*инта/10+умелка)/100+левл.шкуры)*коэф.части тела + / 100); //=((*/10+)/100+.)*. if (CAN_WEAR(obj, EWearFlag::kBody)) //0.9 { wearkoeff = 90; @@ -1078,7 +1078,7 @@ void MakeRecept::make_value_wear(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_ } obj->set_val(1, (GetSkill(ch, ESkill::kMakeWear) / 25 + (GET_OBJ_VAL(ingrs[0], 3) + 1)) * wearkoeff - / 100); //броня=(%умелки/25+левл.шкуры)*коэф.части тела + / 100); //=(%/25+.)*. } float MakeRecept::count_mort_requred(ObjData *obj) { float result = 0.0; @@ -1092,11 +1092,11 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float total_weight = 0.0; - // аффекты APPLY_x + // APPLY_x for (int k = 0; k < kMaxObjAffect; k++) { if (obj->get_affected(k).location == 0) continue; - // случай, если один аффект прописан в нескольких полях + // , for (int kk = 0; kk < kMaxObjAffect; kk++) { if (obj->get_affected(k).location == obj->get_affected(kk).location && k != kk) { @@ -1115,7 +1115,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { weight, GET_OBJ_VNUM(obj)); total_weight += pow(weight, SQRT_MOD); } - // савесы которые с минусом должны тогда понижать вес если в + + // + else if ((obj->get_affected(k).modifier > 0) && ((obj->get_affected(k).location == EApply::kAc) || (obj->get_affected(k).location == EApply::kSavingWill) || (obj->get_affected(k).location == EApply::kSavingCritical) || @@ -1124,7 +1124,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float weight = count_affect_weight(obj->get_affected(k).location, 0 - obj->get_affected(k).modifier); total_weight -= pow(weight, -SQRT_MOD); } - //Добавленый кусок учет савесов с - значениями + // - else if ((obj->get_affected(k).modifier < 0) && ((obj->get_affected(k).location == EApply::kAc) || (obj->get_affected(k).location == EApply::kSavingWill) || @@ -1134,7 +1134,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float weight = count_affect_weight(obj->get_affected(k).location, obj->get_affected(k).modifier); total_weight += pow(weight, SQRT_MOD); } - //Добавленый кусок учет отрицательного значения но не савесов + // else if ((obj->get_affected(k).modifier < 0) && ((obj->get_affected(k).location != EApply::kAc) && (obj->get_affected(k).location != EApply::kSavingWill) && @@ -1145,7 +1145,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { total_weight -= pow(weight, -SQRT_MOD); } } - // аффекты AFF_x через equipment_affect + // AFF_x equipment_affect for (const auto &m : equipment_affect) { if (obj->GetEEquipmentAffect(m.aff_pos)) { if (m.aff_affect == EAffect::kAirShield) { @@ -1222,77 +1222,77 @@ float MakeRecept::count_affect_weight(int num, int mod) { } void MakeRecept::make_object(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_PARTS], int ingr_cnt) { int i, j; - //ставим именительные именительные падежи в алиасы + // sprintf(buf, "%s %s %s %s", obj->get_PName(grammar::ECase::kNom).c_str(), ingrs[0]->get_PName(grammar::ECase::kGen).c_str(), ingrs[1]->get_PName(grammar::ECase::kIns).c_str(), ingrs[2]->get_PName(grammar::ECase::kIns).c_str()); obj->set_aliases(buf); - for (i = grammar::ECase::kFirstCase; i <= grammar::ECase::kLastCase; i++) // ставим падежи в имя с учетов ингров + for (i = grammar::ECase::kFirstCase; i <= grammar::ECase::kLastCase; i++) // { auto name_case = static_cast(i); sprintf(buf, "%s", obj->get_PName(name_case).c_str()); - strcat(buf, " из "); + strcat(buf, " "); strcat(buf, ingrs[0]->get_PName(grammar::ECase::kGen).c_str()); - strcat(buf, " с "); + strcat(buf, " "); strcat(buf, ingrs[1]->get_PName(grammar::ECase::kIns).c_str()); - strcat(buf, " и "); + strcat(buf, " "); strcat(buf, ingrs[2]->get_PName(grammar::ECase::kIns).c_str()); obj->set_PName(name_case, buf); - if (i == 0) // именительный падеж + if (i == 0) // { obj->set_short_description(buf); if (GET_OBJ_SEX(obj) == EGender::kMale) { - snprintf(buf2, kMaxStringLength, "Брошенный %s лежит тут.", buf); + snprintf(buf2, kMaxStringLength, " %s .", buf); } else if (GET_OBJ_SEX(obj) == EGender::kFemale) { - snprintf(buf2, kMaxStringLength, "Брошенная %s лежит тут.", buf); + snprintf(buf2, kMaxStringLength, " %s .", buf); } else if (GET_OBJ_SEX(obj) == EGender::kPoly) { - snprintf(buf2, kMaxStringLength, "Брошенные %s лежат тут.", buf); + snprintf(buf2, kMaxStringLength, " %s .", buf); } - obj->set_description(buf2); // описание на земле + obj->set_description(buf2); // } } - obj->set_is_rename(true); // ставим флаг что объект переименован + obj->set_is_rename(true); // auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[0]->get_affect_flags(), get_ingr_pow(ingrs[0])); obj->SetEquipmentAffectFlags(temp_flags); - // перносим эффекты ... с ингров на прототип, 0 объект шкура переносим все, с остальных 1 рандом + // ... , 0 , 1 merge_extra_flags(ch, obj, ingrs[0], get_ingr_pow(ingrs[0])); auto temp_affected = obj->get_all_affected(); add_affects(ch, temp_affected, ingrs[0]->get_all_affected(), get_ingr_pow(ingrs[0])); obj->set_all_affected(temp_affected); - add_rnd_skills(ch, ingrs[0], obj); //переносим случайную умелку со шкуры - obj->set_extra_flag(EObjFlag::kNoalter); // нельзя сфрешить черным свитком + add_rnd_skills(ch, ingrs[0], obj); // + obj->set_extra_flag(EObjFlag::kNoalter); // obj->set_timer((GET_OBJ_VAL(ingrs[0], 3) + 1) * 1000 - + GetSkill(ch, ESkill::kMakeWear) / 2 * number(160, 220)); // таймер зависит в основном от умелки - obj->set_craft_timer(obj->get_timer()); // запомним таймер созданной вещи для правильного отображения при осм для ее сост. + + GetSkill(ch, ESkill::kMakeWear) / 2 * number(160, 220)); // + obj->set_craft_timer(obj->get_timer()); // . for (j = 1; j < ingr_cnt; j++) { int i, raffect = 0; - for (i = 0; i < kMaxObjAffect; i++) // посмотрим скока аффектов + for (i = 0; i < kMaxObjAffect; i++) // { if (ingrs[j]->get_affected(i).location == EApply::kNone) { break; } } - if (i > 0) // если > 0 переносим случайный + if (i > 0) // > 0 { raffect = number(0, i - 1); for (int i = 0; i < kMaxObjAffect; i++) { const auto &ra = ingrs[j]->get_affected(raffect); if (obj->get_affected(i).location - == ra.location) // если аффект такой уже висит и он меньше, переставим значение + == ra.location) // , { if (obj->get_affected(i).modifier < ra.modifier) { obj->set_affected(i, obj->get_affected(i).location, ra.modifier); } break; } - if (obj->get_affected(i).location == EApply::kNone) // добавляем афф на свободное место + if (obj->get_affected(i).location == EApply::kNone) // { - if (number(1, 100) > GET_OBJ_VAL(ingrs[j], 2)) // проверим фейл на силу ингра + if (number(1, 100) > GET_OBJ_VAL(ingrs[j], 2)) // { break; } @@ -1301,14 +1301,14 @@ void MakeRecept::make_object(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_PART } } } - // переносим аффекты ... c ингров на прототип. + // ... c . auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[j]->get_affect_flags(), get_ingr_pow(ingrs[j])); obj->SetEquipmentAffectFlags(temp_flags); - // перносим эффекты ... с ингров на прототип. + // ... . merge_extra_flags(ch, obj, ingrs[j], get_ingr_pow(ingrs[j])); - // переносим 1 рандом аффект - add_rnd_skills(ch, ingrs[j], obj); //переноси случайную умелку с ингров + // 1 + add_rnd_skills(ch, ingrs[j], obj); // } } static std::string craft_recipe_name(int recipe_id) { @@ -1334,39 +1334,39 @@ static void record_craft_success(int recipe_id, ESkill skill) { }); } -// создать предмет по рецепту +// int MakeRecept::make(CharData *ch) { char tmpbuf[kMaxStringLength];//, tmpbuf2[kMaxStringLength]; ObjData *ingrs[MAX_PARTS]; string tmpstr, charwork, roomwork, charfail, roomfail, charsucc, roomsucc, chardam, roomdam, tagging, itemtag; int dam = 0; bool make_fail; - // 1. Проверить есть ли скилл у чара + // 1. if (ch->IsNpc() || !GetSkill(ch, skill)) { - SendMsgToChar("Странно что вам вообще пришло в голову cделать это.\r\n", ch); + SendMsgToChar(" c .\r\n", ch); return (false); } - // 2. Проверить есть ли ингры у чара + // 2. if (!can_make(ch)) { - SendMsgToChar("У вас нет составляющих для этого.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return (false); } if (ch->get_move() < MIN_MAKE_MOVE) { - SendMsgToChar("Вы слишком устали и вам ничего не хочется делать.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return (false); } auto tobj = GetObjectPrototype(obj_proto); if (!tobj) { return 0; } - // Проверяем возможность создания предмета + // if (!privilege::IsImmortal(ch) && (skill == ESkill::kMakeStaff)) { const ObjData obj(*tobj); - act("Вы не готовы к тому чтобы сделать $o3.", false, ch, &obj, 0, kToChar); + act(" $o3.", false, ch, &obj, 0, kToChar); return (false); } - // Прогружаем в массив реальные ингры - // 3. Проверить уровни ингров и чара + // + // 3. int ingr_cnt = 0, ingr_lev, i, craft_weight, ingr_pow; for (i = 0; i < MAX_PARTS; i++) { if (parts[i].proto == 0) @@ -1374,124 +1374,124 @@ int MakeRecept::make(CharData *ch) { ingrs[i] = get_obj_in_list_ingr(parts[i].proto, ch->carrying); ingr_lev = get_ingr_lev(ingrs[i]); if (!privilege::IsImpl(ch) && (ingr_lev > (GetRealLevel(ch) + 2 * remort::GetRealRemort(ch)))) { - tmpstr = "Вы побоялись испортить " + ingrs[i]->get_PName(grammar::ECase::kAcc) - + "\r\n и прекратили работу над " + tobj->get_PName(grammar::ECase::kIns) + ".\r\n"; + tmpstr = " " + ingrs[i]->get_PName(grammar::ECase::kAcc) + + "\r\n " + tobj->get_PName(grammar::ECase::kIns) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); return (false); }; ingr_pow = get_ingr_pow(ingrs[i]); if (ingr_pow < parts[i].min_power) { - tmpstr = "$o не подходит для изготовления " + tobj->get_PName(grammar::ECase::kGen) + "."; + tmpstr = "$o " + tobj->get_PName(grammar::ECase::kGen) + "."; act(tmpstr.c_str(), false, ch, ingrs[i], 0, kToChar); return (false); } ingr_cnt++; } //int stat_bonus; - // Делаем всякие доп проверки для различных умений. + // . switch (skill) { case ESkill::kMakeWeapon: case ESkill::kMakeArmor: - // Проверяем есть ли тут наковальня или комната кузня. + // . if ((!ROOM_FLAGGED(ch->in_room, ERoomFlag::kForge)) && (!privilege::IsImmortal(ch))) { - SendMsgToChar("Вам нужно попасть в кузницу для этого.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return (false); } - charwork = "Вы поместили заготовку на наковальню и начали ковать $o3."; - roomwork = "$n поместил$g закотовку на наковальню и начал$g ковать."; - charfail = "Заготовка под вашими ударами покрылась сетью трещин и раскололась."; - roomfail = "Под ударами молота $n1 заготовка раскололась."; - charsucc = "Вы выковали $o3."; - roomsucc = "$n выковал$G $o3."; - chardam = "Заготовка выскочила из под молота и больно ударила вас."; - roomdam = "Заготовка выскочила из под молота $n1, больно $s ударив."; - tagging = "Вы поставили свое клеймо на $o5."; - itemtag = "На $o5 стоит клеймо 'Выковал$g $n'."; + charwork = " $o3."; + roomwork = "$n $g $g ."; + charfail = " ."; + roomfail = " $n1 ."; + charsucc = " $o3."; + roomsucc = "$n $G $o3."; + chardam = " ."; + roomdam = " $n1, $s ."; + tagging = " $o5."; + itemtag = " $o5 '$g $n'."; dam = 70; - // Бонус сила + // //stat_bonus = number(0, GetRealStr(ch)); break; - case ESkill::kMakeBow: charwork = "Вы начали мастерить $o3."; - roomwork = "$n начал$g мастерить что-то очень напоминающее $o3."; - charfail = "С громким треском $o сломал$U в ваших неумелых руках."; - roomfail = "С громким треском $o сломал$U в неумелых руках $n1."; - charsucc = "Вы cмастерили $o3."; - roomsucc = "$n смастерил$g $o3."; - chardam = "$o3 с громким треском сломал$U оцарапав вам руки."; - roomdam = "$o3 с громким треском сломал$U оцарапав руки $n2."; - tagging = "Вы вырезали свое имя на $o5."; - itemtag = "На $o5 видна метка 'Смастерил$g $n'."; - // Бонус ловкость + case ESkill::kMakeBow: charwork = " $o3."; + roomwork = "$n $g - $o3."; + charfail = " $o $U ."; + roomfail = " $o $U $n1."; + charsucc = " c $o3."; + roomsucc = "$n $g $o3."; + chardam = "$o3 $U ."; + roomdam = "$o3 $U $n2."; + tagging = " $o5."; + itemtag = " $o5 '$g $n'."; + // //stat_bonus = number(0, GetRealDex(ch)); dam = 40; break; - case ESkill::kMakeWear: charwork = "Вы взяли в руку иголку и начали шить $o3."; - roomwork = "$n взял$g в руку иголку и начал$g увлеченно шить."; - charfail = "У вас ничего не получилось сшить."; - roomfail = "$n пытал$u что-то сшить, но ничего не вышло."; - charsucc = "Вы сшили $o3."; - roomsucc = "$n сшил$g $o3."; - chardam = "Игла глубоко вошла в вашу руку. Аккуратнее надо быть."; - roomdam = "$n глубоко воткнул$g иглу в себе в руку. \r\nА с виду вполне нормальный человек."; - tagging = "Вы пришили к $o2 бирку со своим именем."; - itemtag = "На $o5 вы заметили бирку 'Сшил$g $n'."; - // Бонус тело , не спрашивайте почему :)) + case ESkill::kMakeWear: charwork = " $o3."; + roomwork = "$n $g $g ."; + charfail = " ."; + roomfail = "$n $u - , ."; + charsucc = " $o3."; + roomsucc = "$n $g $o3."; + chardam = " . ."; + roomdam = "$n $g . \r\n ."; + tagging = " $o2 ."; + itemtag = " $o5 '$g $n'."; + // , :)) //stat_bonus = number(0, GetRealCon(ch)); dam = 30; break; - case ESkill::kMakeAmulet: charwork = "Вы взяли в руки необходимые материалы и начали мастерить $o3."; - roomwork = "$n взял$g в руки что-то и начал$g увлеченно пыхтеть над чем-то."; - charfail = "У вас ничего не получилось"; - roomfail = "$n пытал$u что-то сделать, но ничего не вышло."; - charsucc = "Вы смастерили $o3."; - roomsucc = "$n смастерил$g $o3."; - chardam = "Яркая вспышка дала вам знать, что с магией необходимо быть поосторожней."; - roomdam = "Яркая вспышка осветила все вокруг. $n2 стоит быть аккуратнее с магией! \r\n"; - tagging = "На обратной стороне $o1 вы нацарапали свое имя."; - itemtag = "На обратной стороне $o1 вы заметили слово '$n', видимо, это имя создателя этой чудной вещицы."; + case ESkill::kMakeAmulet: charwork = " $o3."; + roomwork = "$n $g - $g -."; + charfail = " "; + roomfail = "$n $u - , ."; + charsucc = " $o3."; + roomsucc = "$n $g $o3."; + chardam = " , ."; + roomdam = " . $n2 ! \r\n"; + tagging = " $o1 ."; + itemtag = " $o1 '$n', , ."; dam = 30; break; - case ESkill::kMakeJewel: charwork = "Вы начали мастерить $o3."; - roomwork = "$n начал мастерить какую-то диковинку."; - charfail = "Ваше неудачное движение повредило $o5."; - roomfail = "Неудачное движение $n, сделало $s работу бессмысленной."; - charsucc = "Вы смастерили $o3."; - roomsucc = "$n смастерил$g $o3."; - chardam = "Мелкий кусочек материала отскочил и попал вам в глаз.\r\nЭто было больно!"; - roomdam = "Мелкий кусочек материала отскочил и попал в глаз $n2."; - tagging = "Вы приладили к $o2 табличку со своим именем."; - itemtag = "С нижней стороны $o1 укреплена табличка 'Cделано $n4'."; - // Бонус харя + case ESkill::kMakeJewel: charwork = " $o3."; + roomwork = "$n - ."; + charfail = " $o5."; + roomfail = " $n, $s ."; + charsucc = " $o3."; + roomsucc = "$n $g $o3."; + chardam = " .\r\n !"; + roomdam = " $n2."; + tagging = " $o2 ."; + itemtag = " $o1 'C $n4'."; + // //stat_bonus = number(0, GetRealCha(ch)); dam = 30; break; - case ESkill::kMakeStaff: charwork = "Вы начали мастерить $o3."; - roomwork = "$n начал мастерить что-то, посылая всех к чертям."; - charfail = "$o3 осветил комнату магическим светом и истаял."; - roomfail = "Предмет в руках $n1 вспыхнул, озарив комнату магическим светом и истаял."; + case ESkill::kMakeStaff: charwork = " $o3."; + roomwork = "$n -, ."; + charfail = "$o3 ."; + roomfail = " $n1 , ."; charsucc = - "Тайные знаки нанесенные на $o3 вспыхнули и погасли.\r\nДа, $E хорошо послужит своему хозяину."; - roomsucc = "$n смастерил$g $o3. Вы почувствовали скрытую силу спрятанную в этом предмете."; - chardam = "$o взорвался в ваших руках. Вас сильно обожгло."; - roomdam = "$o взорвался в руках $n1, опалив его.\r\nВокруг приятно запахло жаренным мясом."; - tagging = "Вы начертили на $o2 свое имя."; - itemtag = "Среди рунных знаков видна надпись 'Создано $n4'."; - // Бонус ум. + " $o3 .\r\n, $E ."; + roomsucc = "$n $g $o3. ."; + chardam = "$o . ."; + roomdam = "$o $n1, .\r\n ."; + tagging = " $o2 ."; + itemtag = " ' $n4'."; + // . //stat_bonus = number(0, GetRealInt(ch)); dam = 70; break; - case ESkill::kMakePotion: charwork = "Вы достали небольшой горшочек и развели под ним огонь, начав варить $o3."; - roomwork = "$n достал горшочек и поставил его на огонь."; - charfail = "Вы не уследили как зелье закипело и пролилось в огонь."; + case ESkill::kMakePotion: charwork = " , $o3."; + roomwork = "$n ."; + charfail = " ."; roomfail = - "Зелье которое варил$g $n закипело и пролилось в огонь,\r\n распространив по комнате ужасную вонь."; - charsucc = "Зелье удалось вам на славу."; - roomsucc = "$n сварил$g $o3. Приятный аромат зелья из горшочка, так и манит вас."; - chardam = "Вы опрокинули горшочек с зельем на себя, сильно ошпарившись."; - roomdam = "Горшочек с $o4 опрокинулся на $n1, ошпарив $s."; - tagging = "Вы на прикрепили к $o2 бирку со своим именем."; - itemtag = "На $o1 вы заметили бирку 'Сварено $n4'"; - // Бонус мудра + " $g $n ,\r\n ."; + charsucc = " ."; + roomsucc = "$n $g $o3. , ."; + chardam = " , ."; + roomdam = " $o4 $n1, $s."; + tagging = " $o2 ."; + itemtag = " $o1 ' $n4'"; + // //stat_bonus = number(0, GetRealWis(ch)); dam = 40; break; @@ -1500,15 +1500,15 @@ int MakeRecept::make(CharData *ch) { const ObjData object(*tobj); act(charwork.c_str(), false, ch, &object, 0, kToChar); act(roomwork.c_str(), false, ch, &object, 0, kToRoom); - // Считаем вероятность испортить отдельный ингридиент - // если уровень чара = уровню ингра то фейл 50% - // если уровень чара > уровня ингра на 15 то фейл 0% - // уровень чара * 2 - random(30) < 15 - фейл то пропадает весь материал - // Выдается Вы испортили (...) - // и выходим. + // + // = 50% + // > 15 0% + // * 2 - random(30) < 15 - + // (...) + // . make_fail = false; - // Это средний уровень получившегося предмета. - // использует при расчете макс количества предметов в мире. + // . + // . int created_lev = 0; int used_non_ingrs = 0; for (i = 0; i < ingr_cnt; i++) { @@ -1518,12 +1518,12 @@ int MakeRecept::make(CharData *ch) { } else { created_lev += ingr_lev; } - // Шанс испортить не ингредиент всетаки есть. + // . if ((number(0, 30) < (5 + ingr_lev - GetRealLevel(ch) - 2 * remort::GetRealRemort(ch))) && !privilege::IsImpl(ch)) { - tmpstr = "Вы испортили " + ingrs[i]->get_PName(grammar::ECase::kAcc) + ".\r\n"; + tmpstr = " " + ingrs[i]->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); - //extract_obj(ingrs[i]); //заменим на обнуление веса - //чтобы не крешило дальше в обработке фейла (Купала) + //extract_obj(ingrs[i]); // + // () IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); make_fail = true; @@ -1532,11 +1532,11 @@ int MakeRecept::make(CharData *ch) { created_lev = created_lev / MAX(1, (ingr_cnt - used_non_ingrs)); int j; int craft_move = MIN_MAKE_MOVE + (created_lev / 2) - 1; - // Снимаем мувы за умение + // if (ch->get_move() < craft_move) { ch->set_move(0); - // Вам не хватило сил доделать. - tmpstr = "Вам не хватило сил доделать " + tobj->get_PName(grammar::ECase::kAcc) + ".\r\n"; + // . + tmpstr = " " + tobj->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); make_fail = true; } else { @@ -1546,72 +1546,72 @@ int MakeRecept::make(CharData *ch) { } TrainSkill(ch, skill, !make_fail, nullptr); - // 4. Считаем сколько материала треба. + // 4. . if (!make_fail) { for (i = 0; i < ingr_cnt; i++) { - if (skill == ESkill::kMakeWear && i == 0) //для шитья всегда раскраиваем шкуру + if (skill == ESkill::kMakeWear && i == 0) // { IS_CARRYING_W(ch) -= ingrs[0]->get_weight(); - ingrs[0]->set_weight(0); // шкуру дикеим полностью - tmpstr = "Вы раскроили полностью " + ingrs[0]->get_PName(grammar::ECase::kAcc) + ".\r\n"; + ingrs[0]->set_weight(0); // + tmpstr = " " + ingrs[0]->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); continue; } // - // нужный материал = мин.материал + + // = . + // random(100) - skill - // если она < 20 то мин.вес + rand(мин.вес/3) - // если она < 50 то мин.вес*rand(1,2) + rand(мин.вес/3) - // если она > 50 мин.вес*rand(2,5) + rand(мин.вес/3) + // < 20 . + rand(./3) + // < 50 .*rand(1,2) + rand(./3) + // > 50 .*rand(2,5) + rand(./3) if (get_ingr_lev(ingrs[i]) == -1) - continue; // Компонент не ингр. пропускаем. + continue; // . . craft_weight = parts[i].min_weight + number(0, (parts[i].min_weight / 3) + 1); j = number(0, 100) - CalcCurrentSkill(ch, skill, 0); if ((j >= 20) && (j < 50)) craft_weight += parts[i].min_weight * number(1, 2); else if (j > 50) craft_weight += parts[i].min_weight * number(2, 5); - // 5. Делаем проверку есть ли столько материала. - // если не хватает то удаляем игридиент и фейлим. + // 5. . + // . int state = craft_weight; - // Обсчет веса ингров в цикле, если не хватило веса берем следующий ингр в инве, если не хватает, делаем фэйл (make_fail) и брекаем внешний цикл, смысл дальше ингры смотреть? - //SendMsgToChar(ch, "Требуется вес %d вес ингра %d требуемое кол ингров %d\r\n", state, ingrs[i]->get_weight(), ingr_cnt); + // , , , (make_fail) , ? + //SendMsgToChar(ch, " %d %d %d\r\n", state, ingrs[i]->get_weight(), ingr_cnt); int obj_vnum_tmp = GET_OBJ_VNUM(ingrs[i]); while (state > 0) { - //Переделаем слегка логику итераций - //Сперва проверяем сколько нам нужно. Если вес ингра больше, чем требуется, то вычитаем вес и останавливаем итерацию. + // + // . , , . if (ingrs[i]->get_weight() > state) { ingrs[i]->sub_weight(state); - SendMsgToChar(ch, "Вы использовали %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); + SendMsgToChar(ch, " %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); IS_CARRYING_W(ch) -= state; break; } - //Если вес ингра ровно столько, сколько требуется, вычитаем вес, ломаем ингр и останавливаем итерацию. + // , , , . else if (ingrs[i]->get_weight() == state) { IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); - SendMsgToChar(ch, "Вы полностью использовали %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); + SendMsgToChar(ch, " %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); //extract_obj(ingrs[i]); break; } - //Если вес ингра меньше, чем требуется, то вычтем этот вес из того, сколько требуется. + // , , , . else { state = state - ingrs[i]->get_weight(); SendMsgToChar(ch, - "Вы полностью использовали %s и начали искать следующий ингредиент.\r\n", + " %s .\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); std::string tmpname = std::string(ingrs[i]->get_PName(grammar::ECase::kGen).c_str()); IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); ExtractObjFromWorld(ingrs[i]); ingrs[i] = nullptr; - //Если некст ингра в инве нет, то сообщаем об этом и идем в фэйл. Некст ингры все равно проверяем + // , . if (!get_obj_in_list_ingr(obj_vnum_tmp, ch->carrying)) { - SendMsgToChar(ch, "У вас в инвентаре больше нет %s.\r\n", tmpname.c_str()); + SendMsgToChar(ch, " %s.\r\n", tmpname.c_str()); make_fail = true; break; } - //Подцепляем некст ингр и идем в нашу проверку заново + // else { ingrs[i] = get_obj_in_list_ingr(obj_vnum_tmp, ch->carrying); } @@ -1620,7 +1620,7 @@ int MakeRecept::make(CharData *ch) { } } if (make_fail) { - // Считаем крит фейл или нет. + // . int crit_fail = number(0, 100); if (crit_fail > 2) { const ObjData obj(*tobj); @@ -1631,16 +1631,16 @@ int MakeRecept::make(CharData *ch) { act(chardam.c_str(), false, ch, &obj, 0, kToChar); act(roomdam.c_str(), false, ch, &obj, 0, kToRoom); dam = number(0, dam); - // Наносим дамаг. + // . if (GetRealLevel(ch) >= kLvlImmortal && dam > 0) { - SendMsgToChar("Будучи бессмертным, вы избежали повреждения...", ch); + SendMsgToChar(" , ...", ch); return (false); } ch->set_hit(ch->get_hit() - dam); update_pos(ch); char_dam_message(dam, ch, ch, 0); if (ch->GetPosition() == EPosition::kDead) { - // Убился веником. + // . if (!ch->IsNpc()) { sprintf(tmpbuf, "%s killed by a crafting at %s", GET_NAME(ch), @@ -1659,17 +1659,17 @@ int MakeRecept::make(CharData *ch) { record_craft_failure(obj_proto, skill); return (false); } - // Лоадим предмет игроку + // const auto obj = world_objects.create_from_prototype_by_vnum(obj_proto); act(charsucc.c_str(), false, ch, obj.get(), 0, kToChar); act(roomsucc.c_str(), false, ch, obj.get(), 0, kToRoom); - // 6. Считаем базовые статсы предмета и таймер - // формула для каждого умения отдельная - // Для числовых х-к: х-ка+(skill - random(100))/20; - // Для флагов ???: random(200) - skill > 0 то флаг переноситься. - // Т.к. сделать мы можем практически любой предмет. - // Модифицируем вес предмета и его таймер. - // Для маг предметов надо в сторону облегчения. + // 6. + // + // -: -+(skill - random(100))/20; + // ???: random(200) - skill > 0 . + // .. . + // . + // . // i = obj->get_weight(); switch (skill) { case ESkill::kMakeBow:; @@ -1687,29 +1687,29 @@ int MakeRecept::make(CharData *ch) { i = obj->get_timer(); obj->set_timer(stat_modify(ch, obj->get_timer(), 1)); - // Модифицируем уникальные для предметов х-ки. - // Балансировка варьируется изменением делителя (!20!) - // при делителе 20 и умении 100 и макс везении будет +5 к параметру - // Можно посчитать бонусы от времени суток. - // Считаем среднюю силу ингров . + // -. + // (!20!) + // 20 100 +5 + // . + // . switch (obj->get_type()) { case EObjType::kLightSource: - // Считаем длительность свечения. + // . if (GET_OBJ_VAL(obj, 2) != -1) { obj->set_val(2, stat_modify(ch, GET_OBJ_VAL(obj, 2), 1)); } break; case EObjType::kWand: case EObjType::kStaff: - // Считаем уровень закла + // // issue.magic-items: the maker's competence (make-staff skill + Int) becomes the casting // potency (kSpellItem* keys), replacing the legacy val[0] = caster level. obj->SetPotionValueKey(ObjVal::EValueKey::kMakerSkill, std::max(1, GetSkill(ch, ESkill::kMakeStaff))); obj->SetPotionValueKey(ObjVal::EValueKey::kMakerStat, GetRealBaseStat(ch, EBaseStat::kInt)); break; case EObjType::kWeapon: - // Считаем число xdy - // модифицируем XdY + // xdy + // XdY if (GET_OBJ_VAL(obj, 1) > GET_OBJ_VAL(obj, 2)) { obj->set_val(1, stat_modify(ch, GET_OBJ_VAL(obj, 1), 1)); } else { @@ -1720,79 +1720,79 @@ int MakeRecept::make(CharData *ch) { case EObjType::kLightArmor: case EObjType::kMediumArmor: case EObjType::kHeavyArmor: - // Считаем + АС + // + obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); - // Считаем поглощение. + // . obj->set_val(1, stat_modify(ch, GET_OBJ_VAL(obj, 1), 1)); break; case EObjType::kPotion: - // Считаем уровень итоговый напитка + // obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kContainer: - // Считаем объем контейнера. + // . obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kLiquidContainer: - // Считаем объем контейнера. + // . obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kMagicIngredient: - // Для ингров ничего не трогаем ... ибо опасно. :) + // ... . :) break; default: break; } - // 7. Считаем доп. статсы предмета. - // х-ка прототипа + - // если (random(100) - сила ингра ) < 1 то переноситься весь параметр. - // если от 1 до 25 то переноситься 1/2 - // если от 25 до 50 то переноситься 1/3 - // больше переноситься 0 - // переносим доп аффекты ...+мудра +ловка и т.п. + // 7. . . + // - + + // (random(100) - ) < 1 . + // 1 25 1/2 + // 25 50 1/3 + // 0 + // ...+ + .. if (skill == ESkill::kMakeWear) { make_object(ch, obj.get(), ingrs, ingr_cnt); make_value_wear(ch, obj.get(), ingrs); - } else // если не шитье то никаких махинаций с падежами и копированием рандом аффекта + } else // { for (j = 0; j < ingr_cnt; j++) { ingr_pow = get_ingr_pow(ingrs[j]); if (ingr_pow < 0) { ingr_pow = 20; } - // переносим аффекты ... c ингров на прототип. + // ... c . auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[j]->get_affect_flags(), ingr_pow); obj->SetEquipmentAffectFlags(temp_flags); - // перносим эффекты ... с ингров на прототип. + // ... . merge_extra_flags(ch, obj.get(), ingrs[j], ingr_pow); auto temp_affected = obj->get_all_affected(); add_affects(ch, temp_affected, ingrs[j]->get_all_affected(), ingr_pow); obj->set_all_affected(temp_affected); } } - // Мочим истраченные ингры. + // . for (i = 0; i < ingr_cnt; i++) { if (ingrs[i]->get_weight() <= 0) { ExtractObjFromWorld(ingrs[i]); } } - // 8. Проверяем мах. инворлд. - // Считаем по формуле (31 - ср. уровень предмета) * 5 - - // овер шмота в мире не 30 лева не больше 5 штук - // Т.к. ср. уровень ингров будет определять - // число шмоток в мире то шмотки по хуже будут вытеснять - // шмотки по лучше (в целом это не так страшно). - // Ставим метку если все хорошо. + // 8. . . + // (31 - . ) * 5 - + // 30 5 + // .. . + // + // ( ). + // . if ((obj->get_type() != EObjType::kMagicIngredient && obj->get_type() != EObjType::kMagicComponent) && (number(1, 100) - CalcCurrentSkill(ch, skill, 0) < 0)) { act(tagging.c_str(), false, ch, obj.get(), 0, kToChar); - // Прибавляем в экстра описание строчку. + // . char *tagchar = format_act(itemtag.c_str(), ch, obj.get(), 0); obj->set_tag(tagchar); free(tagchar); }; - // простановка мортов при шитье + // auto total_weight = count_mort_requred(obj.get()) * 7 / 10; if (total_weight > 35) { @@ -1809,14 +1809,14 @@ int MakeRecept::make(CharData *ch) { obj->set_minimum_remorts(0); } - // Пишем производителя в поле. + // . obj->set_crafter_uid(ch->get_uid()); - // 9. Проверяем минимум 2 + // 9. 2 if (ch->GetCarryingQuantity() >= CAN_CARRY_N(ch)) { - SendMsgToChar("Вы не сможете унести столько предметов.\r\n", ch); + SendMsgToChar(" .\r\n", ch); PlaceObjToRoom(obj.get(), ch->in_room); } else if (ch->GetCarryingWeight() + obj->get_weight() > CAN_CARRY_W(ch)) { - SendMsgToChar("Вы не сможете унести такой вес.\r\n", ch); + SendMsgToChar(" .\r\n", ch); PlaceObjToRoom(obj.get(), ch->in_room); } else { PlaceObjToInventory(obj.get(), ch); @@ -1825,11 +1825,11 @@ int MakeRecept::make(CharData *ch) { record_craft_success(obj_proto, skill); return (true); } -// вытащить рецепт из строки. -// Модификатор базовых значений. +// . +// . int MakeRecept::stat_modify(CharData *ch, int value, float devider) { - // Для числовых х-к: х-ка+(skill - random(100))/20; - // Для флагов ???: random(200) - skill > 0 то флаг переноситься. + // -: -+(skill - random(100))/20; + // ???: random(200) - skill > 0 . int res = value; float delta = 0; int skill_prc = 0; @@ -1844,7 +1844,7 @@ int MakeRecept::stat_modify(CharData *ch, int value, float devider) { delta = (value / 4) * delta / MUD::Skill(skill).difficulty / devider; } res += (int) delta; - // Если параметр завалили то возвращаем 1; + // 1; if (res < 0) { return 1; } @@ -1857,14 +1857,14 @@ void MakeRecept::add_rnd_skills(CharData * /*ch*/, ObjData *obj_from, ObjData *o int percent; CObjectPrototype::skills_t skills; obj_from->get_skills(skills); - int i = static_cast(skills.size()); // сколько добавлено умелок - rskill = number(0, i); // берем рандом + int i = static_cast(skills.size()); // + rskill = number(0, i); // for (const auto &it : skills) { - if (z == rskill) // ставим рандомную умелку + if (z == rskill) // { auto skill_num = it.first; percent = it.second; - // TODO: такого не должно быть? + // TODO: ? if (percent == 0) { continue; } @@ -1876,7 +1876,7 @@ void MakeRecept::add_rnd_skills(CharData * /*ch*/, ObjData *obj_from, ObjData *o } template int MakeRecept::add_flags(CharData *ch, F *base_flag, const F *add_flag, int/* delta*/) { -// БЕз вариантов вообще :( +// :( int tmpprob, skl = CalcCurrentSkill(ch, skill, 0); for (int i = 0; i < 3; i++) { for (int j = 0; j < 32; j++) { @@ -1891,9 +1891,9 @@ int MakeRecept::add_flags(CharData *ch, F *base_flag, const F *add_flag, int/* d template int MakeRecept::add_flags(CharData *, BitsetFlags *, const BitsetFlags *, int); template int MakeRecept::add_flags(CharData *, BitsetFlags *, const BitsetFlags *, int); -// Переносит extra-флаги ингредиента на предмет (как add_flags), но НЕ даёт ингредиенту -// навешивать служебные/привязочные/распадные флаги (issue #3459: крафт плодил "!бросить" и т.п.). -// Свои такие флаги предмет сохраняет -- блокируется только добавление их с ингредиента. +// extra- ( add_flags), +// // (issue #3459: "!" ..). +// -- . void MakeRecept::merge_extra_flags(CharData *ch, ObjData *obj, ObjData *ingr, int pow) { static const EObjFlag kNotTransferable[] = { EObjFlag::kNodrop, EObjFlag::kNorent, EObjFlag::kNodonate, EObjFlag::kNosell, @@ -1910,7 +1910,7 @@ void MakeRecept::merge_extra_flags(CharData *ch, ObjData *obj, ObjData *ingr, in auto temp = obj->get_extra_flags(); add_flags(ch, &temp, &ingr->get_extra_flags(), pow); obj->set_extra_flags(temp); - // откатываем только те служебные флаги, которых у предмета не было (т.е. их добавил ингредиент) + // , (.. ) for (std::size_t i = 0; i < std::size(kNotTransferable); ++i) { if (!had[i] && obj->has_flag(kNotTransferable[i])) { obj->unset_extraflag(kNotTransferable[i]); @@ -1932,7 +1932,7 @@ int MakeRecept::add_affects(CharData *ch, if (base[j].location == EApply::kNone) continue; if (add[i].location == base[j].location) { - // Аффекты совпали. + // . found = true; if (number(0, 100) > delta) break; @@ -1941,7 +1941,7 @@ int MakeRecept::add_affects(CharData *ch, } } if (!found) { - // Ищем первый свободный аффект и втыкаем туда новый. + // . for (int j = 0; j < kMaxObjAffect; j++) { if (base[j].location == EApply::kNone) { if (number(0, 100) > delta) @@ -1957,7 +1957,7 @@ int MakeRecept::add_affects(CharData *ch, return (true); } -// Форматирование вывода в соответствии с форматом act-a +// act-a // output act format// char *format_act(const char *orig, CharData *ch, ObjData *obj, const void *vict_obj) { const char *i = nullptr; diff --git a/src/gameplay/mechanics/groups.cpp b/src/gameplay/mechanics/groups.cpp index eac31300c..b7e38c2d6 100644 --- a/src/gameplay/mechanics/groups.cpp +++ b/src/gameplay/mechanics/groups.cpp @@ -2,8 +2,8 @@ \file groups.h - a part of the Bylins engine. \authors Created by Sventovit. \date 15.09.2024. -\brief Код групп игроков и мобов. -\detail Группы, вступление, покидание, дележка опыта - должно быть тут. +\brief . +\detail , , , - . */ #include "utils/native_text.h" @@ -34,7 +34,7 @@ bool group::same_group(CharData *ch, CharData *tch) { if (!ch || !tch) return false; - // Добавлены проверки чтобы не любой заследовавшийся моб считался согруппником (Купала) + // () if (ch->IsNpc() && ch->has_master() && !ch->get_master()->IsNpc() @@ -97,19 +97,19 @@ int group::perform_group(CharData *ch, CharData *vict) { AFF_FLAGS(vict).set(EAffect::kGroup); if (ch != vict) { - act("$N принят$A в члены вашего кружка (тьфу-ты, группы :).", false, ch, nullptr, vict, kToChar); - act("Вы приняты в группу $n1.", false, ch, nullptr, vict, kToVict); - act("$N принят$A в группу $n1.", false, ch, nullptr, vict, kToNotVict | kToArenaListen); + act("$N $A (-, :).", false, ch, nullptr, vict, kToChar); + act(" $n1.", false, ch, nullptr, vict, kToVict); + act("$N $A $n1.", false, ch, nullptr, vict, kToNotVict | kToArenaListen); } return (true); } /** -* Смена лидера группы на персонажа с макс лидеркой. -* Сам лидер при этом остается в группе, если он живой. -* \param vict - новый лидер, если смена происходит по команде 'гр лидер имя', -* старый лидер соответственно группится обратно, если нулевой, то считаем, что -* произошла смерть старого лидера и новый выбирается по наибольшей лидерке. +* . +* , . +* \param vict - , ' ', +* , , , +* . */ void change_leader(CharData *ch, CharData *vict) { if (ch->IsNpc() @@ -120,7 +120,7 @@ void change_leader(CharData *ch, CharData *vict) { CharData *leader = vict; if (!leader) { - // лидер умер, ищем согрупника с максимальным скиллом лидерки + // , for (auto *l : ch->followers) { if (!is_group_member(ch, l)) continue; @@ -135,7 +135,7 @@ void change_leader(CharData *ch, CharData *vict) { return; } - // для реследования используем стандартные функции + // std::vector temp_list; auto followers_copy = ch->followers; for (auto *l : followers_copy) { @@ -159,9 +159,9 @@ void change_leader(CharData *ch, CharData *vict) { } } - // бывшего лидера последним закидываем обратно в группу, если он живой + // , if (vict) { - // флаг группы надо снять, иначе при регрупе не будет писаться о старом лидере + // , //AFF_FLAGS(ch).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(ch); follow::AddFollowerSilently(leader, ch); @@ -179,64 +179,64 @@ void change_leader(CharData *ch, CharData *vict) { if (group::perform_group(leader, f)) ++num_followers; } else { - SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } } } void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { - const char *WORD_STATE[] = {"При смерти", - "Оч.тяж.ран", - "Оч.тяж.ран", - " Тяж.ранен", - " Тяж.ранен", - " Ранен ", - " Ранен ", - " Ранен ", - "Лег.ранен ", - "Лег.ранен ", - "Слег.ранен", - " Невредим " + const char *WORD_STATE[] = {" ", + "..", + "..", + " .", + " .", + " ", + " ", + " ", + ". ", + ". ", + ".", + " " }; - const char *MOVE_STATE[] = {"Истощен", - "Истощен", - "О.устал", - " Устал ", - " Устал ", - "Л.устал", - "Л.устал", - "Хорошо ", - "Хорошо ", - "Хорошо ", - "Отдохн.", - " Полон " + const char *MOVE_STATE[] = {"", + "", + ".", + " ", + " ", + ".", + ".", + " ", + " ", + " ", + ".", + " " }; auto generate_affects_string = [](CharData *k) -> std::string { std::string affects; - affects += AFF_FLAGGED(k, EAffect::kSanctuary) ? "&RО" : (AFF_FLAGGED(k, EAffect::kPrismaticAura) ? "&RП" : " "); - affects += AFF_FLAGGED(k, EAffect::kWaterBreath) ? "&gД" : " "; - affects += AFF_FLAGGED(k, EAffect::kInvisible) ? "&CН" : " "; + affects += AFF_FLAGGED(k, EAffect::kSanctuary) ? "&R" : (AFF_FLAGGED(k, EAffect::kPrismaticAura) ? "&R" : " "); + affects += AFF_FLAGGED(k, EAffect::kWaterBreath) ? "&g" : " "; + affects += AFF_FLAGGED(k, EAffect::kInvisible) ? "&C" : " "; affects += (AFF_FLAGGED(k, EAffect::kSingleLight) || AFF_FLAGGED(k, EAffect::kHolyLight) || (GET_EQ(k, EEquipPos::kLight) - && GET_OBJ_VAL(GET_EQ(k, EEquipPos::kLight), 2))) ? "&YС" : " "; - affects += AFF_FLAGGED(k, EAffect::kFly) ? "&BЛ" : " "; - affects += mount::IsOnHorse(k) ? "&YВ" : " "; + && GET_OBJ_VAL(GET_EQ(k, EEquipPos::kLight), 2))) ? "&Y" : " "; + affects += AFF_FLAGGED(k, EAffect::kFly) ? "&B" : " "; + affects += mount::IsOnHorse(k) ? "&Y" : " "; return affects; }; auto generate_debuf_string = [](CharData *k) -> std::string { std::string debuf; - debuf += AFF_FLAGGED(k, EAffect::kHold) ? "&RХ" : " "; - debuf += AFF_FLAGGED(k, EAffect::kSleep) ? "&BС" : " "; - debuf += AFF_FLAGGED(k, EAffect::kSilence) ? "&yМ" : " "; - debuf += AFF_FLAGGED(k, EAffect::kDeafness) ? "&gГ" : " "; - debuf += AFF_FLAGGED(k, EAffect::kBlind) ? "&YБ" : " "; - debuf += AFF_FLAGGED(k, EAffect::kCurse) ? "&mП" : " "; - debuf += IsAffected(k, EAffect::kFever) ? "&cЛ" : " "; + debuf += AFF_FLAGGED(k, EAffect::kHold) ? "&R" : " "; + debuf += AFF_FLAGGED(k, EAffect::kSleep) ? "&B" : " "; + debuf += AFF_FLAGGED(k, EAffect::kSilence) ? "&y" : " "; + debuf += AFF_FLAGGED(k, EAffect::kDeafness) ? "&g" : " "; + debuf += AFF_FLAGGED(k, EAffect::kBlind) ? "&Y" : " "; + debuf += AFF_FLAGGED(k, EAffect::kCurse) ? "&m" : " "; + debuf += IsAffected(k, EAffect::kFever) ? "&c" : " "; return debuf; }; @@ -249,10 +249,10 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { if (div > 0) { if (!IS_MANA_CASTER(k)) { ok2 = std::max(0, 1 + k->mem_queue.total - k->mem_queue.stored); - ok2 = ok2 * 60 / div; // время мема в сек + ok2 = ok2 * 60 / div; // } else { ok2 = std::max(0, 1 + Mana(GetRealWis(k)) - k->mem_queue.stored); - ok2 = ok2 / div; // время восстановления в секундах + ok2 = ok2 / div; // } ok = ok2 / 60; ok2 %= 60; @@ -271,19 +271,19 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { if (k->IsNpc()) { std::ostringstream buffer; if (!header) - buffer << "Персонаж | Здоровье | Рядом | Аффект | Дебаф | Положение\r\n"; + buffer << " | | | | | \r\n"; buffer << fmt::format("&B{:<20}&n|", k->get_name().substr(0, native_text::char_offset(k->get_name(), 20))); buffer << fmt::format("{}", GetWarmValueColor(k->get_hit(), k->get_real_max_hit())); buffer << fmt::format("{:<10}&n|", WORD_STATE[posi_value(k->get_hit(), k->get_real_max_hit()) + 1]); - buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&gДа" : "&rНет"); + buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&g" : "&r"); - // АФФЕКТЫ + // buffer << fmt::format(" {:<5}", generate_affects_string(k)); - buffer << fmt::format("{:<1} &n|", IsCharmExpiring(k) ? "&nТ" : " "); + buffer << fmt::format("{:<1} &n|", IsCharmExpiring(k) ? "&n" : " "); - // ДЕБАФЫ + // buffer << fmt::format(" {:<7} &n|", generate_debuf_string(k)); buffer << fmt::format(" {:<10}\r\n", position_types[(int) k->GetPosition()]); @@ -293,7 +293,7 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { } else { std::ostringstream buffer; if (!header) - buffer << "Персонаж | Здоровье | Энергия | Рядом | Учить | Аффект | Дебаф | Кто | Строй | Положение \r\n"; + buffer << " | | | | | | | | | \r\n"; std::string health_color = GetWarmValueColor(k->get_hit(), k->get_real_max_hit()); std::string move_color = GetWarmValueColor(k->get_move(), k->get_real_max_move()); @@ -306,15 +306,15 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { buffer << fmt::format("{}", move_color); buffer << fmt::format("{:^9}&n|", MOVE_STATE[posi_value(k->get_move(), k->get_real_max_move()) + 1]); - buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&gДа" : "&rНет"); + buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&g" : "&r"); buffer << fmt::format(" {:^5} &n|", generate_mem_string(k)); buffer << fmt::format(" {:<5} &n|", generate_affects_string(k)); buffer << fmt::format(" {:<7} &n|", generate_debuf_string(k)); - buffer << fmt::format(" {:^5} &n|", leader ? "Лидер" : ""); - buffer << fmt::format(" {:^5} &n|", k->IsFlagged(EPrf::kSkirmisher) ? " &gДа " : "Нет"); - buffer << fmt::format(" {:<10}\r\n", k->GetEnemy() ? "Сражается" : mount::IsOnHorse(k) ? "Верхом" : position_types[(int) k->GetPosition()]); + buffer << fmt::format(" {:^5} &n|", leader ? "" : ""); + buffer << fmt::format(" {:^5} &n|", k->IsFlagged(EPrf::kSkirmisher) ? " &g " : ""); + buffer << fmt::format(" {:<10}\r\n", k->GetEnemy() ? "" : mount::IsOnHorse(k) ? "" : position_types[(int) k->GetPosition()]); SendMsgToChar(buffer.str().c_str(), ch); } @@ -325,9 +325,9 @@ void group::print_list_group(CharData *ch) { int count = 1; k = (ch->has_master() ? ch->get_master() : ch); if (AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar("Ваша группа состоит из:\r\n", ch); + SendMsgToChar(" :\r\n", ch); if (AFF_FLAGGED(k, EAffect::kGroup)) { - sprintf(buf1, "Лидер: %s\r\n", GET_NAME(k)); + sprintf(buf1, ": %s\r\n", GET_NAME(k)); SendMsgToChar(buf1, ch); } @@ -335,12 +335,12 @@ void group::print_list_group(CharData *ch) { if (!AFF_FLAGGED(f, EAffect::kGroup)) { continue; } - sprintf(buf1, "%d. Согруппник: %s\r\n", count, GET_NAME(f)); + sprintf(buf1, "%d. : %s\r\n", count, GET_NAME(f)); SendMsgToChar(buf1, ch); count++; } } else { - SendMsgToChar("Но вы же не член (в лучшем смысле этого слова) группы!\r\n", ch); + SendMsgToChar(" ( ) !\r\n", ch); } } @@ -353,7 +353,7 @@ void group::print_group(CharData *ch) { ch->desc->msdp_report(msdp::constants::GROUP); if (AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar("Ваша группа состоит из:\r\n", ch); + SendMsgToChar(" :\r\n", ch); if (AFF_FLAGGED(k, EAffect::kGroup)) { group::print_one_line(ch, k, true, gfound++); } @@ -371,11 +371,11 @@ void group::print_group(CharData *ch) { continue; } if (!cfound) - SendMsgToChar("Ваши последователи:\r\n", ch); + SendMsgToChar(" :\r\n", ch); print_one_line(ch, f, false, cfound++); } if (!gfound && !cfound) { - SendMsgToChar("Но вы же не член (в лучшем смысле этого слова) группы!\r\n", ch); + SendMsgToChar(" ( ) !\r\n", ch); return; } if (ch->IsFlagged(EPrf::kShowGroup)) { @@ -400,7 +400,7 @@ void group::print_group(CharData *ch) { } if (!cfound) { - SendMsgToChar("Последователи членов вашей группы:\r\n", ch); + SendMsgToChar(" :\r\n", ch); } group::print_one_line(ch, f, false, cfound++); } @@ -419,7 +419,7 @@ void group::print_group(CharData *ch) { } if (!cfound) { - SendMsgToChar("Последователи членов вашей группы:\r\n", ch); + SendMsgToChar(" :\r\n", ch); } group::print_one_line(ch, g, false, cfound++); } @@ -437,11 +437,11 @@ void group::GoGroup(CharData *ch, char *argument) { CharData *vict; if (!str_cmp(buf, "all") - || !str_cmp(buf, "все")) { + || !str_cmp(buf, "")) { int found = 0; for (auto *f : ch->followers) { if ((f_number + found) >= group::max_group_size(ch)) { - SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; } if (IsAffected(f, EAffect::kFrenzy)) { @@ -451,13 +451,13 @@ void group::GoGroup(CharData *ch, char *argument) { } if (!found) { - SendMsgToChar("Все, кто за вами следуют, уже включены в вашу группу.\r\n", ch); + SendMsgToChar(", , .\r\n", ch); } else { group::perform_group(ch, ch); } return; - } else if (!str_cmp(buf, "leader") || !str_cmp(buf, "лидер")) { + } else if (!str_cmp(buf, "leader") || !str_cmp(buf, "")) { vict = target_resolver::FindPlayerVis(ch, argument); if (vict && vict->IsNpc() @@ -473,14 +473,14 @@ void group::GoGroup(CharData *ch, char *argument) { } if (!vict) { - SendMsgToChar("Нет такого персонажа.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } else if (vict == ch) { - SendMsgToChar("Вы и так лидер группы...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); return; } else if (!AFF_FLAGGED(vict, EAffect::kGroup) || vict->get_master() != ch) { - SendMsgToChar(ch, "%s не является членом вашей группы.\r\n", GET_NAME(vict)); + SendMsgToChar(ch, "%s .\r\n", GET_NAME(vict)); return; } change_leader(ch, vict); @@ -492,25 +492,25 @@ void group::GoGroup(CharData *ch, char *argument) { if (!vict) { SendMsgToChar(CommonMsg(ECommonMsg::kNoPerson) + "\r\n", ch); } else if ((vict->get_master() != ch) && (vict != ch)) { - act("$N2 нужно следовать за вами, чтобы стать членом вашей группы.", false, ch, nullptr, vict, kToChar); + act("$N2 , .", false, ch, nullptr, vict, kToChar); } else if (IsAffected(vict, EAffect::kFrenzy)) { - act("$N слишком агрессивн$W и непредсказуем$W! Нельзя брать $s!", false, ch, nullptr, vict, kToChar); + act("$N $W $W! $s!", false, ch, nullptr, vict, kToChar); } else { if (!AFF_FLAGGED(vict, EAffect::kGroup)) { if (vict->IsFlagged(EMobFlag::kCompanion) || mount::IsHorse(vict)) { - SendMsgToChar("Только равноправные персонажи могут быть включены в группу.\r\n", ch); - SendMsgToChar("Только равноправные персонажи могут быть включены в группу.\r\n", vict); + SendMsgToChar(" .\r\n", ch); + SendMsgToChar(" .\r\n", vict); }; if (f_number >= group::max_group_size(ch)) { - SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } group::perform_group(ch, ch); group::perform_group(ch, vict); } else if (ch != vict) { - act("$N исключен$A из состава вашей группы.", false, ch, nullptr, vict, kToChar); - act("Вы исключены из группы $n1!", false, ch, nullptr, vict, kToVict); - act("$N был$G исключен$A из группы $n1!", false, ch, nullptr, vict, kToNotVict | kToArenaListen); + act("$N $A .", false, ch, nullptr, vict, kToChar); + act(" $n1!", false, ch, nullptr, vict, kToVict); + act("$N $G $A $n1!", false, ch, nullptr, vict, kToNotVict | kToArenaListen); //AFF_FLAGS(vict).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(vict); } @@ -520,7 +520,7 @@ void group::GoGroup(CharData *ch, char *argument) { void group::GoUngroup(CharData *ch, char *argument) { CharData *tch; if (!*argument) { - sprintf(buf2, "Вы исключены из группы %s.\r\n", GET_PAD(ch, 1)); + sprintf(buf2, " %s.\r\n", GET_PAD(ch, 1)); auto copy = ch->followers; for (auto *f : copy) { if (AFF_FLAGGED(f, EAffect::kGroup)) { @@ -536,7 +536,7 @@ void group::GoUngroup(CharData *ch, char *argument) { } AFF_FLAGS(ch).unset(EAffect::kGroup); group::RemoveGroupFlags(ch); - SendMsgToChar("Вы распустили группу.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } auto copy2 = ch->followers; @@ -547,14 +547,14 @@ void group::GoUngroup(CharData *ch, char *argument) { && !mount::IsHorse(tch)) { //AFF_FLAGS(tch).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(tch); - act("$N более не член вашей группы.", false, ch, nullptr, tch, kToChar); - act("Вы исключены из группы $n1!", false, ch, nullptr, tch, kToVict); - act("$N был$G изгнан$A из группы $n1!", false, ch, nullptr, tch, kToNotVict | kToArenaListen); + act("$N .", false, ch, nullptr, tch, kToChar); + act(" $n1!", false, ch, nullptr, tch, kToVict); + act("$N $G $A $n1!", false, ch, nullptr, tch, kToNotVict | kToArenaListen); follow::StopFollower(tch, follow::kSfEmpty); return; } } - SendMsgToChar("Этот игрок не входит в состав вашей группы.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -562,11 +562,11 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!*argument) { if (!AFF_FLAGGED(ch, EAffect::kGroup) && !AFF_FLAGGED(ch, EAffect::kCharmed)) { - SendMsgToChar("И перед кем вы отчитываетесь?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } if (IS_MANA_CASTER(ch)) { - sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V, %d(%d)M\r\n", + sprintf(buf, "%s %s : %d(%d)H, %d(%d)V, %d(%d)M\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move(), @@ -580,13 +580,13 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V, %dL\r\n", + sprintf(buf, "%s %s : %d(%d)H, %d(%d)V, %dL\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move(), loyalty); } else { - sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V\r\n", + sprintf(buf, "%s %s : %d(%d)H, %d(%d)V\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move()); @@ -603,18 +603,18 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (k != ch && !AFF_FLAGGED(k, EAffect::kDeafness)) { SendMsgToChar(buf, k); } - SendMsgToChar("Вы доложили о состоянии всем членам вашей группы.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } else { - if (CompareParam(argument, "умения")) { + if (CompareParam(argument, "")) { if (ch->followers.empty()) { - SendMsgToChar(ch, "За вами никто не следует."); + SendMsgToChar(ch, " ."); return; } for (auto *f : ch->followers) { if (IsCharmice(f)) { std::vector skills; - SendMsgToChar(ch, "%s доложил%s свои умения:", utils::CAP(f->get_name()).c_str(), grammar::SexEnding((f)->get_sex(), 1)); + SendMsgToChar(ch, "%s %s :", utils::CAP(f->get_name()).c_str(), grammar::SexEnding((f)->get_sex(), 1)); for (const auto &skill : MUD::Skills()) { if (skill.IsValid() && GetSkill(f, skill.GetId())) { skills.emplace_back(skill.GetName()); @@ -623,11 +623,11 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar(ch, "&C %s&n\r\n", utils::OutWordsList(skills, ch->player_specials->saved.stringLength).c_str()); } else { - SendMsgToChar(ch, "%s не подчиняется вам, и не хочет ничего докладывать.\r\n", utils::CAP(f->get_name()).c_str()); + SendMsgToChar(ch, "%s , .\r\n", utils::CAP(f->get_name()).c_str()); } } } else { - SendMsgToChar(ch, "Кому чего доложить?\r\n"); + SendMsgToChar(ch, " ?\r\n"); } } } @@ -649,19 +649,19 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, if (is_number(buf)) { amount = atoi(buf); if (amount <= 0) { - SendMsgToChar("И как вы это планируете сделать?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } if (amount > currencies::GetHand(*ch, currency_vnum)) { - SendMsgToChar("И где бы взять вам столько денег?.\r\n", ch); + SendMsgToChar(" ?.\r\n", ch); return; } k = ch->has_master() ? ch->get_master() : ch; - // issue #3669: лидера-моба считать нельзя -- долю получают только игроки - // (ниже стоит !k->IsNpc()), иначе его доля списывалась бы у делящего и - // не доставалась никому. + // issue #3669: - -- + // ( !k->IsNpc()), + // . if (AFF_FLAGGED(k, EAffect::kGroup) && !k->IsNpc() && (k->in_room == ch->in_room)) { @@ -670,7 +670,7 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, num = 0; } - int absent = 0; // issue #3669: члены группы вне комнаты -- долю не получают + int absent = 0; // issue #3669: -- if (AFF_FLAGGED(k, EAffect::kGroup) && !k->IsNpc() && k->in_room != ch->in_room) { @@ -692,14 +692,14 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, share = amount / num; rest = amount % num; } else { - SendMsgToChar("С кем вы хотите разделить это добро?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } - // issue #3669: делить нечего -- каждому вышло бы по нулю. Молча оставляем - // всё делящему, а не рассылаем группе "вам досталось 0". + // issue #3669: -- . + // ӣ , " 0". if (share <= 0) { - SendMsgToChar(ch, "%d %s на %d не разделить -- оставили себе.\r\n", + SendMsgToChar(ch, "%d %s %d -- .\r\n", amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), num); return; } @@ -707,7 +707,7 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, currencies::RemoveHand(*ch, currency_vnum, share * (num - 1)); - sprintf(buf, "%s разделил%s %d %s; вам досталось %d.\r\n", + sprintf(buf, "%s %s %d %s; %d.\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), share); if (AFF_FLAGGED(k, EAffect::kGroup) && k->in_room == ch->in_room && !k->IsNpc() && k != ch) { SendMsgToChar(buf, k); @@ -722,29 +722,29 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, currencies::AddHand(*f, currency_vnum, share - (currency_vnum == currencies::kGoldVnum ? ClanSystem::do_gold_tax(f, share) : 0), false, true); } } - sprintf(buf, "Вы разделили %d %s на %d - по %d каждому.\r\n", + sprintf(buf, " %d %s %d - %d .\r\n", amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), num, share); if (rest) { sprintf(buf + strlen(buf), - "Как истинный еврей вы оставили %d %s (которые не смогли разделить нацело) себе.\r\n", + " %d %s ( ) .\r\n", rest, MUD::Currency(currency_vnum).GetNameWithAmount(rest, grammar::ECase::kAcc).c_str()); } - // issue #3669: делится только на тех, кто рядом. Иначе непонятно, почему - // в группе народу больше, чем долей. + // issue #3669: , . , + // , . if (absent > 0) { sprintf(buf + strlen(buf), - "Членов группы вне комнаты: %d -- доля им не досталась.\r\n", absent); + " : %d -- .\r\n", absent); } SendMsgToChar(buf, ch); - // клан-налог лутера с той части, которая пошла каждому в группе + // - , if (currency_vnum == currencies::kGoldVnum) { const long clan_tax = ClanSystem::do_gold_tax(ch, share); currencies::RemoveHand(*ch, currencies::kGold, clan_tax); } } else { - SendMsgToChar("Сколько и чего вы хотите разделить?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return; } } diff --git a/src/gameplay/mechanics/obj_sets_olc.cpp b/src/gameplay/mechanics/obj_sets_olc.cpp index 1e09f69ff..c1d1e2d9d 100644 --- a/src/gameplay/mechanics/obj_sets_olc.cpp +++ b/src/gameplay/mechanics/obj_sets_olc.cpp @@ -38,20 +38,20 @@ using namespace obj_sets; namespace obj_sets_olc { -/// класс для редактирования сета в олц, содержит состояние редактирования -/// (наружу торчит только CON_SEDIT) и всю временную инфу под это дело +/// , +/// ( CON_SEDIT) class sedit { public: sedit() : state(-1), new_entry(false), obj_edit(-1), activ_edit(-1), apply_edit(-1) {}; - // стадия редактирования (в какой менюшке) + // ( ) int state; - // правка глобальных сообщений + // obj_sets::SetMsgNode msg_edit; - // редактируемый сет + // obj_sets::SetNode olc_set; - // идет создание нового сета + // bool new_entry; void parse_global_msg(CharData *ch, const char *arg); @@ -85,11 +85,11 @@ class sedit { void show_global_msg(CharData *ch); private: - // если правится предмет - его внум + // - int obj_edit; - // если правится активатор - его размер + // - int activ_edit; - // если правится аффект - его индекс в списке + // - size_t apply_edit; void show_activ_ench_vnum(CharData *ch); @@ -99,10 +99,10 @@ class sedit { void show_activ_affects(CharData *ch); void show_activ_edit(CharData *ch); void show_obj_edit(CharData *ch); - // проверка поменялось ли что-то в сете, - // чтобы не спамить про сохранения при выходе + // - , + // bool changed(); - // для сравнения в changed() + // changed() bool operator!=(const sedit &r) const { return olc_set != r.olc_set; } @@ -154,7 +154,7 @@ enum { STATE_GLBMSG_ROOM_OFF }; -// распечатка главного меню + смещение для следующих пунктов через MAIN_TOTAL +// + MAIN_TOTAL enum { MAIN_SET_REMOVE = 1, MAIN_ENABLED, @@ -169,11 +169,11 @@ enum { }; namespace { -const auto MISSING_OBJECT_NAME = "&R<объект с таким VNUM не существует>&n"; +const auto MISSING_OBJECT_NAME = "&R< VNUM >&n"; } -/// распечатка форматированного списка шмоток сета, форматирование идет как -/// как по столбцам, так и по длине имени и внума шмоток, вобщем чтоб красиво +/// , +/// , , std::string main_menu_objlist(const SetNode &set, int menu) { std::string out; char buf_[128]; @@ -234,37 +234,37 @@ std::string main_menu_objlist(const SetNode &set, int menu) { const char *main_menu_str(SetNode &olc_set, int num) { static char buf_[1024]; switch (num) { - case MAIN_SET_REMOVE: return "Удалить набор"; + case MAIN_SET_REMOVE: return " "; case MAIN_ENABLED: - snprintf(buf_, sizeof(buf_), "Статус : %s%s%s", kColorCyn, - olc_set.enabled ? "активен" : "неактивен", kColorNrm); + snprintf(buf_, sizeof(buf_), " : %s%s%s", kColorCyn, + olc_set.enabled ? "" : "", kColorNrm); break; case MAIN_NAME: - snprintf(buf_, sizeof(buf_), "Имя : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.name.c_str()); break; case MAIN_ALIAS: - snprintf(buf_, sizeof(buf_), "Алиасы : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.alias.c_str()); break; case MAIN_COMMENT: - snprintf(buf_, sizeof(buf_), "Комментарий : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.comment.c_str()); break; case MAIN_MSG_CHAR_ON: - snprintf(buf_, sizeof(buf_), "Активатор персонажу : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.messages.char_on_msg.c_str()); break; case MAIN_MSG_CHAR_OFF: - snprintf(buf_, sizeof(buf_), "Деактиватор персонажу : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.messages.char_off_msg.c_str()); break; case MAIN_MSG_ROOM_ON: - snprintf(buf_, sizeof(buf_), "Активатор в комнату : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.messages.room_on_msg.c_str()); break; case MAIN_MSG_ROOM_OFF: - snprintf(buf_, sizeof(buf_), "Деактиватор в комнату : %s", + snprintf(buf_, sizeof(buf_), " : %s", olc_set.messages.room_off_msg.c_str()); break; default: return ""; @@ -279,17 +279,17 @@ void sedit::show_main(CharData *ch) { std::string out("\r\n"); if (new_entry) { - out += "Создание нового набора предметов\r\n"; + out += " \r\n"; } else { size_t idx = setidx_by_uid(olc_set.uid); if (idx >= sets_list.size()) { - SendMsgToChar("Редактирование прервано: набор был удален.\r\n", ch); + SendMsgToChar(" : .\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; return; } snprintf(buf_, sizeof(buf_), - "Редактирование набора предметов #%llu\r\n", static_cast(idx + 1)); + " #%llu\r\n", static_cast(idx + 1)); out += buf_; } int i = 1; @@ -299,18 +299,18 @@ void sedit::show_main(CharData *ch) { main_menu_str(olc_set, i)); out += buf_; } - // предметы - snprintf(buf_, sizeof(buf_), "\r\n%s%2d%s) Добавить предмет(ы)\r\n", + // + snprintf(buf_, sizeof(buf_), "\r\n%s%2d%s) ()\r\n", kColorGrn, i++, kColorNrm); out += buf_; out += main_menu_objlist(olc_set, i); i += static_cast(olc_set.obj_list.size()); - // активаторы + // snprintf(buf_, sizeof(buf_), - "\r\n%s%2d%s) Распечатать сумму активаторов\r\n", + "\r\n%s%2d%s) \r\n", kColorGrn, i++, kColorNrm); out += buf_; - snprintf(buf_, sizeof(buf_), "%s%2d%s) Добавить активатор\r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) \r\n", kColorGrn, i++, kColorNrm); out += buf_; for (auto & k : olc_set.activ_list) { @@ -318,13 +318,13 @@ void sedit::show_main(CharData *ch) { std::string prof; PrinSetClasses(k.second.prof, prof); snprintf(buf_, sizeof(buf_), - "%s%2d%s) Редактировать активатор: %s%d (%s)%s\r\n", + "%s%2d%s) : %s%d (%s)%s\r\n", kColorGrn, i++, kColorNrm, kColorCyn, k.first, prof.c_str(), kColorNrm); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) Редактировать активатор: %s%d%s\r\n", + "%s%2d%s) : %s%d%s\r\n", kColorGrn, i++, kColorNrm, kColorCyn, k.first, kColorNrm); } @@ -332,8 +332,8 @@ void sedit::show_main(CharData *ch) { } snprintf(buf_, sizeof(buf_), - "\r\n%s%2d%s) Выйти Q(В)\r\n" - "Ваш выбор : ", + "\r\n%s%2d%s) Q()\r\n" + " : ", kColorGrn, i++, kColorNrm); out += buf_; @@ -346,7 +346,7 @@ void sedit::show_obj_edit(CharData *ch) { auto obj = olc_set.obj_list.find(obj_edit); if (obj == olc_set.obj_list.end()) { SendMsgToChar(ch, - "Ошибка: предмет не в наборе %s:%d (%s).\r\n", + ": %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; @@ -359,15 +359,15 @@ void sedit::show_obj_edit(CharData *ch) { : obj_proto[rnum]->get_short_description().c_str(); char buf_[2048]; snprintf(buf_, sizeof(buf_), - "\r\nРедактирование предмета '%s'\r\n" - "%s 1%s) Удалить из набора\r\n" - "%s 2%s) Виртуальный номер (vnum) : %s%d%s\r\n" - "%s 3%s) Активатор персонажу : %s\r\n" - "%s 4%s) Деактиватор персонажу : %s\r\n" - "%s 5%s) Активатор в комнату : %s\r\n" - "%s 6%s) Деактиватор в комнату : %s\r\n" - "%s 7%s) В главное меню Q(В)\r\n" - "Ваш выбор : ", + "\r\n '%s'\r\n" + "%s 1%s) \r\n" + "%s 2%s) (vnum) : %s%d%s\r\n" + "%s 3%s) : %s\r\n" + "%s 4%s) : %s\r\n" + "%s 5%s) : %s\r\n" + "%s 6%s) : %s\r\n" + "%s 7%s) Q()\r\n" + " : ", name, kColorGrn, kColorNrm, kColorGrn, kColorNrm, @@ -385,7 +385,7 @@ void sedit::show_activ_edit(CharData *ch) { auto i = olc_set.activ_list.find(activ_edit); if (i == olc_set.activ_list.end()) { - SendMsgToChar(ch, "Ошибка: активатор не найден %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); + SendMsgToChar(ch, ": %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; } @@ -398,17 +398,17 @@ void sedit::show_activ_edit(CharData *ch) { if (!activ.prof.all()) { PrinSetClasses(activ.prof, prof_str); } else { - prof_str = "все"; + prof_str = ""; } std::string out; char buf_[2048]; snprintf(buf_, sizeof(buf_), - "\r\nРедактирование активатора\r\n" - "%s 1%s) Удалить из набора\r\n" - "%s 2%s) Количество предметов : %s%d%s\r\n" - "%s 3%s) Профессии : %s%s%s\r\n" - "%s 4%s) Аффекты : %s%s%s\r\n", + "\r\n \r\n" + "%s 1%s) \r\n" + "%s 2%s) : %s%d%s\r\n" + "%s 3%s) : %s%s%s\r\n" + "%s 4%s) : %s%s%s\r\n", kColorGrn, kColorNrm, kColorGrn, kColorNrm, kColorCyn, activ_edit, kColorNrm, @@ -421,26 +421,26 @@ void sedit::show_activ_edit(CharData *ch) { int cnt = 5; for (auto apply : activ.apply) { if (apply.location > 0) { - snprintf(buf_, sizeof(buf_), "%s%2d%s) Наводимый аффект : %s", + snprintf(buf_, sizeof(buf_), "%s%2d%s) : %s", kColorGrn, cnt++, kColorNrm, print_obj_affects(apply).c_str()); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) Наводимый аффект : %s%s%s\r\n", + "%s%2d%s) : %s%s%s\r\n", kColorGrn, cnt++, kColorNrm, - kColorCyn, "ничего", kColorNrm); + kColorCyn, "", kColorNrm); } out += buf_; } if (MUD::Skills().IsValid(activ.skill.first)) { snprintf(buf_, sizeof(buf_), - "%s%2d%s) Изменяемое умение : %s%+d to %s%s\r\n", + "%s%2d%s) : %s%+d to %s%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.skill.second, MUD::Skill(activ.skill.first).GetName(), kColorNrm); } else { - snprintf(buf_, sizeof(buf_), "%s%2d%s) Изменяемое умение : нет\r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) : \r\n", kColorGrn, cnt++, kColorNrm); } out += buf_; @@ -451,45 +451,45 @@ void sedit::show_activ_edit(CharData *ch) { (rnum >= 0 ? obj_proto[rnum]->get_short_description().c_str() : ""); if (obj_proto[rnum]->get_type() == EObjType::kWeapon) { snprintf(buf_, sizeof(buf_), - "%s%2d%s) Зачарование предмета : %s[%d] %s вес %+d, кубики %+dD%+d%s\r\n", + "%s%2d%s) : %s[%d] %s %+d, %+dD%+d%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.enchant.first, name, activ.enchant.second.weight, activ.enchant.second.ndice, activ.enchant.second.sdice, kColorNrm); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) Зачарование предмета : %s[%d] %s вес %+d%s\r\n", + "%s%2d%s) : %s[%d] %s %+d%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.enchant.first, name, activ.enchant.second.weight, kColorNrm); } } else { - snprintf(buf_, sizeof(buf_), "%s%2d%s) Зачарование предмета: нет\r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) : \r\n", kColorGrn, cnt++, kColorNrm); } out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) Увеличение физ. урона : %s%+d%%%s\r\n", + "%s%2d%s) . : %s%+d%%%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.bonus.phys_dmg, kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) Увеличение маг. урона : %s%+d%%%s\r\n", + "%s%2d%s) . : %s%+d%%%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.bonus.mage_dmg, kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) Актив на мобах : %s%s%s\r\n", + "%s%2d%s) : %s%s%s\r\n", kColorGrn, cnt++, kColorNrm, - kColorCyn, (activ.npc ? "Да" : "Нет"), kColorNrm); + kColorCyn, (activ.npc ? "" : ""), kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) В главное меню Q(В)\r\n" - "Ваш выбор : ", + "%s%2d%s) Q()\r\n" + " : ", kColorGrn, cnt++, kColorNrm); out += buf_; @@ -521,7 +521,7 @@ void sedit::save_olc(CharData *ch) { obj_sets::save(); init_obj_index(); } else { - SendMsgToChar("Ошибка сохранения: набор был удален.", ch); + SendMsgToChar(" : .", ch); } } @@ -534,19 +534,19 @@ void parse_main_exit(CharData *ch, const char *arg) { case rus::kDeUpper: ch->desc->state = EConState::kPlaying; ch->desc->sedit->save_olc(ch); ch->desc->sedit.reset(); - SendMsgToChar("Изменения сохранены.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; case 'n': case 'N': case rus::kEn: case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; default: SendMsgToChar( - "Неверный выбор!\r\n" - "Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + " !\r\n" + " ? Y()/N() : ", ch); break; } } @@ -563,7 +563,7 @@ void parse_set_remove(CharData *ch, const char *arg) { sets_list.erase(i); obj_sets::save(); init_obj_index(); - SendMsgToChar("Набор удален.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; } } @@ -574,13 +574,13 @@ void parse_set_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); ch->desc->sedit->show_main(ch); break; default: SendMsgToChar( - "Неверный выбор!\r\n" - "Подтвердите удаление набора Y(Д)/N(Н) : ", ch); + " !\r\n" + " Y()/N() : ", ch); break; } } @@ -595,7 +595,7 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { auto i = olc_set.obj_list.find(obj_edit); if (i != olc_set.obj_list.end()) { olc_set.obj_list.erase(i); - SendMsgToChar("Предмет удален из набора.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } obj_edit = -1; show_main(ch); @@ -604,11 +604,11 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); show_obj_edit(ch); break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); - SendMsgToChar("Подтвердите удаление предмета Y(Д)/N(Н) :", ch); + default: SendMsgToChar(" !\r\n", ch); + SendMsgToChar(" Y()/N() :", ch); break; } } @@ -623,7 +623,7 @@ void sedit::parse_activ_remove(CharData *ch, const char *arg) { auto i = olc_set.activ_list.find(activ_edit); if (i != olc_set.activ_list.end()) { olc_set.activ_list.erase(i); - SendMsgToChar("Активатор удален из набора.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } activ_edit = -1; show_main(ch); @@ -632,11 +632,11 @@ void sedit::parse_activ_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); + case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); show_activ_edit(ch); break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); - SendMsgToChar("Подтвердите удаление активатора Y(Д)/N(Н) :", ch); + default: SendMsgToChar(" !\r\n", ch); + SendMsgToChar(" Y()/N() :", ch); break; } } @@ -646,13 +646,13 @@ void sedit::show_global_msg(CharData *ch) { char buf_[1024]; snprintf(buf_, sizeof(buf_), - "\r\nРедактирование глобальных сообщений\r\n" - "%s 1%s) Активатор персонажу : %s\r\n" - "%s 2%s) Деактиватор персонажу : %s\r\n" - "%s 3%s) Активатор в комнату : %s\r\n" - "%s 4%s) Деактиватор в комнату : %s\r\n" - "%s 5%s) Выйти Q(В)\r\n" - "Ваш выбор : ", + "\r\n \r\n" + "%s 1%s) : %s\r\n" + "%s 2%s) : %s\r\n" + "%s 3%s) : %s\r\n" + "%s 4%s) : %s\r\n" + "%s 5%s) Q()\r\n" + " : ", kColorGrn, kColorNrm, msg_edit.char_on_msg.c_str(), kColorGrn, kColorNrm, msg_edit.char_off_msg.c_str(), kColorGrn, kColorNrm, msg_edit.room_on_msg.c_str(), @@ -664,7 +664,7 @@ void sedit::show_global_msg(CharData *ch) { void sedit::parse_global_msg(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar(" !\r\n", ch); show_global_msg(ch); return; } @@ -675,15 +675,15 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: if (msg_edit != global_msg) { - SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + SendMsgToChar(" ? Y()/N() : ", ch); state = STATE_GLOBAL_MSG_EXIT; } else { - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_global_msg(ch); break; } @@ -693,29 +693,29 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case 1: SendMsgToChar("Сообщение персонажу при активации : ", ch); + case 1: SendMsgToChar(" : ", ch); state = STATE_GLBMSG_CHAR_ON; break; - case 2: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); + case 2: SendMsgToChar(" : ", ch); state = STATE_GLBMSG_CHAR_OFF; break; - case 3: SendMsgToChar("Сообщение в комнату при активации : ", ch); + case 3: SendMsgToChar(" : ", ch); state = STATE_GLBMSG_ROOM_ON; break; - case 4: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); + case 4: SendMsgToChar(" : ", ch); state = STATE_GLBMSG_ROOM_OFF; break; case 5: if (msg_edit != global_msg) { - SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + SendMsgToChar(" ? Y()/N() : ", ch); state = STATE_GLOBAL_MSG_EXIT; } else { - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_global_msg(ch); break; } @@ -731,19 +731,19 @@ void parse_global_msg_exit(CharData *ch, const char *arg) { global_msg = ch->desc->sedit->msg_edit; obj_sets::save(); ch->desc->sedit.reset(); - SendMsgToChar("Изменения сохранены.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; case 'n': case 'N': case rus::kEn: case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; default: SendMsgToChar( - "Неверный выбор!\r\n" - "Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + " !\r\n" + " ? Y()/N() : ", ch); break; } } @@ -751,7 +751,7 @@ void parse_global_msg_exit(CharData *ch, const char *arg) { void sedit::parse_main(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar(" !\r\n", ch); show_main(ch); return; } @@ -762,15 +762,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: if (new_entry || changed()) { - SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + SendMsgToChar(" ? Y()/N() : ", ch); state = STATE_MAIN_EXIT; } else { - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_main(ch); break; } @@ -780,33 +780,33 @@ void sedit::parse_main(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case MAIN_SET_REMOVE: SendMsgToChar("Подтвердите удаление набора Y(Д)/N(Н) : ", ch); + case MAIN_SET_REMOVE: SendMsgToChar(" Y()/N() : ", ch); state = STATE_SET_REMOVE; return; case MAIN_ENABLED: olc_set.enabled = !olc_set.enabled; show_main(ch); return; - case MAIN_NAME: SendMsgToChar("Имя набора (опознание, справка сеты) : ", ch); + case MAIN_NAME: SendMsgToChar(" (, ) : ", ch); state = STATE_NAME; return; case MAIN_ALIAS: SendMsgToChar( - "Алиасы набора для справки (через запятую и/или пробел) : ", ch); + " ( / ) : ", ch); state = STATE_ALIAS; return; - case MAIN_COMMENT: SendMsgToChar("Ваш комментарий до 40 символов (виден по slist) : ", ch); + case MAIN_COMMENT: SendMsgToChar(" 40 ( slist) : ", ch); state = STATE_COMMENT; return; - case MAIN_MSG_CHAR_ON: SendMsgToChar("Сообщение персонажу при активации : ", ch); + case MAIN_MSG_CHAR_ON: SendMsgToChar(" : ", ch); state = STATE_SETMSG_CHAR_ON; return; - case MAIN_MSG_CHAR_OFF: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); + case MAIN_MSG_CHAR_OFF: SendMsgToChar(" : ", ch); state = STATE_SETMSG_CHAR_OFF; return; - case MAIN_MSG_ROOM_ON: SendMsgToChar("Сообщение в комнату при активации : ", ch); + case MAIN_MSG_ROOM_ON: SendMsgToChar(" : ", ch); state = STATE_SETMSG_ROOM_ON; return; - case MAIN_MSG_ROOM_OFF: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); + case MAIN_MSG_ROOM_OFF: SendMsgToChar(" : ", ch); state = STATE_SETMSG_ROOM_OFF; return; default: break; @@ -817,15 +817,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { const unsigned NUM_ADD_ACTIV = NUM_TOTAL_ACTIV + 1; const unsigned NUM_QUIT = NUM_ADD_ACTIV + static_cast(olc_set.activ_list.size()) + 1; - // после статичного меню идут предметы, за ними активаторы + // , if (num == NUM_ADD_OBJ) { SendMsgToChar( - "Vnum добавляемого предмета (несколько через пробел) : ", ch); + "Vnum ( ) : ", ch); state = STATE_OBJ_ADD; } else if (!olc_set.obj_list.empty() && num > NUM_ADD_OBJ && num <= NUM_ADD_OBJ + olc_set.obj_list.size()) { - // редактирование предмета + // const unsigned offset = NUM_ADD_OBJ + 1; auto i = olc_set.obj_list.begin(); if (num > offset) { @@ -836,16 +836,16 @@ void sedit::parse_main(CharData *ch, const char *arg) { } else if (num == NUM_TOTAL_ACTIV) { state = STATE_TOTAL_ACTIV; SendMsgToChar(print_total_activ(olc_set), ch); - SendMsgToChar("Введите любую команду для продолжения : ", ch); + SendMsgToChar(" : ", ch); } else if (num == NUM_ADD_ACTIV) { SendMsgToChar(ch, - "Укажите кол-во предметов для активации (%u-%u) : ", + " - (%u-%u) : ", MIN_ACTIVE_SIZE, MAX_ACTIVE_SIZE); state = STATE_ACTIV_ADD; } else if (!olc_set.activ_list.empty() && num > NUM_ADD_ACTIV && num <= NUM_ADD_ACTIV + olc_set.activ_list.size()) { - // редактирование активатора + // const unsigned offset = NUM_ADD_ACTIV + 1; auto i = olc_set.activ_list.begin(); if (num > offset) { @@ -855,15 +855,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { show_activ_edit(ch); } else if (num == NUM_QUIT) { if (new_entry || changed()) { - SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); + SendMsgToChar(" ? Y()/N() : ", ch); state = STATE_MAIN_EXIT; } else { - SendMsgToChar("Редактирование отменено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } } else { - SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar(" !\r\n", ch); show_main(ch); } } @@ -871,7 +871,7 @@ void sedit::parse_main(CharData *ch, const char *arg) { void sedit::parse_setname(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Имя набора удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); olc_set.name.clear(); } else { olc_set.name = arg; @@ -882,7 +882,7 @@ void sedit::parse_setname(CharData *ch, const char *arg) { void sedit::parse_setalias(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Алиас набора удален.\r\n", ch); + SendMsgToChar(" .\r\n", ch); olc_set.alias.clear(); } else { olc_set.alias = arg; @@ -893,7 +893,7 @@ void sedit::parse_setalias(CharData *ch, const char *arg) { void sedit::parse_setcomment(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Комментарий набора удален.\r\n", ch); + SendMsgToChar(" .\r\n", ch); olc_set.comment.clear(); } else { olc_set.comment = arg; @@ -902,7 +902,7 @@ void sedit::parse_setcomment(CharData *ch, const char *arg) { show_main(ch); } -/// чтобы в одном месте срау обработать все три вида сообщений +/// enum { PARSE_GLB_MSG, PARSE_SET_MSG, PARSE_OBJ_MSG }; void sedit::parse_setmsg(CharData *ch, const char *arg) { @@ -932,25 +932,25 @@ void sedit::parse_setmsg(CharData *ch, const char *arg) { case STATE_OBJMSG_CHAR_ON: case STATE_GLBMSG_CHAR_ON: if ((msg.char_on_msg = arg).empty()) - SendMsgToChar("Сообщение активатора персонажу удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; case STATE_SETMSG_CHAR_OFF: case STATE_OBJMSG_CHAR_OFF: case STATE_GLBMSG_CHAR_OFF: if ((msg.char_off_msg = arg).empty()) - SendMsgToChar("Сообщение деактиватора персонажу удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; case STATE_SETMSG_ROOM_ON: case STATE_OBJMSG_ROOM_ON: case STATE_GLBMSG_ROOM_ON: if ((msg.room_on_msg = arg).empty()) - SendMsgToChar("Сообщение активатора в комнату удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; case STATE_SETMSG_ROOM_OFF: case STATE_OBJMSG_ROOM_OFF: case STATE_GLBMSG_ROOM_OFF: if ((msg.room_off_msg = arg).empty()) - SendMsgToChar("Сообщение деактиватора в комнату удалено.\r\n", ch); + SendMsgToChar(" .\r\n", ch); break; default: break; } @@ -970,16 +970,16 @@ void sedit::parse_activ_add(CharData *ch, const char *arg) { if (!*arg || !a_isdigit(*arg) || (num = atoi(arg)) < MIN_ACTIVE_SIZE || num > MAX_ACTIVE_SIZE) { - SendMsgToChar("Некорректное кол-во предметов для активации.\r\n", ch); + SendMsgToChar(" - .\r\n", ch); show_main(ch); return; } auto i = olc_set.activ_list.find(num); if (i != olc_set.activ_list.end()) { - SendMsgToChar(ch, "В наборе уже есть активатор на %d %s.\r\n", + SendMsgToChar(ch, " %d %s.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } else { - SendMsgToChar(ch, "Активатор на %d %s добавлен в набор.\r\n", + SendMsgToChar(ch, " %d %s .\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); ActivNode node; // GCC 4.4 @@ -992,7 +992,7 @@ void sedit::parse_activ_add(CharData *ch, const char *arg) { void sedit::show_activ_ench_vnum(CharData *ch) { state = STATE_ACTIV_ENCH_VNUM; std::string out = main_menu_objlist(olc_set, 1); - out += "Укажите vnum предмета (0 - удалить и выйти, пустой ввод - выход) :"; + out += " vnum (0 - , - ) :"; SendMsgToChar(out, ch); } @@ -1028,12 +1028,12 @@ void sedit::parse_activ_ench_vnum(CharData *ch, const char *arg) { } else { rnum = GetObjRnum(vnum); if (rnum < 0) { - SendMsgToChar(ch, "Предметов с vnum %d не существует.\r\n", vnum); + SendMsgToChar(ch, " vnum %d .\r\n", vnum); show_activ_ench_vnum(ch); return; } else if (olc_set.obj_list.find(vnum) == olc_set.obj_list.end()) { SendMsgToChar(ch, - "В данном наборе нет предмета с vnum %d.\r\n", vnum); + " vnum %d.\r\n", vnum); show_activ_ench_vnum(ch); return; } @@ -1044,12 +1044,12 @@ void sedit::parse_activ_ench_vnum(CharData *ch, const char *arg) { if (rnum >= 0 && obj_proto[rnum]->get_type() == EObjType::kWeapon) { state = STATE_ACTIV_ENCH_NDICE; - SendMsgToChar("Укажите изменение бросков кубика (0 - без изменений) :", ch); + SendMsgToChar(" (0 - ) :", ch); } else { olc_set.activ_list.at(activ_edit).enchant.second.ndice = 0; olc_set.activ_list.at(activ_edit).enchant.second.sdice = 0; state = STATE_ACTIV_ENCH_WEIGHT; - SendMsgToChar("Укажите прибавляемый вес (0 - без изменений) :", ch); + SendMsgToChar(" (0 - ) :", ch); } } @@ -1071,7 +1071,7 @@ void sedit::parse_activ_ench_ndice(CharData *ch, const char *arg) { std::max(std::min(ndice, 100), -100); state = STATE_ACTIV_ENCH_SDICE; - SendMsgToChar("Укажите изменение граней кубиков (0 - без изменений) :", ch); + SendMsgToChar(" (0 - ) :", ch); } void sedit::parse_activ_ench_sdice(CharData *ch, const char *arg) { @@ -1082,14 +1082,14 @@ void sedit::parse_activ_ench_sdice(CharData *ch, const char *arg) { std::max(std::min(sdice, 100), -100); state = STATE_ACTIV_ENCH_WEIGHT; - SendMsgToChar("Укажите прибавляемый вес (0 - без изменений) :", ch); + SendMsgToChar(" (0 - ) :", ch); } void sedit::parse_obj_add(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar("Некорректный виртуальный номер предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_main(ch); return; } @@ -1101,16 +1101,16 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { const int rnum = GetObjRnum(vnum); if (olc_set.obj_list.size() >= MAX_OBJ_LIST) { SendMsgToChar( - "Набор уже содержит максимальное кол-во предметов.\r\n", ch); + " - .\r\n", ch); } else if (rnum < 0) { - SendMsgToChar(ch, "Предметов с vnum %d не существует.\r\n", vnum); + SendMsgToChar(ch, " vnum %d .\r\n", vnum); } else if (is_duplicate(olc_set.uid, vnum)) { SendMsgToChar(ch, - "Предмет '%s' уже является частью другого набора.\r\n", + " '%s' .\r\n", obj_proto[rnum]->get_short_description().c_str()); } else if (!verify_wear_flag(obj_proto[rnum])) { SendMsgToChar(ch, - "Предмет '%s' имеет запрещенный слот для надевания.\r\n", + " '%s' .\r\n", obj_proto[rnum]->get_short_description().c_str()); } else { SetMsgNode empty_msg; @@ -1118,7 +1118,7 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { //olc_set.obj_list.emplace(vnum, empty_msg); olc_set.obj_list.insert(std::make_pair(vnum, empty_msg)); SendMsgToChar(ch, - "Предмет '%s' добавлен в набор.\r\n", + " '%s' .\r\n", obj_proto[rnum]->get_short_description().c_str()); } } @@ -1129,7 +1129,7 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { void sedit::parse_obj_edit(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar(" !\r\n", ch); show_obj_edit(ch); return; } @@ -1140,7 +1140,7 @@ void sedit::parse_obj_edit(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: show_main(ch); break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_obj_edit(ch); break; } @@ -1149,27 +1149,27 @@ void sedit::parse_obj_edit(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case 1: SendMsgToChar("Подтвердите удаление предмета Y(Д)/N(Н) :", ch); + case 1: SendMsgToChar(" Y()/N() :", ch); state = STATE_OBJ_REMOVE; break; - case 2: SendMsgToChar("Виртуальный номер нового предмета : ", ch); + case 2: SendMsgToChar(" : ", ch); state = STATE_OBJ_CHANGE; break; - case 3: SendMsgToChar("Сообщение персонажу при активации : ", ch); + case 3: SendMsgToChar(" : ", ch); state = STATE_OBJMSG_CHAR_ON; break; - case 4: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); + case 4: SendMsgToChar(" : ", ch); state = STATE_OBJMSG_CHAR_OFF; break; - case 5: SendMsgToChar("Сообщение в комнату при активации : ", ch); + case 5: SendMsgToChar(" : ", ch); state = STATE_OBJMSG_ROOM_ON; break; - case 6: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); + case 6: SendMsgToChar(" : ", ch); state = STATE_OBJMSG_ROOM_OFF; break; case 7: show_main(ch); break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_obj_edit(ch); break; } @@ -1204,7 +1204,7 @@ void sedit::show_activ_skill(CharData *ch) { } SendMsgToChar(out, ch); SendMsgToChar( - "\r\nУкажите номер и уровень владения умением (0 - конец) : ", ch); + "\r\n (0 - ) : ", ch); } void sedit::show_activ_prof(CharData *ch) { @@ -1221,17 +1221,17 @@ void sedit::show_activ_prof(CharData *ch) { } snprintf(buf_, sizeof(buf_), - "%s%2llu%s) Сбросить все\r\n" - "%s%2llu%s) Установить все\r\n", + "%s%2llu%s) \r\n" + "%s%2llu%s) \r\n", kColorGrn, static_cast(bits.size() + 1), kColorNrm, kColorGrn, static_cast(bits.size() + 2), kColorNrm); out += buf_; - snprintf(buf_, sizeof(buf_), "Текущие профессии : %s", kColorCyn); + snprintf(buf_, sizeof(buf_), " : %s", kColorCyn); out += buf_; PrinSetClasses(bits, out, true); out += kColorNrm; - out += "\r\nВыберите профессии для данного активатора (0 - выход) : "; + out += "\r\n (0 - ) : "; SendMsgToChar(out, ch); } @@ -1239,7 +1239,7 @@ void sedit::show_activ_prof(CharData *ch) { void sedit::parse_activ_prof(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar("Некорректный ввод.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_activ_prof(ch); return; } @@ -1258,7 +1258,7 @@ void sedit::parse_activ_prof(CharData *ch, const char *arg) { } else if (num == bits.size() + 2) { bits.set(); } else { - SendMsgToChar("Некорректный ввод.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } show_activ_prof(ch); } @@ -1276,13 +1276,13 @@ void sedit::parse_activ_skill(CharData *ch, const char *arg) { } if (sscanf(arg, "%d %d", &ssnum, &ssval) < 2) { - SendMsgToChar("Не указан уровень владения умением.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_activ_skill(ch); return; } auto skill_id = static_cast(ssnum); if (MUD::Skills().IsInvalid(skill_id)) { - SendMsgToChar("Неизвестное умение.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_activ_skill(ch); } else if (ssval == 0) { skill.first = ESkill::kUndefined; @@ -1325,7 +1325,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { auto i = olc_set.activ_list.find(activ_edit); if (i == olc_set.activ_list.end()) { SendMsgToChar(ch, - "Ошибка: активатор не найден %s:%d (%s).\r\n", + ": %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; @@ -1334,7 +1334,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar(" !\r\n", ch); show_activ_edit(ch); return; } @@ -1345,7 +1345,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: show_main(ch); break; - default: SendMsgToChar("Неверный выбор!\r\n", ch); + default: SendMsgToChar(" !\r\n", ch); show_activ_edit(ch); break; } @@ -1355,10 +1355,10 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { case 1: state = STATE_ACTIV_REMOVE; - SendMsgToChar("Подтвердите удаление активатора Y(Д)/N(Н) :", ch); + SendMsgToChar(" Y()/N() :", ch); return; case 2: state = STATE_ACTIV_CHANGE; - SendMsgToChar("Введите новое кол-во предметов для активации :", ch); + SendMsgToChar(" - :", ch); return; case 3: show_activ_prof(ch); return; @@ -1377,23 +1377,23 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { show_activ_ench_vnum(ch); } else if (num == offset + activ.apply.size() + 3) { state = STATE_ACTIV_PHYS_DMG; - SendMsgToChar("Укажите процент прибавляемого физ. урона :", ch); + SendMsgToChar(" . :", ch); } else if (num == offset + activ.apply.size() + 4) { state = STATE_ACTIV_MAGE_DMG; - SendMsgToChar("Укажите процент прибавляемого маг. урона :", ch); + SendMsgToChar(" . :", ch); } else if (num == offset + activ.apply.size() + 5) { if (activ.npc) { - SendMsgToChar("Теперь НЕ активится на мобах.", ch); + SendMsgToChar(" .", ch); i->second.npc = false; } else { - SendMsgToChar("Теперь активится на мобах.", ch); + SendMsgToChar(" .", ch); i->second.npc = true; } show_activ_edit(ch); } else if (num == offset + activ.apply.size() + 6) { show_main(ch); } else { - SendMsgToChar("Некорректный ввод.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_activ_edit(ch); } } @@ -1426,12 +1426,12 @@ void sedit::parse_activ_apply_loc(CharData *ch, const char *arg) { apply.modifier = 0; show_activ_edit(ch); } else if (num < 0 || num >= EApply::kNumberApplies) { - SendMsgToChar("Некорректный ввод.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_apply_olc(ch->desc); } else { apply.location = static_cast(num); state = STATE_ACTIV_APPLY_MOD; - SendMsgToChar("Введите модификатор : ", ch); + SendMsgToChar(" : ", ch); } } @@ -1455,14 +1455,14 @@ void sedit::parse_activ_apply_mod(CharData *ch, const char *arg) { void sedit::parse_obj_change(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar("Некорректный виртуальный номер предмета.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_obj_edit(ch); return; } const int vnum = atoi(arg); if (vnum == obj_edit) { - SendMsgToChar("Вы и так редактируете данный предмет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); show_obj_edit(ch); return; } @@ -1472,9 +1472,9 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { ? MISSING_OBJECT_NAME : obj_proto[rnum]->get_short_description().c_str(); if (is_duplicate(olc_set.uid, vnum)) { - SendMsgToChar(ch, "Предмет '%s' уже является частью другого набора.\r\n", name); + SendMsgToChar(ch, " '%s' .\r\n", name); } else if (olc_set.obj_list.find(vnum) != olc_set.obj_list.end()) { - SendMsgToChar(ch, "Предмет '%s' уже является частью данного набора.\r\n", name); + SendMsgToChar(ch, " '%s' .\r\n", name); } else { SetMsgNode msg; auto i = olc_set.obj_list.find(obj_edit); @@ -1485,7 +1485,7 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { obj_edit = vnum; olc_set.obj_list.insert(std::make_pair(vnum, msg)); - SendMsgToChar(ch, "Предмет '%s' добавлен в набор.\r\n", name); + SendMsgToChar(ch, " '%s' .\r\n", name); } show_obj_edit(ch); @@ -1494,17 +1494,17 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { void sedit::parse_activ_change(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar("Некорректное кол-во предметов для активации.\r\n", ch); + SendMsgToChar(" - .\r\n", ch); show_activ_edit(ch); return; } const int num = atoi(arg); if (num == activ_edit) { - SendMsgToChar("Кол-во предметов активации не изменилось.\r\n", ch); + SendMsgToChar("- .\r\n", ch); } else if (olc_set.activ_list.find(num) != olc_set.activ_list.end()) { SendMsgToChar(ch, - "Набор уже содержит активатор на %d %s.\r\n", + " %d %s.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } else { ActivNode activ; @@ -1517,15 +1517,15 @@ void sedit::parse_activ_change(CharData *ch, const char *arg) { // GCC 4.4 //olc_set.activ_list.emplace(num, activ); olc_set.activ_list.insert(std::make_pair(num, activ)); - SendMsgToChar(ch, "Активатор на %d %s добавлен в набор.\r\n", + SendMsgToChar(ch, " %d %s .\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } show_activ_edit(ch); } -/// наружу при обработке команд в олц торчит только эта функция -/// плюс она обернута в try/catch для ловли возможных out_of_range, т.к. внутри -/// все через .at и местами код на это закладывается вместо ручных сравнений +/// +/// try/catch out_of_range, .. +/// .at void parse_input(CharData *ch, const char *arg) { sedit &olc = *(ch->desc->sedit); @@ -1602,7 +1602,7 @@ void parse_input(CharData *ch, const char *arg) { default: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar(ch, - "Ошибка: не найден STATE, редактирование отменено %s:%d (%s).\r\n", + ": STATE, %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); break; } // switch @@ -1626,15 +1626,15 @@ void start_sedit(CharData *ch, size_t idx) { } const char *SEDIT_HELP = - "Формат команды:\r\n" - " sedit - создание нового сета\r\n" - " sedit <прядковый номер сета из slist> - редактирование существующего сета\r\n" - " sedit - редактирование существующего сета\r\n" - " sedit - редактирование глобальных сообщений сетов\r\n"; + " :\r\n" + " sedit - \r\n" + " sedit < slist> - \r\n" + " sedit - \r\n" + " sedit - \r\n"; } // namespace -/// иммский sedit, см. SEDIT_HELP +/// sedit, . SEDIT_HELP void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) { return; @@ -1642,20 +1642,20 @@ void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { skip_spaces(&argument); if (!argument || !*argument) { - // создание нового сета + // start_sedit(ch, -1); } else if (a_isdigit(*argument)) { - // редактирование существующего + // unsigned num = atoi(argument); if (num > 0 && num <= sets_list.size()) { - // по номеру сета из slist + // slist --num; start_sedit(ch, num); } else { - // по внуму предмета + // if (GetObjRnum(num) < 0) { SendMsgToChar(SEDIT_HELP, ch); - SendMsgToChar(ch, "Предметов с vnum %s не существует.\r\n", + SendMsgToChar(ch, " vnum %s .\r\n", argument); return; } @@ -1665,11 +1665,11 @@ void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else { SendMsgToChar(SEDIT_HELP, ch); SendMsgToChar(ch, - "В сетах предметов с vnum %s не найдено.\r\n", argument); + " vnum %s .\r\n", argument); } } } else if (!str_cmp(argument, "msg_set") || !str_cmp(argument, "messages")) { - // редактирование глобальных сообщений + // ch->desc->state = EConState::kSedit; ch->desc->sedit = std::make_shared(); ch->desc->sedit->msg_edit = global_msg; diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 8140a442d..11c9547fa 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -59,22 +59,22 @@ namespace sight { #define TAG_AUTUMNDAY "" #define MAX_FIRES 6 -const char *Fires[MAX_FIRES] = {"тлеет небольшая кучка угольков", - "тлеет небольшая кучка угольков", - "еле-еле теплится огонек", - "догорает небольшой костер", - "весело трещит костер", - "ярко пылает костер" +const char *Fires[MAX_FIRES] = {" ", + " ", + "- ", + " ", + " ", + " " }; -const char *ObjState[8][2] = {{"рассыпается", "рассыпается"}, - {"плачевно", "в плачевном состоянии"}, - {"плохо", "в плохом состоянии"}, - {"неплохо", "в неплохом состоянии"}, - {"средне", "в рабочем состоянии"}, - {"хорошо", "в хорошем состоянии"}, - {"очень хорошо", "в очень хорошем состоянии"}, - {"великолепно", "в великолепном состоянии"} +const char *ObjState[8][2] = {{"", ""}, + {"", " "}, + {"", " "}, + {"", " "}, + {"", " "}, + {"", " "}, + {" ", " "}, + {"", " "} }; void list_obj_to_char(const ObjData::obj_list_t &list, CharData *ch, int mode, int show); @@ -114,25 +114,25 @@ std::string BuildCompactShieldSuffix(const CharData *viewer, const CharData *tar }; if (AFF_FLAGGED(target, EAffect::kFireShield)) { - append("&RОЩ&R"); + append("&R&R"); } if (AFF_FLAGGED(target, EAffect::kAirShield)) { - append("&WВЩ&R"); + append("&W&R"); } if (AFF_FLAGGED(target, EAffect::kIceShield)) { - append("&CЛЩ&R"); + append("&C&R"); } if (AFF_FLAGGED(viewer, EAffect::kDetectMagic) && AFF_FLAGGED(target, EAffect::kMagicGlass)) { - append("&wМЗ&R"); + append("&w&R"); } if (mode == EBriefShieldsMode::kCompressed) { if (AFF_FLAGGED(target, EAffect::kSanctuary)) { - append("&YО&R"); + append("&Y&R"); } else if (AFF_FLAGGED(target, EAffect::kPrismaticAura)) { - append("&MП&R"); + append("&M&R"); } if (AFF_FLAGGED(viewer, EAffect::kDetectMagic) && AFF_FLAGGED(target, EAffect::kBrokenChains)) { - append("&CРО&R"); + append("&C&R"); } } @@ -160,11 +160,11 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { if (!ch->desc) return; if (is_dark(ch->in_room) && !CanSeeInDark(ch) && !CanUseFeat(ch, EFeat::kDarkReading)) { - SendMsgToChar("Слишком темно...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); show_glow_objs(ch); return; } else if (AFF_FLAGGED(ch, EAffect::kBlind)) { - SendMsgToChar("Вы все еще слепы...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); return; } else if (ch->GetPosition() < EPosition::kSleep) { return; @@ -183,7 +183,7 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { SendMsgToChar(kColorBoldCyn, ch); if (!ch->IsNpc() && (ch->IsFlagged(EPrf::kRoomFlags) || InTestZone(ch))) { - // иммам рандомная * во флагах ломает мапер грят + // * const bool has_flag = ROOM_FLAGGED(ch->in_room, ERoomFlag::kBfsMark) ? true : false; world[ch->in_room]->unset_flag(ERoomFlag::kBfsMark); @@ -213,7 +213,7 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { SendMsgToChar("\r\n", ch); if (is_dark(ch->in_room) && !ch->IsFlagged(EPrf::kHolylight)) { - SendMsgToChar("Слишком темно...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); } else if ((!ch->IsNpc() && !ch->IsFlagged(EPrf::kBrief)) || ignore_brief || ROOM_FLAGGED(ch->in_room, ERoomFlag::kDeathTrap)) { show_extend_room(GlobalObjects::descriptions().get(world[ch->in_room]->description_num).c_str(), ch); } @@ -222,14 +222,14 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { do_auto_exits(ch); } - // Отображаем аффекты комнаты. После автовыходов чтобы не ломать популярный маппер. - // Описания живут в spell_msg.xml под ключами kRoomAffect{Visible,Invisible,SelfInvisible} - // (issue.sight-fmt); show_room_affects выбирает нужный по флагам ch. + // . . + // spell_msg.xml kRoomAffect{Visible,Invisible,SelfInvisible} + // (issue.sight-fmt); show_room_affects ch. show_room_affects(ch); // now list characters & objects if (world[ch->in_room]->fires) { - sprintf(buf, "%sВ центре %s.%s\r\n", kColorRed, Fires[MIN(world[ch->in_room]->fires, MAX_FIRES - 1)], kColorNrm); + sprintf(buf, "%s %s.%s\r\n", kColorRed, Fires[MIN(world[ch->in_room]->fires, MAX_FIRES - 1)], kColorNrm); SendMsgToChar(buf, ch); } // kPortalTimer rendering (regular + PK variant + immortal/tester timer suffix) @@ -238,8 +238,8 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { if (world[ch->in_room]->holes) { const int ar = round_up(world[ch->in_room]->holes / kHolesTime); - sprintf(buf, "%sЗдесь выкопана ямка глубиной примерно в %i аршин%s.%s\r\n", - kColorYel, ar, (ar == 1 ? "" : (ar < 5 ? "а" : "ов")), (kColorNrm)); + sprintf(buf, "%s %i %s.%s\r\n", + kColorYel, ar, (ar == 1 ? "" : (ar < 5 ? "" : "")), (kColorNrm)); SendMsgToChar(buf, ch); } @@ -251,29 +251,29 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { case ESector::kForestSnow: case ESector::kHillsSnow: case ESector::kMountainSnow: - sprintf(buf, "%sСнежный ковер лежит у вас под ногами.%s\r\n", + sprintf(buf, "%s .%s\r\n", kColorWht, kColorNrm); break; case ESector::kFieldRain: case ESector::kForestRain: case ESector::kHillsRain: sprintf(buf, - "%sВы просто увязаете в грязи.%s\r\n", + "%s .%s\r\n", kColorBoldBlk, kColorNrm); break; case ESector::kThickIce: sprintf(buf, - "%sУ вас под ногами толстый лед.%s\r\n", + "%s .%s\r\n", kColorBoldBlu, kColorNrm); break; case ESector::kNormalIce: - sprintf(buf, "%sУ вас под ногами достаточно толстый лед.%s\r\n", + sprintf(buf, "%s .%s\r\n", kColorBoldBlu, kColorNrm); break; case ESector::kThinIce: - sprintf(buf, "%sТоненький ледок вот-вот проломится под вами.%s\r\n", + sprintf(buf, "%s - .%s\r\n", kColorBoldCyn, kColorNrm); break; }; @@ -285,12 +285,12 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { // issue.runestones phase 3: the runestone now shows itself as a physical object in the room // contents below (no registry-driven room-presence hack needed). list_obj_to_char(world[ch->in_room]->contents, ch, 0, false); - list_char_to_char_thing(world[ch->in_room]->people, ch); //добавим отдельный вызов если моб типа предмет выводим желтым + list_char_to_char_thing(world[ch->in_room]->people, ch); // SendMsgToChar("&R&q", ch); list_char_to_char(world[ch->in_room]->people, ch); SendMsgToChar("&Q&n", ch); - // вход в новую зону + // if (!ch->IsNpc()) { ZoneRnum inroom = world[ch->in_room]->zone_rn; if (zone_table[world[ch->get_from_room()]->zone_rn].vnum != zone_table[inroom].vnum) { @@ -303,8 +303,8 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { } if (zone_table[world[ch->get_from_room()]->zone_rn].vnum >= dungeons::kZoneStartDungeons && zone_table[inroom].vnum < dungeons::kZoneStartDungeons) { - SendMsgToChar("&GВы покинули зазеркалье.\r\n&n", ch); -// dungeons::SwapObjectDungeon(ch); перенесено на закрытие данжа + SendMsgToChar("&G .\r\n&n", ch); +// dungeons::SwapObjectDungeon(ch); } } } @@ -323,8 +323,8 @@ void show_glow_objs(CharData *ch) { if (!cnt) return; const char *str = cnt > 1 ? - "Вы видите очертания каких-то блестящих предметов.\r\n" : - "Вы видите очертания какого-то блестящего предмета.\r\n"; + " - .\r\n" : + " - .\r\n"; SendMsgToChar(str, ch); } @@ -394,7 +394,7 @@ void show_extend_room(const char *const description, CharData *ch) { * * Thanks to Angus Mezick for the * suggested fix to this problem. - * \return флаг если смотрим в клан-сундук, чтобы после осмотра не смотреть второй раз по look_in_obj + * \return -, look_in_obj */ bool look_at_target(CharData *ch, char *arg, int subcmd) { int bits, found = false, fnum, i = 0; @@ -409,18 +409,18 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { } if (!*arg) { - SendMsgToChar("На что вы так мечтаете посмотреть?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return false; } half_chop(arg, whatp, where); what = whatp; - if (isname(where, "земля комната room ground")) + if (isname(where, " room ground")) where_bits = EFind::kObjRoom | EFind::kCharInRoom; - else if (isname(where, "инвентарь inventory")) + else if (isname(where, " inventory")) where_bits = EFind::kObjInventory; - else if (isname(where, "экипировка equipment")) + else if (isname(where, " equipment")) where_bits = EFind::kObjEquip; bits = generic_find(what, where_bits, ch, &found_char, &found_obj); @@ -440,27 +440,27 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return false; } if (CanSee(found_char, ch)) - act("$n оглядел$g вас с головы до пят.", true, ch, nullptr, found_char, kToVict); - act("$n посмотрел$g на $N3.", true, ch, nullptr, found_char, kToNotVict); + act("$n $g .", true, ch, nullptr, found_char, kToVict); + act("$n $g $N3.", true, ch, nullptr, found_char, kToNotVict); } return false; } // Strip off "number." from 2.foo and friends. if (!(fnum = get_number(&what))) { - SendMsgToChar("Что осматриваем?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); return false; } - // заглянуть в пентаграмму + // i = 0; - if (isname(whatp, "пентаграмма") && IS_SET(where_bits, EFind::kObjRoom)) { + if (isname(whatp, "") && IS_SET(where_bits, EFind::kObjRoom)) { RoomRnum to_room = kNowhere; bool one_way = false; for (const auto &aff : world[ch->in_room]->affected) { if (room_spells::IsPortalAffect(aff->affect_type)) { if (aff->affect_type == room_spells::ERoomAffect::kNoPortalExit) { - SendMsgToChar("Похоже, этого здесь нет!\r\n", ch); + SendMsgToChar(", !\r\n", ch); return false; } if (++i == fnum) { @@ -477,10 +477,10 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { } if (to_room != kNowhere) { const auto r = ch->in_room; - SendMsgToChar("Приблизившись к пентаграмме, вы осторожно заглянули в нее.\r\n\r\n", ch); - act("$n0 осторожно заглянул$g в пентаграмму.\r\n", true, ch, nullptr, nullptr, kToRoom); + SendMsgToChar(" , .\r\n\r\n", ch); + act("$n0 $g .\r\n", true, ch, nullptr, nullptr, kToRoom); if (!one_way) { - SendMsgToChar("Яркий свет, идущий с противоположного конца прохода, застилает вам глаза.\r\n\r\n", ch); + SendMsgToChar(" , , .\r\n\r\n", ch); return false; } ch->in_room = to_room; @@ -496,8 +496,8 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return false; } - // Совпадает ли аргумент с ключевыми словами какой-нибудь двери в комнате? - // Тогда покажем описание этого направления (#3209). + // - ? + // (#3209). for (int d = 0; d < EDirection::kMaxDirNum; ++d) { const auto &exit = world[ch->in_room]->dir_option[d]; if (!exit || !exit->keyword || !*exit->keyword) { @@ -509,7 +509,7 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { if (!exit->general_description.empty()) { SendMsgToChar(exit->general_description.c_str(), ch); } else { - SendMsgToChar("Вы не видите ничего особенного.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } return false; } @@ -528,8 +528,8 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return true; } - // Собственно изменение. Вместо проверки "if (!found)" юзается проверка - // наличия описания у объекта, найденного функцией "generic_find" + // . "if (!found)" + // , "generic_find" if (!(desc = find_exdesc(what, found_obj->get_ex_description())) || ((found_obj->get_type() == EObjType::kNote) && !found_obj->get_action_description().empty())) { show_obj_to_char(found_obj, ch, 5, true, 1); // Show no-description @@ -542,7 +542,7 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { obj_info(ch, found_obj, buf); SendMsgToChar(buf, ch); } else - SendMsgToChar("Похоже, этого здесь нет!\r\n", ch); + SendMsgToChar(", !\r\n", ch); return false; } @@ -560,100 +560,100 @@ void look_at_char(CharData *i, CharData *ch) { else SendMsgToChar(ch, "*\r\n%s*\r\n", AddLeadingStringSpace(i->player_data.description).c_str()); } else if (!i->IsNpc()) { - strcpy(buf, "\r\nЭто"); + strcpy(buf, "\r\n"); if (IsFemale(i)) { if (GET_HEIGHT(i) <= 151) { if (GET_WEIGHT(i) >= 140) - strcat(buf, " маленькая плотная дамочка.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 125) - strcat(buf, " маленькая женщина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " миниатюрная дамочка.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 159) { if (GET_WEIGHT(i) >= 145) - strcat(buf, " невысокая плотная мадам.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 130) - strcat(buf, " невысокая женщина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " изящная леди.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 165) { if (GET_WEIGHT(i) >= 145) - strcat(buf, " среднего роста женщина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " среднего роста изящная красавица.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 175) { if (GET_WEIGHT(i) >= 150) - strcat(buf, " высокая дородная баба.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 135) - strcat(buf, " высокая стройная женщина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " высокая изящная женщина.\r\n"); + strcat(buf, " .\r\n"); } else { if (GET_WEIGHT(i) >= 155) - strcat(buf, " очень высокая крупная дама.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 140) - strcat(buf, " очень высокая стройная женщина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " очень высокая худощавая женщина.\r\n"); + strcat(buf, " .\r\n"); } } else { if (GET_HEIGHT(i) <= 165) { if (GET_WEIGHT(i) >= 170) - strcat(buf, " маленький, похожий на колобок, мужчина.\r\n"); + strcat(buf, " , , .\r\n"); else if (GET_WEIGHT(i) >= 150) - strcat(buf, " маленький плотный мужчина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " маленький плюгавенький мужичонка.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 175) { if (GET_WEIGHT(i) >= 175) - strcat(buf, " невысокий коренастый крепыш.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 160) - strcat(buf, " невысокий крепкий мужчина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " невысокий худощавый мужчина.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 185) { if (GET_WEIGHT(i) >= 180) - strcat(buf, " среднего роста коренастый мужчина.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 165) - strcat(buf, " среднего роста крепкий мужчина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " среднего роста худощавый мужчина.\r\n"); + strcat(buf, " .\r\n"); } else if (GET_HEIGHT(i) <= 195) { if (GET_WEIGHT(i) >= 185) - strcat(buf, " высокий крупный мужчина.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 170) - strcat(buf, " высокий стройный мужчина.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, " длинный, худощавый мужчина.\r\n"); + strcat(buf, " , .\r\n"); } else { if (GET_WEIGHT(i) >= 190) - strcat(buf, " огромный мужик.\r\n"); + strcat(buf, " .\r\n"); else if (GET_WEIGHT(i) >= 180) - strcat(buf, " очень высокий, крупный амбал.\r\n"); + strcat(buf, " , .\r\n"); else - strcat(buf, " длиннющий, похожий на жердь мужчина.\r\n"); + strcat(buf, " , .\r\n"); } } SendMsgToChar(buf, ch); } else - act("\r\nНичего необычного в $n5 вы не заметили.", false, i, nullptr, ch, kToVict); + act("\r\n $n5 .", false, i, nullptr, ch, kToVict); if (AFF_FLAGGED(i, EAffect::kHaemorrhage)) { - act("$n покрыт$a кровоточащими ранами!", false, i, nullptr, ch, kToVict); + act("$n $a !", false, i, nullptr, ch, kToVict); } if (AFF_FLAGGED(i, EAffect::kLacerations)) { - act("$n покрыт$a рваными ранами!", false, i, nullptr, ch, kToVict); + act("$n $a !", false, i, nullptr, ch, kToVict); } if (AFF_FLAGGED(i, EAffect::kCharmed) && i->get_master() == ch) { if (IsCharmExpiring(i)) { - act("$n скоро перестанет следовать за вами.", false, i, nullptr, ch, kToVict); + act("$n .", false, i, nullptr, ch, kToVict); } else { for (const auto &aff : i->affected) { if (IS_SET(aff->battleflag, kAfCharmBond)) { sprintf(buf, - IsPoly(i) ? "$n будут слушаться вас еще %d %s." : "$n будет слушаться вас еще %d %s.", + IsPoly(i) ? "$n %d %s." : "$n %d %s.", aff->duration/2, grammar::GetDeclensionInNumber(aff->duration/2, grammar::EWhat::kHour)); act(buf, false, i, nullptr, ch, kToVict); @@ -665,15 +665,15 @@ void look_at_char(CharData *i, CharData *ch) { if (mount::IsHorse(i) && i->get_master() == ch) { - strcpy(buf, "\r\nЭто ваш скакун. Он "); + strcpy(buf, "\r\n . "); if (GET_HORSESTATE(i) <= 0) - strcat(buf, "загнан.\r\n"); + strcat(buf, ".\r\n"); else if (GET_HORSESTATE(i) <= 20) - strcat(buf, "весь в мыле.\r\n"); + strcat(buf, " .\r\n"); else if (GET_HORSESTATE(i) <= 80) - strcat(buf, "в хорошем состоянии.\r\n"); + strcat(buf, " .\r\n"); else - strcat(buf, "выглядит совсем свежим.\r\n"); + strcat(buf, " .\r\n"); SendMsgToChar(buf, ch); }; @@ -686,7 +686,7 @@ void look_at_char(CharData *i, CharData *ch) { } if (found) { SendMsgToChar("\r\n", ch); - act("$n одет$a :", false, i, nullptr, ch, kToVict); + act("$n $a :", false, i, nullptr, ch, kToVict); for (j = 0; j < EEquipPos::kNumEquipPos; j++) { if (GET_EQ(i, j) && CanSeeObj(ch, GET_EQ(i, j))) { SendMsgToChar(where[j], ch); @@ -702,7 +702,7 @@ void look_at_char(CharData *i, CharData *ch) { if (ch != i && (GetSkill(ch, ESkill::kPry) || privilege::IsImmortal(ch))) { found = false; - act("\r\nВы попытались заглянуть в $s ношу:", false, i, nullptr, ch, kToVict); + act("\r\n $s :", false, i, nullptr, ch, kToVict); for (tmp_obj = i->carrying; tmp_obj; tmp_obj = tmp_obj->get_next_content()) { if (CanSeeObj(ch, tmp_obj) && (number(0, 30) < GetRealLevel(ch))) { if (!push) { @@ -721,7 +721,7 @@ void look_at_char(CharData *i, CharData *ch) { if (push && push_count) show_obj_to_char(push, ch, 1, ch == i, push_count); if (!found) - SendMsgToChar("...и ничего не обнаружили.\r\n", ch); + SendMsgToChar("... .\r\n", ch); } } @@ -777,7 +777,7 @@ void list_char_to_char_thing(const RoomData::people_t &list, CharData *ch) { // // kPortalTimer adds one extra debug line for immortals/testers: // "[timer: into: ]", echoing the immortals-only -// "(время: %d, куда: %d)" suffix that used to live in look_at_room. +// "(: %d, : %d)" suffix that used to live in look_at_room. void show_room_affects(CharData *ch) { std::ostringstream buffer; @@ -848,7 +848,7 @@ void show_room_affects(CharData *ch) { } buffer << "\r\n"; // kPortalTimer debug suffix: timer (room-tick pulses) + destination vnum. - // Replaces the immortals-only "(время: %d, куда: %d)" suffix that used to + // Replaces the immortals-only "(: %d, : %d)" suffix that used to // be inlined into the main description in look_at_room. if (room_spells::IsPortalAffect(af->affect_type) && is_immortal_or_tester) { char debug_buf[128]; @@ -910,11 +910,11 @@ void do_auto_exits(CharData *ch) { *buf = '\0'; for (door = 0; door < EDirection::kMaxDirNum; door++) { - // Наконец-то добавлена отрисовка в автовыходах закрытых дверей + // - if (EXIT(ch, door) && EXIT(ch, door)->to_room() != kNowhere) { if (EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kClosed)) { - // Печатаем первую БУКВУ направления: под UTF-8 у русской буквы два байта, и %c - // выводил половину (issue #3681). + // : UTF-8 , %c + // (issue #3681). slen += sprintf(buf + slen, "("); slen += static_cast(native_text::copy_lower_char(dirs[door], buf + slen)); slen += sprintf(buf + slen, ") "); @@ -977,11 +977,11 @@ void list_obj_to_char(ObjData *list, CharData *ch, int mode, int show) { } if (!found && show) { if (show == 1) - SendMsgToChar(" Внутри ничего нет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else if (show == 2) - SendMsgToChar(" Вы ничего не несете.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else if (show == 3) { - SendMsgToChar(" Пусто...\r\n", ch); + SendMsgToChar(" ...\r\n", ch); return; } } @@ -1048,7 +1048,7 @@ void list_char_to_char(const RoomData::people_t &list, CharData *ch) { && i->in_room == ch->in_room && !CanSeeInDark(ch) && AFF_FLAGGED(i, EAffect::kInfravision)) { - SendMsgToChar("Пара светящихся глаз смотрит на вас.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } } @@ -1108,20 +1108,20 @@ void look_in_direction(CharData *ch, int dir, int info_is) { count += sprintf(buf, "%s%s:%s ", kColorYel, dirs_rus[dir], kColorNrm); if (EXIT_FLAGGED(rdata, EExitFlag::kClosed)) { if (rdata->keyword) { - count += sprintf(buf + count, " закрыто (%s).\r\n", rdata->keyword); + count += sprintf(buf + count, " (%s).\r\n", rdata->keyword); } else { - count += sprintf(buf + count, " закрыто (вероятно дверь).\r\n"); + count += sprintf(buf + count, " ( ).\r\n"); } const int skill_pick = GetSkill(ch, ESkill::kPickLock); if (EXIT_FLAGGED(rdata, EExitFlag::kLocked) && skill_pick) { if (EXIT_FLAGGED(rdata, EExitFlag::kPickroof)) { count += sprintf(buf + count - 2, - "%s вы никогда не сможете ЭТО взломать!%s\r\n", + "%s !%s\r\n", kColorBoldCyn, kColorNrm); } else if (EXIT_FLAGGED(rdata, EExitFlag::kBrokenLock)) { - count += sprintf(buf + count - 2, "%s Замок сломан... %s\r\n", kColorRed, kColorNrm); + count += sprintf(buf + count - 2, "%s ... %s\r\n", kColorRed, kColorNrm); } else { const PickProbabilityInformation &pbi = get_pick_probability(ch, rdata->lock_complexity); count += sprintf(buf + count - 2, "%s\r\n", pbi.text.c_str()); @@ -1135,7 +1135,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { } if (is_dark(rdata->to_room())) { - count += sprintf(buf + count, " слишком темно.\r\n"); + count += sprintf(buf + count, " .\r\n"); SendMsgToChar(buf, ch); if (info_is & EXIT_SHOW_LOOKING) { SendMsgToChar("&R&q", ch); @@ -1146,7 +1146,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { TrainSkill(ch, ESkill::kLooking, probe >= percent, tch); if (HERE(tch) && InvisOk(ch, tch) && probe >= percent && (percent < 100 || privilege::IsImmortal(ch))) { - // Если моб не вещь и смотрящий не им + // if (GET_RACE(tch) != ENpcRace::kConstruct || privilege::IsImmortal(ch)) { ListOneChar(tch, ch, ESkill::kLooking); count++; @@ -1155,7 +1155,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { } if (!count) { - SendMsgToChar("Вы ничего не смогли разглядеть!\r\n", ch); + SendMsgToChar(" !\r\n", ch); } SendMsgToChar("&Q&n", ch); } @@ -1173,7 +1173,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { SendMsgToChar("&Q&n", ch); } } else if (info_is & EXIT_SHOW_WALL) - SendMsgToChar("И что вы там мечтаете увидеть?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); } bool quest_item(ObjData *obj) { @@ -1191,26 +1191,26 @@ void look_in_obj(CharData *ch, char *arg) { int where_bits = EFind::kObjInventory | EFind::kObjRoom | EFind::kObjEquip; if (!*arg) - SendMsgToChar("Смотреть во что?\r\n", ch); + SendMsgToChar(" ?\r\n", ch); else half_chop(arg, whatp, where); - if (isname(where, "земля комната room ground")) + if (isname(where, " room ground")) where_bits = EFind::kObjRoom; - else if (isname(where, "инвентарь inventory")) + else if (isname(where, " inventory")) where_bits = EFind::kObjInventory; - else if (isname(where, "экипировка equipment")) + else if (isname(where, " equipment")) where_bits = EFind::kObjEquip; bits = generic_find(arg, where_bits, ch, &dummy, &obj); if ((obj == nullptr) || !bits) { - sprintf(buf, "Вы не видите здесь '%s'.\r\n", arg); + sprintf(buf, " '%s'.\r\n", arg); SendMsgToChar(buf, ch); } else if (obj->get_type() != EObjType::kLiquidContainer && obj->get_type() != EObjType::kFountain && obj->get_type() != EObjType::kContainer) { - SendMsgToChar("Ничего в нем нет!\r\n", ch); + SendMsgToChar(" !\r\n", ch); } else { if (Clan::ChestShow(obj, ch)) { return; @@ -1225,17 +1225,17 @@ void look_in_obj(CharData *ch, char *arg) { if (obj->get_type() == EObjType::kContainer) { if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kShutted))) { - act("Закрыт$A.", false, ch, obj, nullptr, kToChar); + act("$A.", false, ch, obj, nullptr, kToChar); const int skill_pick = GetSkill(ch, ESkill::kPickLock); - int count = sprintf(buf, "Заперт%s.", grammar::ObjSexEnding((obj)->get_sex(), 6)); + int count = sprintf(buf, "%s.", grammar::ObjSexEnding((obj)->get_sex(), 6)); if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kLockedUp)) && skill_pick) { if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kUncrackable))) count += sprintf(buf + count, - "%s Вы никогда не сможете ЭТО взломать!%s\r\n", + "%s !%s\r\n", kColorBoldCyn, kColorNrm); else if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kLockIsBroken))) - count += sprintf(buf + count, "%s Замок сломан... %s\r\n", kColorRed, kColorNrm); + count += sprintf(buf + count, "%s ... %s\r\n", kColorRed, kColorNrm); else { const PickProbabilityInformation &pbi = get_pick_probability(ch, GET_OBJ_VAL(obj, 3)); count += sprintf(buf + count, "%s\r\n", pbi.text.c_str()); @@ -1245,25 +1245,25 @@ void look_in_obj(CharData *ch, char *arg) { } else { SendMsgToChar(OBJN(obj, ch, grammar::ECase::kNom), ch); switch (bits) { - case EFind::kObjInventory: SendMsgToChar("(в руках)\r\n", ch); + case EFind::kObjInventory: SendMsgToChar("( )\r\n", ch); break; - case EFind::kObjRoom: SendMsgToChar("(на земле)\r\n", ch); + case EFind::kObjRoom: SendMsgToChar("( )\r\n", ch); break; - case EFind::kObjEquip: SendMsgToChar("(в амуниции)\r\n", ch); + case EFind::kObjEquip: SendMsgToChar("( )\r\n", ch); break; - default: SendMsgToChar("(неведомо где)\r\n", ch); + default: SendMsgToChar("( )\r\n", ch); break; } if (!obj->get_contains()) - SendMsgToChar(" Внутри ничего нет.\r\n", ch); + SendMsgToChar(" .\r\n", ch); else { if (GET_OBJ_VAL(obj, 0) > 0 && bits != EFind::kObjRoom) { - /* amt - индекс массива из 6 элементов (0..5) с описанием наполненности - с помощью нехитрых мат. преобразований мы получаем соотношение веса и максимального объема контейнера, - выраженные числами от 0 до 5. (причем 5 будет лишь при полностью полном контейнере) + /* amt - 6 (0..5) + . , + 0 5. ( 5 ) */ amt = std::clamp((obj->get_weight() * 100) / (GET_OBJ_VAL(obj, 0) * 20), 0, 5); - sprintf(buf, "Заполнен%s содержимым %s:\r\n", grammar::ObjSexEnding((obj)->get_sex(), 6), fullness[amt]); + sprintf(buf, "%s %s:\r\n", grammar::ObjSexEnding((obj)->get_sex(), 6), fullness[amt]); SendMsgToChar(buf, ch); } list_obj_to_char(obj->get_contains(), ch, 1, bits != EFind::kObjRoom); @@ -1287,7 +1287,7 @@ void skip_hide_on_look(CharData *ch) { } } -// mode 1 show_state 3 для хранилище (4 - хранилище ингров) +// mode 1 show_state 3 (4 - ) const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_state, int how) { *buf = '\0'; if ((mode < 5) && (ch->IsFlagged(EPrf::kRoomFlags) || InTestZone(ch))) @@ -1303,24 +1303,24 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s } else if (mode == 5) { if (object->get_type() == EObjType::kNote) { if (!object->get_action_description().empty()) { - strcpy(buf, "Вы прочитали следующее :\r\n\r\n"); + strcpy(buf, " :\r\n\r\n"); strcat(buf, AddLeadingStringSpace(object->get_action_description()).c_str()); strcat(buf, "\r\n"); page_string(ch->desc, buf, 1); } else { - SendMsgToChar("Чисто.\r\n", ch); + SendMsgToChar(".\r\n", ch); } return nullptr; } else if (object->get_type() == EObjType::kBandage) { - strcpy(buf, "Бинты для перевязки ран ('перевязать').\r\n"); - snprintf(buf2, kMaxStringLength, "Осталось применений: %d, восстановление: %d", + strcpy(buf, " ('').\r\n"); + snprintf(buf2, kMaxStringLength, " : %d, : %d", object->get_weight(), GET_OBJ_VAL(object, 0) * 10); strcat(buf, buf2); } else if (object->get_type() != EObjType::kLiquidContainer) { - strcpy(buf, "Вы не видите ничего необычного."); + strcpy(buf, " ."); } else // ITEM_TYPE == kLiquidContainer||FOUNTAIN { - strcpy(buf, "Это емкость для жидкости."); + strcpy(buf, " ."); } } @@ -1329,9 +1329,9 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s if (mode == 1 && how <= 1) { if (object->get_type() == EObjType::kLightSource) { if (GET_OBJ_VAL(object, 2) == -1) - strcpy(buf2, " (вечный свет)"); + strcpy(buf2, " ( )"); else if (GET_OBJ_VAL(object, 2) == 0) - sprintf(buf2, " (погас%s)", grammar::ObjSexEnding((object)->get_sex(), 4)); + sprintf(buf2, " (%s)", grammar::ObjSexEnding((object)->get_sex(), 4)); else sprintf(buf2, " (%d %s)", GET_OBJ_VAL(object, 2), grammar::GetDeclensionInNumber(GET_OBJ_VAL(object, 2), grammar::EWhat::kHour)); @@ -1351,28 +1351,28 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s } } if ((object->get_type() == EObjType::kContainer) - && !IS_SET(GET_OBJ_VAL((object), 1), (EContainerFlag::kShutted))) // если закрыто, содержимое не показываем + && !IS_SET(GET_OBJ_VAL((object), 1), (EContainerFlag::kShutted))) // , { if (object->get_contains()) { - strcat(buf2, " (есть содержимое)"); + strcat(buf2, " ( )"); } else { - if (GET_OBJ_VAL(object, 3) < 1) // есть ключ для открытия, пустоту не показываем2 - sprintf(buf2 + strlen(buf2), " (пуст%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); + if (GET_OBJ_VAL(object, 3) < 1) // , 2 + sprintf(buf2 + strlen(buf2), " (%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); } } if ((object->get_type() == EObjType::kNote) && !object->get_action_description().empty()) { - strcat(buf2, " (что-то накарябано)"); + strcat(buf2, " (- )"); } } else if (mode >= 2 && how <= 1) { std::string obj_name = OBJN(object, ch, grammar::ECase::kNom); native_text::capitalize_first(obj_name); if (object->get_type() == EObjType::kLightSource) { if (GET_OBJ_VAL(object, 2) == -1) { - sprintf(buf2, "\r\n%s дает вечный свет.", obj_name.c_str()); + sprintf(buf2, "\r\n%s .", obj_name.c_str()); } else if (GET_OBJ_VAL(object, 2) == 0) { - sprintf(buf2, "\r\n%s погас%s.", obj_name.c_str(), grammar::ObjSexEnding((object)->get_sex(), 4)); + sprintf(buf2, "\r\n%s %s.", obj_name.c_str(), grammar::ObjSexEnding((object)->get_sex(), 4)); } else { - sprintf(buf2, "\r\n%s будет светить %d %s.", obj_name.c_str(), GET_OBJ_VAL(object, 2), + sprintf(buf2, "\r\n%s %d %s.", obj_name.c_str(), GET_OBJ_VAL(object, 2), grammar::GetDeclensionInNumber(GET_OBJ_VAL(object, 2), grammar::EWhat::kHour)); } } else if (object->get_current_durability() < object->get_maximum_durability()) { @@ -1387,45 +1387,45 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s // issue.obj-suppressor-affect: brief marker when a worn item's conferred magic is currently // suppressed (dispelled + auto-returning); the full list with timers shows on examine/identify. if (mode != 3 && how <= 1 && object->has_suppressed_affects()) { - strcat(buf, " <магия подавлена>"); + strcat(buf, " < >"); } if (mode != 3 && how <= 1) { if (object->has_flag(EObjFlag::kInvisible)) { - sprintf(buf2, " (невидим%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); + sprintf(buf2, " (%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); strcat(buf, buf2); } if (object->has_flag(EObjFlag::kBless) && AFF_FLAGGED(ch, EAffect::kDetectAlign)) - strcat(buf, " ..голубая аура!"); + strcat(buf, " .. !"); if (object->has_flag(EObjFlag::kMagic) && AFF_FLAGGED(ch, EAffect::kDetectMagic)) - strcat(buf, " ..желтая аура!"); + strcat(buf, " .. !"); if (object->has_flag(EObjFlag::kPoisoned) && AFF_FLAGGED(ch, EAffect::kDetectPoison)) { - sprintf(buf2, "..отравлен%s!", grammar::ObjSexEnding((object)->get_sex(), 6)); + sprintf(buf2, "..%s!", grammar::ObjSexEnding((object)->get_sex(), 6)); strcat(buf, buf2); } if (object->has_flag(EObjFlag::kGlow)) - strcat(buf, " ..блестит!"); + strcat(buf, " ..!"); if (object->has_flag(EObjFlag::kHum) && !AFF_FLAGGED(ch, EAffect::kDeafness)) - strcat(buf, " ..шумит!"); + strcat(buf, " ..!"); if (object->has_flag(EObjFlag::kFire)) - strcat(buf, " ..горит!"); + strcat(buf, " ..!"); if (object->has_flag(EObjFlag::kBloody)) { - sprintf(buf2, " %s..покрыт%s кровью!%s", kColorBoldRed, grammar::ObjSexEnding((object)->get_sex(), 6), kColorNrm); + sprintf(buf2, " %s..%s !%s", kColorBoldRed, grammar::ObjSexEnding((object)->get_sex(), 6), kColorNrm); strcat(buf, buf2); } } if (mode == 1) { - // клан-сундук, выводим список разом постранично + // -, if (show_state == 3) { sprintf(buf + strlen(buf), " [%d %s]\r\n", object->get_rent_on() * kClanStorehouseCoeff / 100, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(object->get_rent_on() * kClanStorehouseCoeff / 100, grammar::ECase::kNom).c_str()); return buf; } - // ингры + // else if (show_state == 4) { sprintf(buf + strlen(buf), " [%d %s]\r\n", object->get_rent_off(), MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(object->get_rent_off(), grammar::ECase::kNom).c_str()); @@ -1439,7 +1439,7 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s strcat(buf, diag_armor_type_to_char(object).c_str()); strcat(buf, diag_timer_to_char(object)); strcat(buf, "\r\n"); - //strcat(buf, diag_uses_to_char(object, ch)); // commented by WorM перенес в obj_info чтобы заряды рун было видно на базаре/ауке + //strcat(buf, diag_uses_to_char(object, ch)); // commented by WorM obj_info / strcat(buf, obj_affects::Diag(object, ch).c_str()); } page_string(ch->desc, buf, true); @@ -1454,11 +1454,11 @@ void print_zone_info(CharData *ch) { bool delim = false; if (!zone->is_town) { delim = put_delim(out, delim); - out << "средний уровень: " << zone->mob_level; + out << " : " << zone->mob_level; } if (zone->group > 1) { delim = put_delim(out, delim); - out << "групповая на " << zone->group + out << " " << zone->group << " " << grammar::GetDeclensionInNumber(zone->group, grammar::EWhat::kPeople); } if (delim) { @@ -1489,8 +1489,8 @@ const char *find_exdesc(const char *word, const std::vector &l } /** -* При чтении писем и осмотре чара в его описании подставляем в начало каждой строки пробел -* (для дурных тригов), пользуясь случаем передаю привет проне! +* +* ( ), ! */ std::string AddLeadingStringSpace(const std::string& text) { if (!text.empty()) { @@ -1556,33 +1556,33 @@ void diag_char_to_char(CharData *i, CharData *ch) { strcpy(buf, PersonName(i, ch, 0)); utils::CAP(buf); - // Состояние жизни красим тем же градиентом, каким показаны хиты в бою -- в промпте - // (iosystem.cpp) и в списке группы (groups.cpp). Без цвета строка при осмотре сливается - // с описанием персонажа. Красится только само состояние, имя и поза остаются как были. + // , -- + // (iosystem.cpp) (groups.cpp). + // . , . strcat(buf, GetWarmValueColor(i->get_hit(), i->get_real_max_hit())); if (percent >= 100) { - sprintf(buf2, " невредим%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 90) { - sprintf(buf2, " слегка поцарапан%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 75) { - sprintf(buf2, " легко ранен%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 50) { - sprintf(buf2, " ранен%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 30) { - sprintf(buf2, " тяжело ранен%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 15) { - sprintf(buf2, " смертельно ранен%s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 0) - strcat(buf, " в ужасном состоянии"); + strcat(buf, " "); else - strcat(buf, " умирает"); + strcat(buf, " "); strcat(buf, kColorNrm); @@ -1590,33 +1590,33 @@ void diag_char_to_char(CharData *i, CharData *ch) { switch (i->GetPosition()) { case EPosition::kPerish: strcat(buf, "."); break; - case EPosition::kIncap: strcat(buf, IsPoly(i) ? ", лежат без сознания." : ", лежит без сознания."); + case EPosition::kIncap: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; - case EPosition::kStun: strcat(buf, IsPoly(i) ? ", лежат в обмороке." : ", лежит в обмороке."); + case EPosition::kStun: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; - case EPosition::kSleep: strcat(buf, IsPoly(i) ? ", спят." : ", спит."); + case EPosition::kSleep: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; - case EPosition::kRest: strcat(buf, IsPoly(i) ? ", отдыхают." : ", отдыхает."); + case EPosition::kRest: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; - case EPosition::kSit: strcat(buf, IsPoly(i) ? ", сидят." : ", сидит."); + case EPosition::kSit: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; - case EPosition::kStand: strcat(buf, IsPoly(i) ? ", стоят." : ", стоит."); + case EPosition::kStand: strcat(buf, IsPoly(i) ? ", ." : ", ."); break; case EPosition::kFight: if (i->GetEnemy()) - strcat(buf, IsPoly(i) ? ", сражаются." : ", сражается."); + strcat(buf, IsPoly(i) ? ", ." : ", ."); else - strcat(buf, IsPoly(i) ? ", махают кулаками." : ", махает кулаками."); + strcat(buf, IsPoly(i) ? ", ." : ", ."); break; default: return; break; } else - strcat(buf, IsPoly(i) ? ", сидят верхом." : ", сидит верхом."); + strcat(buf, IsPoly(i) ? ", ." : ", ."); if (AFF_FLAGGED(ch, EAffect::kDetectPoison)) if (AFF_FLAGGED(i, EAffect::kPoisoned)) { - sprintf(buf2, " (отравлен%s)", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " (%s)", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } @@ -1625,12 +1625,12 @@ void diag_char_to_char(CharData *i, CharData *ch) { } -//ф-ция вывода доп инфы об объекте -//buf это буфер в который дописывать инфу, в нем уже может быть что-то иначе надо перед вызовом присвоить *buf='\0' +//- +//buf , - *buf='\0' void obj_info(CharData *ch, ObjData *obj, char buf[kMaxStringLength]) { int j; if (CanUseFeat(ch, EFeat::kSkilledTrader) || ch->IsFlagged(EPrf::kHolylight) || GetSkill(ch, ESkill::kJewelry)) { - sprintf(buf + strlen(buf), "Материал : %s", kColorCyn); + sprintf(buf + strlen(buf), " : %s", kColorCyn); sprinttype(obj->get_material(), material_name, buf + strlen(buf)); sprintf(buf + strlen(buf), "\r\n%s", kColorNrm); } @@ -1641,59 +1641,59 @@ void obj_info(CharData *ch, ObjData *obj, char buf[kMaxStringLength]) { for (j = 0; imtypes[j].id != GET_OBJ_VAL(obj, IM_TYPE_SLOT) && j <= top_imtypes;) { j++; } - sprintf(buf + strlen(buf), "Это ингредиент вида '%s'.\r\n", imtypes[j].name); + sprintf(buf + strlen(buf), " '%s'.\r\n", imtypes[j].name); const int imquality = GET_OBJ_VAL(obj, IM_POWER_SLOT); if (GetRealLevel(ch) >= imquality) { - sprintf(buf + strlen(buf), "Качество ингредиента "); + sprintf(buf + strlen(buf), " "); if (imquality > 25) - strcat(buf + strlen(buf), "наилучшее.\r\n"); + strcat(buf + strlen(buf), ".\r\n"); else if (imquality > 20) - strcat(buf + strlen(buf), "отличное.\r\n"); + strcat(buf + strlen(buf), ".\r\n"); else if (imquality > 15) - strcat(buf + strlen(buf), "очень хорошее.\r\n"); + strcat(buf + strlen(buf), " .\r\n"); else if (imquality > 10) - strcat(buf + strlen(buf), "выше среднего.\r\n"); + strcat(buf + strlen(buf), " .\r\n"); else if (imquality > 5) - strcat(buf + strlen(buf), "весьма посредственное.\r\n"); + strcat(buf + strlen(buf), " .\r\n"); else - strcat(buf + strlen(buf), "хуже не бывает.\r\n"); + strcat(buf + strlen(buf), " .\r\n"); } else { - strcat(buf + strlen(buf), "Вы не в состоянии определить качество этого ингредиента.\r\n"); + strcat(buf + strlen(buf), " .\r\n"); } } //|| EPrf::FLAGGED(ch, EPrf::HOLYLIGHT) if (CanUseFeat(ch, EFeat::kJeweller)) { - sprintf(buf + strlen(buf), "Слоты : %s", kColorCyn); + sprintf(buf + strlen(buf), " : %s", kColorCyn); if (obj->has_flag(EObjFlag::kHasThreeSlots)) { - strcat(buf, "доступно 3 слота\r\n"); + strcat(buf, " 3 \r\n"); } else if (obj->has_flag(EObjFlag::kHasTwoSlots)) { - strcat(buf, "доступно 2 слота\r\n"); + strcat(buf, " 2 \r\n"); } else if (obj->has_flag(EObjFlag::kHasOneSlot)) { - strcat(buf, "доступен 1 слот\r\n"); + strcat(buf, " 1 \r\n"); } else { - strcat(buf, "нет слотов\r\n"); + strcat(buf, " \r\n"); } sprintf(buf + strlen(buf), "\r\n%s", kColorNrm); } if (AUTH_CUSTOM_LABEL(obj, ch) && !obj->get_custom_label()->text_label.empty()) { if (!obj->get_custom_label()->clan_abbrev.empty()) { - strcat(buf, "Метки дружины: "); + strcat(buf, " : "); } else { - strcat(buf, "Ваши метки: "); + strcat(buf, " : "); } sprintf(buf + strlen(buf), "%s\r\n", obj->get_custom_label()->text_label.c_str()); } sprintf(buf + strlen(buf), "%s", diag_uses_to_char(obj, ch)); sprintf(buf + strlen(buf), "%s", diag_shot_to_char(obj, ch)); if (GET_OBJ_VNUM(obj) >= DUPLICATE_MINI_SET_VNUM) { - sprintf(buf + strlen(buf), "Светится белым сиянием.\r\n"); + sprintf(buf + strlen(buf), " .\r\n"); } if (((obj->get_type() == EObjType::kLiquidContainer) && (GET_OBJ_VAL(obj, 1) > 0)) || (obj->get_type() == EObjType::kFood)) { - sprintf(buf1, "Качество: %s\r\n", drinkcon::diag_liquid_timer(obj)); + sprintf(buf1, ": %s\r\n", drinkcon::diag_liquid_timer(obj)); strcat(buf, buf1); } } @@ -1784,37 +1784,37 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { char aura_txt[200]; const char *positions[] = { - "лежит здесь, мертвый. ", - "лежит здесь, при смерти. ", - "лежит здесь, без сознания. ", - "лежит здесь, в обмороке. ", - "спит здесь. ", - "отдыхает здесь. ", - "сидит здесь. ", - "СРАЖАЕТСЯ! ", - "стоит здесь. " + " , . ", + " , . ", + " , . ", + " , . ", + " . ", + " . ", + " . ", + "! ", + " . " }; - // Здесь и далее при использовании IsPoly() - патч для отображения позиций мобов типа "они" -- Ковшегуб + // IsPoly() - "" -- const char *poly_positions[] = { - "лежат здесь, мертвые. ", - "лежат здесь, при смерти. ", - "лежат здесь, без сознания. ", - "лежат здесь, в обмороке. ", - "спят здесь. ", - "отдыхают здесь. ", - "сидят здесь. ", - "СРАЖАЮТСЯ! ", - "стоят здесь. " + " , . ", + " , . ", + " , . ", + " , . ", + " . ", + " . ", + " . ", + "! ", + " . " }; if (mount::IsHorse(i) && mount::IsOnHorse(i->get_master())) { if (ch == i->get_master()) { if (!IsPoly(i)) { - act("$N несет вас на своей спине.", false, ch, nullptr, i, kToChar); + act("$N .", false, ch, nullptr, i, kToChar); } else { - act("$N несут вас на своей спине.", false, ch, nullptr, i, kToChar); + act("$N .", false, ch, nullptr, i, kToChar); } } @@ -1824,9 +1824,9 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (mode == ESkill::kLooking) { if (HERE(i) && InvisOk(ch, i) && GetRealLevel(ch) >= (i->IsNpc() ? 0 : GET_INVIS_LEV(i))) { if (GET_RACE(i) == ENpcRace::kConstruct && privilege::IsImmortal(ch)) { - sprintf(buf, "Вы разглядели %s.(предмет)\r\n", GET_PAD(i, 3)); + sprintf(buf, " %s.()\r\n", GET_PAD(i, 3)); } else { - sprintf(buf, "Вы разглядели %s.\r\n", GET_PAD(i, 3)); + sprintf(buf, " %s.\r\n", GET_PAD(i, 3)); } SendMsgToChar(buf, ch); } @@ -1840,34 +1840,34 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { *buf = 0; if (IS_SET(mode_flags, kAcheckAffects)) { REMOVE_BIT(mode_flags, kAcheckAffects); - sprintf(buf + strlen(buf), "магический ореол%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckLight)) { REMOVE_BIT(mode_flags, kAcheckLight); - sprintf(buf + strlen(buf), "яркий свет%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckGlowing) && IS_SET(mode_flags, kAcheckHumming) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckGlowing); REMOVE_BIT(mode_flags, kAcheckHumming); - sprintf(buf + strlen(buf), "шум и блеск экипировки%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckGlowing)) { REMOVE_BIT(mode_flags, kAcheckGlowing); - sprintf(buf + strlen(buf), "блеск экипировки%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckHumming) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckHumming); - sprintf(buf + strlen(buf), "шум экипировки%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckWeight) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckWeight); - sprintf(buf + strlen(buf), "бряцание металла%s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); } - strcat(buf, "выдает чье-то присутствие.\r\n"); + strcat(buf, " - .\r\n"); SendMsgToChar(utils::CAP(buf), ch); return; } @@ -1886,35 +1886,35 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && !AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (AFF_FLAGGED(i, EAffect::kForcesOfEvil)) { - strcat(buf, "(черная аура) "); + strcat(buf, "( ) "); } } if (AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (i->IsNpc()) { if (NPC_FLAGGED(i, ENpcFlag::kAirCreature)) - sprintf(buf + strlen(buf), "%s(аура воздуха)%s ", + sprintf(buf + strlen(buf), "%s( )%s ", kColorBoldBlu, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kWaterCreature)) - sprintf(buf + strlen(buf), "%s(аура воды)%s ", + sprintf(buf + strlen(buf), "%s( )%s ", kColorBoldCyn, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kFireCreature)) - sprintf(buf + strlen(buf), "%s(аура огня)%s ", + sprintf(buf + strlen(buf), "%s( )%s ", kColorBoldMag, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kEarthCreature)) - sprintf(buf + strlen(buf), "%s(аура земли)%s ", + sprintf(buf + strlen(buf), "%s( )%s ", kColorBoldGrn, kColorBoldRed); } } if (AFF_FLAGGED(i, EAffect::kInvisible)) - sprintf(buf + strlen(buf), "(невидим%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); if (AFF_FLAGGED(i, EAffect::kHide)) - sprintf(buf + strlen(buf), "(спрятал%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kDisguise)) - sprintf(buf + strlen(buf), "(замаскировал%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kFly)) - strcat(buf, IsPoly(i) ? "(летят) " : "(летит) "); + strcat(buf, IsPoly(i) ? "() " : "() "); if (AFF_FLAGGED(i, EAffect::kHorse)) - strcat(buf, "(под седлом) "); + strcat(buf, "( ) "); std::string line = buf; line += i->player_data.long_descr; @@ -1930,66 +1930,66 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { strcpy(buf1, i->get_npc_name().c_str()); strcat(buf1, " "); if (AFF_FLAGGED(i, EAffect::kHorse)) - strcat(buf1, "(под седлом) "); + strcat(buf1, "( ) "); utils::CAP(buf1); } else { - sprintf(buf1, "%s%s ", i->race_or_title().c_str(), i->IsFlagged(EPlrFlag::kKiller) ? " <ДУШЕГУБ>" : ""); + sprintf(buf1, "%s%s ", i->race_or_title().c_str(), i->IsFlagged(EPlrFlag::kKiller) ? " <>" : ""); } snprintf(buf, kMaxStringLength, "%s%s", AFF_FLAGGED(i, EAffect::kCharmed) ? "*" : "", buf1); if (AFF_FLAGGED(i, EAffect::kInvisible)) - sprintf(buf + strlen(buf), "(невидим%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); if (AFF_FLAGGED(i, EAffect::kHide)) - sprintf(buf + strlen(buf), "(спрятал%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kDisguise)) - sprintf(buf + strlen(buf), "(замаскировал%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (!i->IsNpc() && !i->desc) - sprintf(buf + strlen(buf), "(потерял%s связь) ", grammar::SexEnding((i)->get_sex(), 1)); + sprintf(buf + strlen(buf), "(%s ) ", grammar::SexEnding((i)->get_sex(), 1)); if (!i->IsNpc() && i->IsFlagged(EPlrFlag::kWriting)) - strcat(buf, "(пишет) "); + strcat(buf, "() "); if (i->GetPosition() != EPosition::kFight) { if (mount::IsOnHorse(i)) { CharData *horse = mount::GetHorse(i); if (horse) { const char *msg = - AFF_FLAGGED(horse, EAffect::kFly) ? "летает" : "сидит"; - sprintf(buf + strlen(buf), "%s здесь верхом на %s. ", + AFF_FLAGGED(horse, EAffect::kFly) ? "" : ""; + sprintf(buf + strlen(buf), "%s %s. ", msg, PersonName(horse, ch, 5)); } } else if (mount::IsHorse(i) && AFF_FLAGGED(i, EAffect::kTethered)) - sprintf(buf + strlen(buf), "привязан%s здесь. ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "%s . ", grammar::SexEnding((i)->get_sex(), 6)); else if ((sector = real_sector(i->in_room)) == ESector::kOnlyFlying) - strcat(buf, IsPoly(i) ? "летают здесь. " : "летает здесь. "); + strcat(buf, IsPoly(i) ? " . " : " . "); else if (sector == ESector::kUnderwater) - strcat(buf, IsPoly(i) ? "плавают здесь. " : "плавает здесь. "); + strcat(buf, IsPoly(i) ? " . " : " . "); else if (i->GetPosition() > EPosition::kSleep && AFF_FLAGGED(i, EAffect::kFly)) - strcat(buf, IsPoly(i) ? "летают здесь. " : "летает здесь. "); + strcat(buf, IsPoly(i) ? " . " : " . "); else if (sector == ESector::kWaterSwim || sector == ESector::kWaterNoswim) - strcat(buf, IsPoly(i) ? "плавают здесь. " : "плавает здесь. "); + strcat(buf, IsPoly(i) ? " . " : " . "); else strcat(buf, IsPoly(i) ? poly_positions[static_cast(i->GetPosition())] : positions[static_cast(i->GetPosition())]); if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && i->IsNpc() && IsAffected(i, EAffect::kCapable)) - sprintf(buf + strlen(buf), "(аура магии) "); + sprintf(buf + strlen(buf), "( ) "); } else { if (i->GetEnemy()) { - strcat(buf, IsPoly(i) ? "сражаются с " : "сражается с "); + strcat(buf, IsPoly(i) ? " " : " "); if (i->in_room != i->GetEnemy()->in_room) - strcat(buf, "чьей-то тенью"); + strcat(buf, "- "); else if (i->GetEnemy() == ch) - strcat(buf, "ВАМИ"); + strcat(buf, ""); else strcat(buf, GET_PAD(i->GetEnemy(), 4)); if (mount::IsOnHorse(i)) - sprintf(buf + strlen(buf), ", сидя верхом на %s! ", PersonName(mount::GetHorse(i), ch, 5)); + sprintf(buf + strlen(buf), ", %s! ", PersonName(mount::GetHorse(i), ch, 5)); else strcat(buf, "! "); } else // NIL fighting pointer { - strcat(buf, IsPoly(i) ? "колотят по воздуху" : "колотит по воздуху"); + strcat(buf, IsPoly(i) ? " " : " "); if (mount::IsOnHorse(i)) - sprintf(buf + strlen(buf), ", сидя верхом на %s. ", PersonName(mount::GetHorse(i), ch, 5)); + sprintf(buf + strlen(buf), ", %s. ", PersonName(mount::GetHorse(i), ch, 5)); else strcat(buf, ". "); } @@ -1998,26 +1998,26 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && !AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "(черная аура) "); + strcat(buf, "( ) "); } if (AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (i->IsNpc()) { if (alignment::IsEvil(i)) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "(иссиня-черная аура) "); + strcat(buf, "(- ) "); else - strcat(buf, "(темная аура) "); + strcat(buf, "( ) "); } else if (alignment::IsGood(i)) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "(серая аура) "); + strcat(buf, "( ) "); else - strcat(buf, "(светлая аура) "); + strcat(buf, "( ) "); } else { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "(черная аура) "); + strcat(buf, "( ) "); } } else { AddPkAuraDescription(i, aura_txt); @@ -2029,7 +2029,7 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { // affect/spell -- every poison affect_type carries the kAfPoison flag (affects.xml; aconitum via poison.cpp). if (AFF_FLAGGED(ch, EAffect::kDetectPoison)) if (IsAffectedWithFlag(i, kAfPoison)) - sprintf(buf + strlen(buf), "(отравлен%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); std::string line = buf; AppendCompactShieldSuffix(line, ch, i); @@ -2047,89 +2047,89 @@ char *diag_weapon_to_char(const CObjectPrototype *obj, int show_wear) { if (obj->get_type() == EObjType::kWeapon) { const auto skill = static_cast(obj->get_spec_param()); if (skill != ESkill::kUndefined) { - sprintf(out_str, "Принадлежит к классу \"%s\".\r\n", MUD::Skill(skill).name.c_str()); + sprintf(out_str, " \"%s\".\r\n", MUD::Skill(skill).name.c_str()); } else { - sprintf(out_str, "!! Не принадлежит к известным типам оружия - сообщите Богам (skill=%d) !!\r\n", obj->get_spec_param()); + sprintf(out_str, "!! - (skill=%d) !!\r\n", obj->get_spec_param()); } } if (show_wear) { if (CAN_WEAR(obj, EWearFlag::kFinger)) { - sprintf(out_str + strlen(out_str), "Можно надеть на палец.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kNeck)) { - sprintf(out_str + strlen(out_str), "Можно надеть на шею.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kBody)) { - sprintf(out_str + strlen(out_str), "Можно надеть на туловище.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHead)) { - sprintf(out_str + strlen(out_str), "Можно надеть на голову.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kLegs)) { - sprintf(out_str + strlen(out_str), "Можно надеть на ноги.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kFeet)) { - sprintf(out_str + strlen(out_str), "Можно обуть.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHands)) { - sprintf(out_str + strlen(out_str), "Можно надеть на кисти.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kArms)) { - sprintf(out_str + strlen(out_str), "Можно надеть на руки.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kShoulders)) { - sprintf(out_str + strlen(out_str), "Можно надеть на плечи.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWaist)) { - sprintf(out_str + strlen(out_str), "Можно надеть на пояс.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kQuiver)) { - sprintf(out_str + strlen(out_str), "Можно использовать как колчан.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWrist)) { - sprintf(out_str + strlen(out_str), "Можно надеть на запястья.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (show_wear > 1) { if (CAN_WEAR(obj, EWearFlag::kShield)) { need_str = std::max(0, calc_str_req((obj->get_weight() + 1) / 2, STR_HOLD_W)); sprintf(out_str + strlen(out_str), - "Можно использовать как щит (требуется %d %s).\r\n", + " ( %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kWield)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_WIELD_W)); sprintf(out_str + strlen(out_str), - "Можно взять в правую руку (требуется %d %s).\r\n", + " ( %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kHold)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_HOLD_W)); sprintf(out_str + strlen(out_str), - "Можно взять в левую руку (требуется %d %s).\r\n", + " ( %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kBoth)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_BOTH_W)); sprintf(out_str + strlen(out_str), - "Можно взять в обе руки (требуется %d %s).\r\n", + " ( %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } } else { if (CAN_WEAR(obj, EWearFlag::kShield)) { - sprintf(out_str + strlen(out_str), "Можно использовать как щит.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWield)) { - sprintf(out_str + strlen(out_str), "Можно взять в правую руку.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHold)) { - sprintf(out_str + strlen(out_str), "Можно взять в левую руку.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } if (CAN_WEAR(obj, EWearFlag::kBoth)) { - sprintf(out_str + strlen(out_str), "Можно взять в обе руки.\r\n"); + sprintf(out_str + strlen(out_str), " .\r\n"); } } } @@ -2138,13 +2138,13 @@ char *diag_weapon_to_char(const CObjectPrototype *obj, int show_wear) { std::string diag_armor_type_to_char(const ObjData *obj) { if (obj->get_type() == EObjType::kLightArmor) { - return "Легкий тип доспехов.\r\n"; + return " .\r\n"; } if (obj->get_type() == EObjType::kMediumArmor) { - return "Средний тип доспехов.\r\n"; + return " .\r\n"; } if (obj->get_type() == EObjType::kHeavyArmor) { - return "Тяжелый тип доспехов.\r\n"; + return " .\r\n"; } return ""; } @@ -2152,7 +2152,7 @@ std::string diag_armor_type_to_char(const ObjData *obj) { char *diag_timer_to_char(const ObjData *obj) { static char out_str[kMaxStringLength]; *out_str = 0; - sprintf(out_str, "Состояние: %s.", diag_obj_timer(obj)); + sprintf(out_str, ": %s.", diag_obj_timer(obj)); return (out_str); } @@ -2165,10 +2165,10 @@ char *diag_uses_to_char(ObjData *obj, CharData *ch) { && IS_MANA_CASTER(ch)) { int i = -1; if ((i = GetObjRnum(GET_OBJ_VAL(obj, 1))) >= 0) { - sprintf(out_str, "Прототип: %s%s%s.\r\n", + sprintf(out_str, ": %s%s%s.\r\n", kColorBoldCyn, obj_proto[i]->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); } - sprintf(out_str + strlen(out_str), "Осталось применений: %s%d&n.\r\n", + sprintf(out_str + strlen(out_str), " : %s%d&n.\r\n", GET_OBJ_VAL(obj, 2) > 100 ? "&G" : "&R", GET_OBJ_VAL(obj, 2)); } return (out_str); @@ -2180,31 +2180,31 @@ char *diag_shot_to_char(ObjData *obj, CharData *ch) { *out_str = 0; if (obj->get_type() == EObjType::kMagicContaner && (ch->GetClass() == ECharClass::kRanger || ch->GetClass() == ECharClass::kCharmer || IS_MANA_CASTER(ch))) { - sprintf(out_str + strlen(out_str), "Осталось стрел: %s%d&n.\r\n", + sprintf(out_str + strlen(out_str), " : %s%d&n.\r\n", GET_OBJ_VAL(obj, 2) > 3 ? "&G" : "&R", GET_OBJ_VAL(obj, 2)); } return (out_str); } -// Чтобы можно было получить только строку состяния +// const char *diag_obj_timer(const ObjData *obj) { int prot_timer; if (obj->get_rnum() != kNothing) { if (stable_objs::IsTimerUnlimited(obj)) { - return "нерушимо"; + return ""; } if (obj->get_craft_timer() > 0) { - prot_timer = obj->get_craft_timer();// если вещь скрафчена, смотрим ее таймер а не у прототипа + prot_timer = obj->get_craft_timer();// , } else { prot_timer = obj_proto[obj->get_rnum()]->get_timer(); } if (!prot_timer) { - return "Прототип предмета имеет нулевой таймер!\r\n"; + return " !\r\n"; } - const int tm = (obj->get_timer() * 100 / prot_timer); // если вещь скрафчена, смотрим ее таймер а не у прототипа + const int tm = (obj->get_timer() * 100 / prot_timer); // , return print_obj_state(tm); } return ""; @@ -2212,22 +2212,22 @@ const char *diag_obj_timer(const ObjData *obj) { const char *print_obj_state(int tm_pct) { if (tm_pct < 20) - return "ужасно"; + return ""; else if (tm_pct < 40) - return "скоро сломается"; + return " "; else if (tm_pct < 60) - return "плоховато"; + return ""; else if (tm_pct < 80) - return "средне"; - //else if (tm_pct <=100) // у только что созданной шмотки значение 100% первый тик, потому <= - // return "идеально"; - else if (tm_pct < 1000) // проблема крафта, на хаймортах таймер больще прототипа - return "идеально"; - else return "нерушимо"; + return ""; + //else if (tm_pct <=100) // 100% , <= + // return ""; + else if (tm_pct < 1000) // , + return ""; + else return ""; } -// Это не совсем механика зрения, нужен отдельный модуль на тему "кто кого (не)видит" -// Но пока эти функции распиханы по всему коду, начиная с handler, по этому временно - тут +// , " ()" +// , handler, - void Appear(CharData *ch) { const bool appear_msg = AFF_FLAGGED(ch, EAffect::kInvisible) || AFF_FLAGGED(ch, EAffect::kDisguise) @@ -2249,9 +2249,9 @@ void Appear(CharData *ch) { if (appear_msg) { if (ch->IsNpc() || GetRealLevel(ch) < kLvlImmortal) { - act("$n медленно появил$u из пустоты.", false, ch, nullptr, nullptr, kToRoom); + act("$n $u .", false, ch, nullptr, nullptr, kToRoom); } else { - act("Вы почувствовали странное присутствие $n1.", + act(" $n1.", false, ch, nullptr, nullptr, kToRoom); } } diff --git a/src/gameplay/statistics/dps.cpp b/src/gameplay/statistics/dps.cpp index fd5c59f27..7f41ab78c 100644 --- a/src/gameplay/statistics/dps.cpp +++ b/src/gameplay/statistics/dps.cpp @@ -13,10 +13,10 @@ namespace DpsSystem { -// кол-во учитываемых чармисов с каждого чара +// - const unsigned MAX_DPS_CHARMICE = 5; -// * Добавление эффективной дамаги и овер-дамаги. +// * -. void DpsNode::add_dmg(int dmg, int over_dmg) { if (dmg >= 0 && over_dmg >= 0) { dmg_ += dmg; @@ -26,8 +26,8 @@ void DpsNode::add_dmg(int dmg, int over_dmg) { } /** -* Имя для вывода в статистике при отсутствии онлайн. -* Пока осталось актуальным только для чармисов. +* . +* . */ void DpsNode::set_name(const char *name) { if (name && *name) { @@ -38,7 +38,7 @@ void DpsNode::set_name(const char *name) { } } -// * Расчет дамаги в раунд. +// * . int DpsNode::get_stat() const { if (rounds_) { return dmg_ / rounds_; @@ -58,7 +58,7 @@ const std::string &DpsNode::get_name() const { return name_; } -// * Используется для идентификации в списках как чармисов, так и чаров. +// * , . long DpsNode::get_id() const { return id_; } @@ -91,7 +91,7 @@ void Dps::AddDmg(int type, CharData *ch, int dmg, int over_dmg) { AddGroupCharmDmg(ch, dmg, over_dmg); } break; - default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); + default: log("SYSERROR: , func: %s", __func__); return; } } @@ -106,12 +106,12 @@ void Dps::EndRound(int type, CharData *ch) { break; case GROUP_CHARM_DPS: EndGroupCharmRound(ch); break; - default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); + default: log("SYSERROR: , func: %s", __func__); return; } } -// * Очистка себя или группы. +// * . void Dps::Clear(int type) { switch (type) { case PERS_DPS: { @@ -126,12 +126,12 @@ void Dps::Clear(int type) { case GROUP_DPS: group_dps_.clear(); break; case GROUP_CHARM_DPS: break; - default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); + default: log("SYSERROR: , func: %s", __func__); return; } } -// * Для сортировки вывода по нанесенной дамаге. +// * . struct sort_node { sort_node(const std::string &in_name, int in_dps, unsigned in_round_dmg, unsigned in_over_dmg) : dps(in_dps), round_dmg(in_round_dmg), over_dmg(in_over_dmg) { @@ -146,7 +146,7 @@ struct sort_node { typedef std::multimap SortGroupType; SortGroupType tmp_group_list; -// суммарный дамаг группы при распечатке статистики +// unsigned tmp_total_dmg = 0; void Dps::AddTmpGroupList(CharData *ch) { @@ -161,9 +161,9 @@ void Dps::AddTmpGroupList(CharData *ch) { } /** -* Распечатка персональной статистики игрока и его чармисов. -* \param ch - игрок, которому идет распечатка. -* \param coder - случай вывода статистики другому персонажу, по умолчанию = 0 +* . +* \param ch - , . +* \param coder - , = 0 */ void Dps::PrintStats(CharData *ch, CharData *coder) { if (!coder) { @@ -185,15 +185,15 @@ void Dps::PrintStats(CharData *ch, CharData *coder) { } /** - * Вывести в поток персональную статистику по урону. - * @param ch - персонаж, для которого формируется статистика. - * @param out - поток для вывода. + * . + * @param ch - , . + * @param out - . */ void Dps::PrintPersonalDpsStat(CharData *ch, std::ostringstream &out) { - out << " Персональная статистика урона:" << "\r\n"; + out << " :" << "\r\n"; table_wrapper::Table table; table << table_wrapper::kHeader << - "Имя" << "Урон" << "В раунд" << "Макс." << "Лишний урон" << table_wrapper::kEndRow; + "" << "" << " " << "." << " " << table_wrapper::kEndRow; table << ch->get_name() << pers_dps_.get_dmg() << pers_dps_.get_stat() @@ -206,29 +206,29 @@ void Dps::PrintPersonalDpsStat(CharData *ch, std::ostringstream &out) { } /** - * Вывести в поток персональную статистику по полученному и потерянному опыту. - * @param out - выводной поток. + * . + * @param out - . */ void Dps::PrintPersonalExpStat(std::ostringstream &out) const { - out << " Персональная статистика по опыту:" << "\r\n"; + out << " :" << "\r\n"; double percent = exp_ ? battle_exp_ * 100.0 / exp_ : 0.0; int balance = exp_ + lost_exp_; - out << kColorBoldBlu << " *" << kColorNrm << " Всего получено опыта: " << PrintNumberByDigits(exp_) - << " Из него за удары: " << PrintNumberByDigits(battle_exp_) + out << kColorBoldBlu << " *" << kColorNrm << " : " << PrintNumberByDigits(exp_) + << " : " << PrintNumberByDigits(battle_exp_) << " (" << std::setprecision(2) << percent << "%)" << "\r\n" - << kColorBoldBlu << " *" << kColorNrm << " Потеряно опыта: " << PrintNumberByDigits(abs(lost_exp_)); + << kColorBoldBlu << " *" << kColorNrm << " : " << PrintNumberByDigits(abs(lost_exp_)); if (balance != 0) { - out << " Баланс: " << (balance > 0 ? "+" : "-") << PrintNumberByDigits(abs(balance)) << "\r\n"; + out << " : " << (balance > 0 ? "+" : "-") << PrintNumberByDigits(abs(balance)) << "\r\n"; } } /** -* Распечатка групповой статистики, находящейся у лидера группы. -* \param ch - игрок, которому идет распечатка. -* \param coder - случай вывода статистики другому персонажу, по умолчанию = nullptr +* , . +* \param ch - , . +* \param coder - , = nullptr */ void Dps::PrintGroupStats(CharData *ch, CharData *coder) { if (!coder) { @@ -250,11 +250,11 @@ void Dps::PrintGroupStats(CharData *ch, CharData *coder) { } std::ostringstream out; - out << " Статистика урона вашей группы:" << "\r\n"; + out << " :" << "\r\n"; table_wrapper::Table table; table << table_wrapper::kHeader << - "Имя" << "Урон" << "(%)" << "В раунд" << "Макс." << "Лишний урон" << table_wrapper::kEndRow; + "" << "" << "(%)" << " " << "." << " " << table_wrapper::kEndRow; for (auto it = tmp_group_list.rbegin(); it != tmp_group_list.rend(); ++it) { double percent = tmp_total_dmg ? it->first * 100.0 / tmp_total_dmg : 0.0; table << it->second.name @@ -319,7 +319,7 @@ void Dps::EndGroupCharmRound(CharData *ch) { } } -// * Чтобы не морочить голову в dps_copy, заменяем только груп.статистику. +// * dps_copy, .. Dps &Dps::operator=(const Dps ©) { if (this != ©) { group_dps_ = copy.group_dps_; @@ -368,7 +368,7 @@ void PlayerDpsNode::add_charm_dmg(CharData *ch, int dmg, int over_dmg) { if (i != charm_list_.end()) { i->add_dmg(dmg, over_dmg); } else { - log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); + log("SYSERROR: , func: %s", __func__); } } @@ -377,11 +377,11 @@ void PlayerDpsNode::end_charm_round(CharData *ch) { if (it != charm_list_.end()) { it->end_round(); } else { - log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); + log("SYSERROR: , func: %s", __func__); } } -// * Чармисы в персональной статистике выводятся без сортировки. +// * . void PlayerDpsNode::print_charm_stats(table_wrapper::Table &table) const { for (const auto & it : charm_list_) { if (it.get_dmg() > 0) { @@ -395,7 +395,7 @@ void PlayerDpsNode::print_charm_stats(table_wrapper::Table &table) const { } } -// * Распечатка групповой статистики живых чармисов по данному игроку. +// * . void PlayerDpsNode::print_group_charm_stats(CharData *ch) const { for (auto *f : ch->followers) { if (!IsCharmice(f)) { @@ -417,7 +417,7 @@ void PlayerDpsNode::print_group_charm_stats(CharData *ch) const { // PlayerDpsNode //////////////////////////////////////////////////////////////////////////////// -// * Подсчет дамаги за предыдущий раунд, дергается в начале раунда и по окончанию боя. +// * , . void check_round(CharData *ch) { if (!ch->IsNpc()) { ch->dps_end_round(DpsSystem::PERS_DPS); @@ -470,17 +470,17 @@ void UpdateDpsStatistics(CharData *ch, int real_dam, int over_dam) { namespace { const char *DMETR_FORMAT = - "Формат команды:\r\n" - "дметр - вывод всей статистики\r\n" - "дметр очистить - очистка персональной статистики\r\n" - "дметр очистить группа - очистка групповой статистики (только лидер)\r\n"; + " :\r\n" + " - \r\n" + " - \r\n" + " - ( )\r\n"; } // namespace /** -* 'дметр' - вывод своей статистики и групповой, если в группе. -* 'дметр очистить' - очистка своей статистики. -* 'дметр очистить группа' - очистка групповой статистики (только лидер). +* '' - , . +* ' ' - . +* ' ' - ( ). */ void do_dmeter(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) { @@ -492,28 +492,28 @@ void do_dmeter(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!*arg) { ch->dps_print_stats(); - } else if (isname(arg, "очистить")) { + } else if (isname(arg, "")) { if (!*name) { ch->dps_clear(DpsSystem::PERS_DPS); - SendMsgToChar("Персональная статистика очищена.\r\n", ch); - } else if (isname(name, "группа")) { + SendMsgToChar(" .\r\n", ch); + } else if (isname(name, "")) { if (!AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar("Вы не состоите в группе.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } if (ch->has_master()) { - SendMsgToChar("Вы не являетесь лидером группы.\r\n", ch); + SendMsgToChar(" .\r\n", ch); return; } ch->dps_clear(DpsSystem::GROUP_DPS); } } else if (ch->IsFlagged(EPrf::kCoderinfo)) { - // распечатка статистики указанного персонажа + // CharData *vict = target_resolver::FindPlayerVis(ch, arg); if (vict) { vict->dps_print_stats(ch); } else { - SendMsgToChar("Нет такого персонажа.\r\n", ch); + SendMsgToChar(" .\r\n", ch); } } else { SendMsgToChar(DMETR_FORMAT, ch); diff --git a/src/gameplay/statistics/mob_stat.cpp b/src/gameplay/statistics/mob_stat.cpp index 5c51b8c4d..d2a4d5cb3 100644 --- a/src/gameplay/statistics/mob_stat.cpp +++ b/src/gameplay/statistics/mob_stat.cpp @@ -22,22 +22,22 @@ namespace char_stat { -/// мобов убито - для 'статистика' +/// - '' int mobs_killed = 0; -/// игроков убито - для 'статистика' +/// - '' int players_killed = 0; using ClassExpStat = std::pair; using ExpStatArray = std::array; using ExpStatArrayPtr = std::unique_ptr; -/// Опыт за ребут с делением по профам - для 'статистика' +/// - '' std::unordered_map classes_exp_stat; /* - * Подобная инициализация небезопасна: всегда существует возможность, что кто-то сунется в структуру - * статистики до ее инициализации. - * \todo Нужно сделать глобальное хранилище статистики в global_objects и засунуть это все туда - * инициализацию, соответственно, разместить в процедуре загрузки мада. ABYRVALG + * : , - + * . + * \todo global_objects + * , , . ABYRVALG */ void InitClassesExpStat() { for ( const auto &it : MUD::Classes()) { @@ -89,11 +89,11 @@ void PrintClassesExpStat(std::ostringstream &out) { auto tmp_array = BuildExpStatArray(); const unsigned long long top_exp = (*tmp_array)[0].second; - out << " Соотношения набранного с перезагрузки опыта:" << "\r\n" << "\r\n"; + out << " :" << "\r\n" << "\r\n"; /* - * При более чем 2 столбцах будет работать неправильно. - * Но возиться сейчас с написанием универсальной функции не вижу смысла: - * нужен полноценный модуль форматирования таблиц, а не такие костыли по всему коду. + * 2 . + * : + * , . * \todo table format */ const int columns{2}; @@ -130,12 +130,12 @@ namespace mob_stat { // mob_stat_new.xml (legacy XML fallback) and mob_stat.bin (current binary store) live // under state/statistics/; their paths come from StateManager (kMobStat / kMobStatBin). -/// за сколько месяцев хранится статистика (+ текущий месяц) +/// (+ ) const int kMobStatHistorySize = 6; -/// выборка кол-ва мобов для show stats при старте мада +/// - show stats std::map count_stats; std::map kill_stats; -/// список мобов по внуму и месяцам +/// const time_t MobKillStat::default_date = 0; @@ -150,14 +150,14 @@ std::pair GetDate() { namespace { -// Бинарный формат mob_stat. Файл машинный, руками не правится, поэтому -// XML тут только тормозил и запись (построение DOM), и чтение при бутe. -// Порядок байт нативный: файл не шарится между архитектурами. +// mob_stat. , , +// XML ( DOM), e. +// : . // -// Записи kills хранятся разреженно (индекс + значение, только ненулевые) -- -// иначе фикс-массив kills[kMaxGroupSize+1] на каждый стат раздувал файл -// больше XML (там тоже писались только ненулевые n#). -constexpr uint32_t kMobStatBinMagic = 0x4253544D; // 'MSTB' в little-endian +// kills ( + , ) -- +// - kills[kMaxGroupSize+1] +// XML ( n#). +constexpr uint32_t kMobStatBinMagic = 0x4253544D; // 'MSTB' little-endian constexpr uint32_t kMobStatBinVersion = 2; template @@ -172,8 +172,8 @@ bool BinRead(std::istream &in, T &value) { } // namespace -// Валидация + накопление count_stats/kill_stats для одного разобранного -// стата. Общая для бинарного и legacy-XML путей загрузки. +// + count_stats/kill_stats +// . legacy-XML . static bool ApplyParsedStat(MobKillStat &tmp_time, MobMonthKillStat tmp_mob) { if (tmp_mob.month <= 0 || tmp_mob.month > 12 || tmp_mob.year <= 0) { return false; @@ -193,8 +193,8 @@ static bool ApplyParsedStat(MobKillStat &tmp_time, MobMonthKillStat tmp_mob) { return true; } -// Чтение бинарного файла. true -- файл существует (даже если оказался -// битым: тогда просто не падаем в XML), false -- файла нет (нужна миграция). +// . true -- ( +// : XML), false -- ( ). static bool LoadBinary() { std::ifstream in(MUD::StateManager().Path(state::EStateFile::kMobStatBin), std::ios::binary); if (!in.is_open()) { @@ -209,8 +209,8 @@ static bool LoadBinary() { return false; } if (!BinRead(in, version) || version != kMobStatBinVersion) { - // Старый/чужой формат -- падаем в XML-миграцию, следующий Save - // перезапишет bin актуальной версией. + // / -- XML-, Save + // bin . mudlog("mob_stat.bin: unsupported version, fall back to XML", CMP, kLvlImmortal, SYSLOG, true); return false; } @@ -236,7 +236,7 @@ static bool LoadBinary() { } tmp_mob.month = month; tmp_mob.year = year; - // Разреженные kills: nz пар (индекс, значение). + // kills: nz (, ). uint8_t nz = 0; if (!BinRead(in, nz)) { break; @@ -267,14 +267,14 @@ static bool LoadBinary() { return true; } -// Разовая миграция со старого XML (mob_stat_new.xml). После первого Save -// данные уже в бинаре, и этот путь больше не используется. +// XML (mob_stat_new.xml). Save +// , . static void LoadXmlLegacy() { char buf_[kMaxInputLength]; pugi::xml_document doc; - // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже - // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. + // KOI8-R; , + // (issue #3681). KOI8-R . const std::string xml_mob_stat = native_text::read_data_file(MUD::StateManager().Path(state::EStateFile::kMobStat)); pugi::xml_parse_result result = doc.load_buffer(xml_mob_stat.data(), xml_mob_stat.size()); if (!result) { @@ -304,7 +304,7 @@ static void LoadXmlLegacy() { kill_date = static_cast(xml_date.as_ullong(kill_date)); } - // инит статы конкретного моба по месяцам + // MobKillStat tmp_time(kill_date); for (pugi::xml_node xml_time = xml_mob.child("t"); xml_time; xml_time = xml_time.next_sibling("t")) { @@ -347,7 +347,7 @@ void Save() { for (const auto &stat : i.second.stats) { BinWrite(buf, static_cast(stat.month)); BinWrite(buf, static_cast(stat.year)); - // Разреженно: только ненулевые kills как пары (индекс, значение). + // : kills (, ). uint8_t nz = 0; for (int g = 0; g <= kMaxGroupSize; ++g) { if (stat.kills.at(g) > 0) { @@ -364,9 +364,9 @@ void Save() { } } - // Запись синхронно в основном потоке -- без отдельного треда: замеряем, - // нужен ли он вообще (см. лог времени ниже). Через .tmp + rename для - // атомарности. + // -- : , + // (. ). .tmp + rename + // . const std::string filename = MUD::StateManager().Path(state::EStateFile::kMobStatBin); const std::string tmp = filename + ".tmp"; { @@ -394,14 +394,14 @@ void ClearZoneStat(ZoneVnum zone_vnum) { void ShowStats(CharData *ch) { std::stringstream out; - out << " Всего уникальных мобов в статистике убийств: " + out << " : " << mob_stat_register.size() << "\r\n" - << " Количество уникальных мобов по месяцам:"; + << " :"; for (auto & count_stat : count_stats) { out << " " << std::setw(2) << std::setfill('0') << count_stat.first << ":" << count_stat.second; } - out << "\r\n" << " Количество убитых мобов по месяцам:"; + out << "\r\n" << " :"; for (auto & kill_stat : kill_stats) { out << " " << std::setw(2) << std::setfill('0') << kill_stat.first << ":" << kill_stat.second; } @@ -414,7 +414,7 @@ void UpdateMobNode(std::list &node_list, int members) { auto date = GetDate(); auto k = node_list.rbegin(); const int months = k->month + k->year * 12; - // сравнение номера месяца с переключением на следующий + // if (months == date.first + date.second * 12) { k->kills.at(members) += 1; } else { @@ -423,7 +423,7 @@ void UpdateMobNode(std::list &node_list, int members) { node.year = date.second; node.kills.at(members) += 1; node_list.push_back(node); - // проверка на переполнение кол-ва месяцев + // - if (node_list.size() > kMobStatHistorySize + 1) { node_list.erase(node_list.begin()); } @@ -446,7 +446,7 @@ void GetLastMobKill(CharData *mob, std::string &result) { const auto killtime = i->second.date; result = asctime(localtime(&killtime)); } else { - result = "никогда!\r\n"; + result = "!\r\n"; } } @@ -519,9 +519,9 @@ void ShowZoneMobKillsStat(CharData *ch, ZoneVnum zone_vnum, int months) { } std::stringstream out; - out << "Статистика убийств мобов в зоне номер " << zone_vnum - << ", месяцев: " << months << "\r\n" - " vnum : имя : pk : группа = убийств (n3=100 моба убили 100 раз втроем)\r\n\r\n"; + out << " " << zone_vnum + << ", : " << months << "\r\n" + " vnum : : pk : = (n3=100 100 )\r\n\r\n"; for (auto & i : sort_list) { out << i.first << " : " << fmt::format("{:>20}", PrintMobName(i.first, 20)) << " : " diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 223b742c6..7a89968df 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -17,9 +17,9 @@ namespace text_id { /// -/// Содержит списки соответствия номер=строка/строка=номер для конверта -/// внутри-игровых констант в строковые ИД и обратно. -/// Нужно для независимости файлов от изменения номеров констант в коде. +/// =/= +/// - . +/// . /// class TextIdNode { public: @@ -33,11 +33,11 @@ class TextIdNode { std::unordered_map str_to_num; }; -// общий список конвертируемых констант +// std::array text_id_list; /// -/// Инит текстовых ИД параметров предметов для сохранения в файл. +/// . /// void InitObjVals() { // issue.magic-items: unified magic-item payload. The FIRST string per key is canonical (Add keeps the @@ -78,7 +78,7 @@ void InitObjVals() { } /// -/// Общий инит системы текстовых ИД, дергается при старте мада. +/// , . /// void Init() { /// OBJ_VALS @@ -87,8 +87,8 @@ void Init() { } /// -/// Конвертирование текстового ИД константы в ее значение в коде. -/// \return значение константы или -1, если ничего не было найдено +/// . +/// \return -1, /// int ToNum(EIdType type, const std::string &str) { if (type < kTextIdCount) { @@ -98,8 +98,8 @@ int ToNum(EIdType type, const std::string &str) { } /// -/// Конвертирование значения константы в ее текстовый ИД. -/// \return текстовый ИД константы или пустая строка, если ничего не было найдено +/// . +/// \return , /// std::string ToStr(EIdType type, int num) { if (type < kTextIdCount) { @@ -109,7 +109,7 @@ std::string ToStr(EIdType type, int num) { } /// -/// Добавление соответствия значение=константа/константа=значение +/// =/= /// void TextIdNode::Add(int num, std::string str) { num_to_str.insert(std::make_pair(num, str)); @@ -117,7 +117,7 @@ void TextIdNode::Add(int num, std::string str) { } /// -/// Конвертирование значение -> текстовый ИД +/// -> /// std::string TextIdNode::ToStr(int num) const { auto i = num_to_str.find(num); @@ -128,7 +128,7 @@ std::string TextIdNode::ToStr(int num) const { } /// -/// Конвертирование текстовый ИД -> значение +/// -> /// int TextIdNode::ToNum(const std::string &str) const { auto i = str_to_num.find(str); @@ -143,8 +143,8 @@ int TextIdNode::ToNum(const std::string &str) const { namespace parse { char buf[kMaxStringLength]; /// -/// Попытка конвертирования \param text в с перехватом исключения -/// \return число или -1, в случае неудачи +/// \param text +/// \return -1, /// int CastToInt(const char *text) { int result = -1; @@ -160,10 +160,10 @@ int CastToInt(const char *text) { } /// -/// Обертка на pugixml для чтения числового аттрибута -/// с логирование в имм- и сислог -/// В конфиге это выглядит как -/// \return -1 в случае неудачи +/// pugixml +/// - +/// +/// \return -1 int ReadAttrAsInt(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); if (!attr) { @@ -173,7 +173,7 @@ int ReadAttrAsInt(const pugi::xml_node &node, const char *text) { return CastToInt(attr.value()); } -// тоже самое, что и attr_int, только, если элемента нет, возвращает -1 +// , attr_int, , , -1 int ReadAttrAsIntT(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); if (!attr) { @@ -183,8 +183,8 @@ int ReadAttrAsIntT(const pugi::xml_node &node, const char *text) { } /// -/// Тоже, что и ReadAttrAsInt, только для чтения child_value() -/// В конфиге это выглядит как 1234 +/// , ReadAttrAsInt, child_value() +/// 1234 /// int ReadChildValueAsInt(const pugi::xml_node &node, const char *text) { pugi::xml_node child_node = node.child(text); @@ -196,7 +196,7 @@ int ReadChildValueAsInt(const pugi::xml_node &node, const char *text) { } /// -/// Аналог ReadAttrAsInt, \return строку со значением или пустую строку +/// ReadAttrAsInt, \return /// std::string ReadAattrAsStr(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); @@ -210,7 +210,7 @@ std::string ReadAattrAsStr(const pugi::xml_node &node, const char *text) { } /// -/// Аналог ReadChildValueAsInt, \return строку со значением или пустую строку +/// ReadChildValueAsInt, \return /// std::string ReadChildValueAsStr(const pugi::xml_node &node, const char *text) { pugi::xml_node child_node = node.child(text); @@ -243,8 +243,8 @@ pugi::xml_node GetChild(const pugi::xml_node &node, const char *name) { } /// -/// проверка валидности внума объекта с логированием ошибки в имм и сислог -/// \return true - если есть прототип объекта (рнум) с данным внумом +/// +/// \return true - () /// bool IsValidObjVnum(int vnum) { if (GetObjRnum(vnum) < 0) { @@ -259,8 +259,8 @@ bool IsValidObjVnum(int vnum) { // ===================================================================================================================== /** - * Прочитать значение value как строку. - * Ecxeption: если строка пуста, сообщение "string is empty"; + * value . + * Ecxeption: , "string is empty"; */ const char *ReadAsStr(const char *value) { if (strcmp(value, "") == 0) { @@ -270,8 +270,8 @@ const char *ReadAsStr(const char *value) { } /** - * Прочитать значение value как int. - * Ecxeption: при неудаче, сообщение - содержимое value. + * value int. + * Ecxeption: , - value. */ int ReadAsInt(const char *value) { try { @@ -282,7 +282,7 @@ int ReadAsInt(const char *value) { } /** - * Прочитать значение value как массив int, разделенный 'I' и записать знаения в num_set. + * value int, 'I' num_set. */ void ReadAsIntSet(std::unordered_set &num_set, const char *value) { if (strcmp(value, "") == 0) { @@ -299,8 +299,8 @@ void ReadAsIntSet(std::unordered_set &num_set, const char *value) { } /** - * Прочитать значение value как float. - * Ecxeption: при неудаче, сообщение - содержимое value. + * value float. + * Ecxeption: , - value. */ float ReadAsFloat(const char *value) { try { @@ -311,8 +311,8 @@ float ReadAsFloat(const char *value) { } /** - * Прочитать значение value как double. - * Ecxeption: при неудаче, сообщение - содержимое value. + * value double. + * Ecxeption: , - value. */ double ReadAsDouble(const char *value) { try { @@ -323,9 +323,9 @@ double ReadAsDouble(const char *value) { } /** - * Прочитать значение value как bool. - * Возвращает true, если значение "1", "true", "T", "t", "Y" или "y" и false в ином случае. - * Ecxeption: если value пусто, сообщение - "value is empty". + * value bool. + * true, "1", "true", "T", "t", "Y" "y" false . + * Ecxeption: value , - "value is empty". */ bool ReadAsBool(const char *value) { if (strcmp(value, "") == 0) { @@ -335,9 +335,9 @@ bool ReadAsBool(const char *value) { || strcmp(value, "T") == 0 || strcmp(value, "y") == 0 || strcmp(value, "Y") == 0); } -// issue.xml-parse-cleaning: единые толерантные читатели атрибутов DataNode (заменяют -// локальные AttrInt/AttrStr, ранее продублированные в загрузчиках). Пустой/отсутствующий/ -// неразобранный атрибут -> def. +// issue.xml-parse-cleaning: DataNode ( +// AttrInt/AttrStr, ). // +// -> def. int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { const char *v = node.GetValue(key); if (!v || !*v) { @@ -352,11 +352,11 @@ int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { std::string AttrStr(const parser_wrapper::DataNode &node, const char *key, const char *def) { const char *v = node.GetValue(key); - // Перекодировки здесь НЕТ и быть не должно: DataNode приводит содержимое файла к нативной - // кодировке один раз на документ, поэтому значение уже нативное. Пока перекодировка стояла - // ещё и здесь, каждое поле переводилось дважды, а так как конфиги движок ещё и пишет - // обратно, файл рос на каждой загрузке -- cfg/mechanics/obj_sets.xml так дорос со 120 КБ - // до 6,7 ГБ и убивал загрузку по памяти (issue #3681). + // : DataNode + // , . + // ݣ , , ݣ + // , -- cfg/mechanics/obj_sets.xml 120 + // 6,7 (issue #3681). return (v && *v) ? std::string(v) : std::string(def); } @@ -469,7 +469,7 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { beg_idx = in_buffer.find_first_not_of(' '); if (beg_idx != std::string::npos) { - // случай с кавычками + // if (in_buffer[beg_idx] == '\'') { if (std::string::npos != (beg_idx = in_buffer.find_first_not_of('\'', beg_idx))) { if (std::string::npos == (end_idx = in_buffer.find_first_of('\'', beg_idx))) { @@ -480,7 +480,7 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { in_buffer.erase(0, ++end_idx); } } - // случай с одним параметром через пробел + // } else { if (std::string::npos != (beg_idx = in_buffer.find_first_not_of(' ', beg_idx))) { if (std::string::npos == (end_idx = in_buffer.find_first_of(' ', beg_idx))) { @@ -499,14 +499,14 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { out_buffer.clear(); } -// регистронезависимое сравнение двух строк по длине первой, флаг - для учета длины строк (неравенство) +// , - () bool CompareParam(const std::string &buffer, const char *str, bool full) { if (!str || !*str || buffer.empty() || (full && buffer.length() != strlen(str))) { return false; } - // Посимвольно, а не побайтово: под UTF-8 регистр русской буквы по одному байту не берётся - // (issue #3681). Шаг обеих строк -- на длину символа, они здесь всегда в одной кодировке. + // , : UTF-8 ң + // (issue #3681). -- , . std::string::size_type i; for (i = 0; i != buffer.length() && *str;) { if (!native_text::chars_equal_ci(buffer.c_str() + i, str)) { @@ -524,14 +524,14 @@ bool CompareParam(const std::string &buffer, const char *str, bool full) { } } -// тоже самое с обоими аргументами стринг +// bool CompareParam(const std::string &buffer, const std::string &buffer2, bool full) { if (buffer.empty() || buffer2.empty() || (full && buffer.length() != buffer2.length())) { return false; } - // Посимвольно, как и в перегрузке выше (issue #3681). + // , (issue #3681). std::string::size_type i; for (i = 0; i != buffer.length() && i != buffer2.length();) { if (!native_text::chars_equal_ci(buffer.c_str() + i, buffer2.c_str() + i)) { diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 6668359c6..2eeae02f7 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -117,7 +117,7 @@ bool IsAbbr(const char *arg1, const char *arg2) { return false; } - // Посимвольно (issue #3681): побайтное сравнение под UTF-8 теряет регистронезависимость. + // (issue #3681): UTF-8 . while (*arg1 && *arg2) { if (!native_text::chars_equal_ci(arg1, arg2)) { return false; @@ -200,10 +200,10 @@ std::vector Split(const std::string s, char delimiter) { TrimLeft(token); tokens.push_back(token); } - return tokens; //если разделитель не найден вернется 1 элемент содержащий полную строку + return tokens; // 1 } -// первое слово разделенное маской +// std::string FirstWordOnString(std::string s, std::string mask) { int pos = s.find_first_of(mask); if (pos > 0) @@ -211,9 +211,9 @@ std::string FirstWordOnString(std::string s, std::string mask) { return s; } -// аналог one_argument для string -// пропускает ведущие пробелы, возвращает первое слово, в remains остаток после пробела -// безопасно вызывать как ExtractFirstArgument(str, str) - нет проблем с алиасингом +// one_argument string +// , , remains +// ExtractFirstArgument(str, str) - std::string ExtractFirstArgument(const std::string &s, std::string &remains) { auto start = s.find_first_not_of(' '); if (start == std::string::npos) { @@ -267,8 +267,8 @@ void ConvertToLow(char *text) { } std::string SubstStrToLow(std::string s) { - // NB: имя говорит "ToLow", а тело поднимает регистр. Расхождение предсуществующее, - // поведение сохранено намеренно -- меняется только байтовая семантика на символьную. + // NB: "ToLow", . , + // -- . native_text::to_upper(s); return s; } @@ -365,11 +365,11 @@ std::string FixDot(std::string s) { return s; } -// Сортировка идёт по ключу, а не по перекодированной на месте строке. Прежний трюк -- перегнать -// список в Windows-1251 (там русские буквы стоят по алфавиту, в отличие от KOI8-R), отсортировать -// и перегнать обратно -- под UTF-8 портил текст: побайтовая таблица KOI8-R -> Windows-1251 для -// многобайтовой строки не значит ничего, и обратное преобразование уже не восстанавливало исходник -// (issue #3681). native_text::sort_key даёт ровно тот же порядок, ничего не меняя в самих строках. +// ģ , . -- +// Windows-1251 ( , KOI8-R), +// -- UTF-8 : KOI8-R -> Windows-1251 +// , +// (issue #3681). native_text::sort_key , . void SortKoiString(std::vector &str) { std::sort(str.begin(), str.end(), [](const std::string &a, const std::string &b) { return native_text::sort_key(a) < native_text::sort_key(b); @@ -442,9 +442,9 @@ std::string CompressSymbol(std::string s, const char ch) { const char *first_letter(const char *txt) { if (txt) { while (*txt && !a_isalpha(*txt)) { - //Предполагается, что для отправки клиенту используется только управляющий код с цветом - //На данный момент в коде присутствует только еще один управляющий код для очистки экрана, - //но он не используется (см. CLEAR_SCREEN) + //, + // , + // (. CLEAR_SCREEN) if ('\x1B' == *txt) { while (*txt && 'm' != *txt) { ++txt; @@ -830,9 +830,9 @@ char *str_str(const char *cs, const char *ct) { if (!cs || !ct) { return nullptr; } - // Сравниваем и шагаем ПО СИМВОЛАМ: под UTF-8 побайтовое приведение регистра для кириллицы - // неверно (таблица регистра -- байтовая, KOI8-R), и поиск подстроки то не находил совпадения, - // то находил ложные на границе байтов (issue #3681). + // : UTF-8 + // ( -- , KOI8-R), , + // (issue #3681). while (*cs) { const char *t = ct; while (*cs && !native_text::chars_equal_ci(cs, t)) { @@ -1080,24 +1080,24 @@ void kill_ems(std::string &str) { std::string utils::OutWordsList(const std::vector &words, size_t max_length, const std::string &separator, const std::string &prefix) { - // prefix печатается один раз и занимает место на первой строке, но не - // склеивается через separator (first остаётся true -- первое слово идёт - // сразу за префиксом без ", "). Видимую длину считаем без цветокодов. + // prefix , + // separator (first true -- ģ + // ", "). . std::string result = prefix; size_t line_length = GetStringWithoutColors(prefix).size(); bool first = true; - // separator -- это и есть то, что стоит между словами на одной строке - // (", " для списка, " " для обычного переноса по словам). На переносе - // строки хвостовые пробелы у разделителя срезаем, чтобы не оставлять их - // перед \r\n (для "," получаем ",\r\n", для " " -- просто "\r\n"). + // separator -- , + // (", " , " " ). + // , + // \r\n ( "," ",\r\n", " " -- "\r\n"). std::string eol_separator = separator; while (!eol_separator.empty() && eol_separator.back() == ' ') { eol_separator.pop_back(); } for (const auto &word : words) { - // ширину считаем по видимой длине -- цветокоды (&R, &n и т.п.) на экране - // места не занимают, иначе строки с цветом переносятся раньше времени + // -- (&R, &n ..) + // , const size_t word_len = GetStringWithoutColors(word).size(); if (!first) { if (line_length + separator.size() + word_len > max_length) { @@ -1131,17 +1131,17 @@ std::string utils::WrapText(const std::string &text, size_t max_length) { std::istringstream stream(text); std::string line; bool first = true; - // разбиваем по '\n', каждую строку переносим отдельно -- авторские - // переносы и пустые строки (абзацы) сохраняются + // '\n', -- + // () while (std::getline(stream, line)) { - if (!line.empty() && line.back() == '\r') { // срезаем хвостовой '\r' от \r\n + if (!line.empty() && line.back() == '\r') { // '\r' \r\n line.pop_back(); } if (!first) { result += "\r\n"; } first = false; - // max_length == 0 -- без переноса; иначе пустая строка вернётся пустой + // max_length == 0 -- ; Σ result += (max_length == 0) ? line : OutWordsList(line, max_length, " "); } return result; @@ -1162,21 +1162,21 @@ std::string sprintGender(int gender_value) { // ---- name/file sanitization + number formatting (moved from interpreter.cpp, // issue.interpreter-cleaning Bucket 4) ---- -// замена в name русских символов на англ в нижнем регистре (для файлов) +// name ( ) void CreateFileName(std::string &name) { - // Имя файла собирается по байтам KOI8-R -- ровно так, как собиралось до миграции. Иначе у - // уже существующего персонажа доска получит другое имя файла, и старая просто потеряется: - // под UTF-8 байтовый AtoL резал русскую букву пополам и оставлял её в имени как есть, отчего - // рядом со 105 старыми досками завелось 70 новых, с кириллицей в имени (issue #3681). + // KOI8-R -- , . + // , : + // UTF-8 AtoL ţ , + // 105 70 , (issue #3681). // - // Приводим строку к KOI8-R и дальше работаем прежним байтовым кодом: под KOI8-R to_koi8 -- - // тождество, так что поведение там не меняется ни на байт. + // KOI8-R : KOI8-R to_koi8 -- + // , . name = native_text::to_koi8(name); for (unsigned i = 0; i != name.length(); ++i) name[i] = LOWER(codepages::AtoL(name[i])); } -// вывод экспы аля диабла +// std::string ExpFormat(long long exp) { std::string prefix; if (exp < 0) { @@ -1186,20 +1186,20 @@ std::string ExpFormat(long long exp) { if (exp < 1000000) return (prefix + fmt::format("{}", exp)); else if (exp < 1000000000) - return (prefix + fmt::format("{}", exp / 1000) + " тыс"); + return (prefix + fmt::format("{}", exp / 1000) + " "); else if (exp < 1000000000000LL) - return (prefix + fmt::format("{}", exp / 1000000) + " млн"); + return (prefix + fmt::format("{}", exp / 1000000) + " "); else - return (prefix + fmt::format("{}", exp / 1000000000LL) + " млрд"); + return (prefix + fmt::format("{}", exp / 1000000000LL) + " "); } -// * Конвертация имени в нижний регистр + первый сивмол в верхний (для единообразного поиска в контейнерах) +// * + ( ) void name_convert(std::string &text) { if (!text.empty()) { utils::ConvertToLow(text); - // Первую букву поднимаем целиком, а не первый байт: под UTF-8 у русской буквы их два, - // и байтовый UPPER превращал имя в мусор -- "имя щщщщщ одобрить" не находило - // персонажа (issue #3681). + // , : UTF-8 , + // UPPER -- " " + // (issue #3681). native_text::capitalize_first(text); } } From da6824c275d754de054b765dc59ba744617f7fae Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Sun, 9 Aug 2026 21:36:58 +0200 Subject: [PATCH 056/110] =?UTF-8?q?refactor(utf8):=20=D0=B2=D1=8B=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD=D1=83=D1=82=D1=8C=20=D0=BC=D1=91=D1=80=D1=82=D0=B2?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После флипа INTERNAL_ENCODING_UTF8 определён всегда, значит вторая половина каждой пары реализаций -- мёртвый код. Байтовый вариант скомпилировать уже нельзя: собрать его из UTF-8-исходников не получится, а держать в дереве код, который никто не собирает и не проверяет, -- худший вид долга. Убраны все #ifdef INTERNAL_ENCODING_UTF8 вместе с #else-ветками: native_text похудел на 200 строк, russian_keys на 46 (осталась таблица кодовых точек, без байтов KOI8-R), table_wrapper -- на выбор между char_table и utf8_table. Заодно свёрнуты рантайм-развилки по native_is_utf8(): восемь сравнений строк в utils_string (за проверкой шёл недостижимый побайтовый цикл), приведение ввода к нативной кодировке, отдача UTF-8-клиенту и подъём буфера в diskio. native_is_utf8() пока остался: на нём ещё ветвятся ожидания в тестах, это следующий заход. --- src/engine/core/iosystem.cpp | 19 +-- src/engine/network/descriptor_data.cpp | 10 +- src/engine/ui/table_wrapper.h | 4 - src/utils/diskio.cpp | 4 +- src/utils/native_text.cpp | 198 ------------------------- src/utils/russian_keys.h | 46 ------ src/utils/utils_string.cpp | 108 +------------- 7 files changed, 14 insertions(+), 375 deletions(-) diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index 84f096c01..a428506d1 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -454,24 +454,7 @@ int process_input(DescriptorData *t) { // Приводим собранную строку к нативной кодировке движка (issue #3681). После разбора // выше в tmp лежит либо UTF-8 (клиент UTF-8), либо KOI8-R (все остальные кодировки // клиентов - их таблицы отдают именно KOI8-R). - if (t->keytable == kCodePageUTF8) { - if (!native_text::native_is_utf8()) { - int i; - char utf8_tmp[kMaxSockBuf * 2 * 3]; - size_t len_i, len_o; - - len_i = strlen(tmp); - - 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; - } - // иначе клиент прислал уже нативную кодировку - трогать нечего - } else if (native_text::native_is_utf8()) { + if (t->keytable != kCodePageUTF8) { const size_t len_i = strlen(tmp); const std::string native = native_text::from_koi8(tmp); strncpy(tmp, native.c_str(), kMaxInputLength - 1); diff --git a/src/engine/network/descriptor_data.cpp b/src/engine/network/descriptor_data.cpp index bb385928f..2b01b4358 100644 --- a/src/engine/network/descriptor_data.cpp +++ b/src/engine/network/descriptor_data.cpp @@ -217,13 +217,9 @@ void DescriptorData::string_to_client_encoding(const char *in_str, char *out_str // Anton Gorev (2016-04-25): we have to be careful. String in UTF-8 encoding may // contain character with code 0xff which telnet interprets as IAC. // II: FE and FF were never defined for any purpose in UTF-8, we are safe - if (native_text::native_is_utf8()) { - // Рантайм уже в UTF-8 - отдаём как есть. Повторная перекодировка испортила бы - // текст (именно так и выглядела первая флип-сборка). - strcpy(out_str, in_str); - } else { - codepages::koi_to_utf8(const_cast(in_str), out_str); - } + // Рантайм в UTF-8 - отдаём как есть. Перекодировка тут испортила бы текст + // (именно так и выглядела первая флип-сборка). + strcpy(out_str, in_str); break; default: diff --git a/src/engine/ui/table_wrapper.h b/src/engine/ui/table_wrapper.h index e3a36238d..0fae67810 100644 --- a/src/engine/ui/table_wrapper.h +++ b/src/engine/ui/table_wrapper.h @@ -46,11 +46,7 @@ using TextAlign = fort::text_align; * точками. Под KOI8-R (1 байт = 1 символ) верен первый, под UTF-8 - второй; при неверном выборе * колонки с русским текстом съезжают вдвое. */ -#ifdef INTERNAL_ENCODING_UTF8 using TableBase = fort::utf8_table; -#else -using TableBase = fort::char_table; -#endif /** * Таблица в нативной кодировке движка. diff --git a/src/utils/diskio.cpp b/src/utils/diskio.cpp index a4af5eb95..902407e4b 100644 --- a/src/utils/diskio.cpp +++ b/src/utils/diskio.cpp @@ -155,8 +155,8 @@ FBFILE *fbopen_for_read(char *fname) { // Данные на диске лежат в KOI8-R. Поднимаем весь буфер до нативной кодировки движка здесь, // где размер под нашим контролем -- построчная конверсия рисковала бы переполнить буфер // вызывающего, ведь кириллица при перекодировке удлиняется. Исходные байты остаются в raw - // для CRC. Под KOI8-R ветка не компилируется вовсе, buf и raw -- один и тот же указатель. - if (native_text::native_is_utf8()) { + // для CRC. + { const std::string native = native_text::from_disk_line(fbfl->buf); if (native.size() != static_cast(fbfl->size)) { char *converted = nullptr; diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 535e53517..84142f436 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -14,21 +14,7 @@ through them changes nothing until the encoding flip. #include "utils_encoding.h" #include "translit_koi8.h" -#ifdef INTERNAL_ENCODING_UTF8 #include "utf8.h" -#else -// The KOI8-R case tables (defined in utils.cpp). Declared directly instead of including utils.h, -// which drags in fmt/ and much of the engine for what is just two 256-byte lookups. -extern const char a_ucc_table[]; -extern const char a_lcc_table[]; -extern const bool a_isalnum_table[]; -extern const bool a_isalpha_table[]; -extern const bool a_isupper_table[]; - -// Yo is absent from the Latin table and always had a special case in get_filename(). -constexpr unsigned char kYoLowerByte = 0xA3; -constexpr unsigned char kYoUpperByte = 0xB3; -#endif #include #include @@ -37,7 +23,6 @@ constexpr unsigned char kYoUpperByte = 0xB3; namespace native_text { -#ifdef INTERNAL_ENCODING_UTF8 bool native_is_utf8() { return true; @@ -502,167 +487,6 @@ std::string translit_to_filename(std::string_view name) { return out; } -#else // KOI8-R: 1 byte == 1 character - -bool native_is_utf8() { - return false; -} - -std::size_t char_count(const char *begin, const char *end) { - return static_cast(end - begin); -} - -std::size_t char_count(std::string_view s) { - return s.size(); -} - -void capitalize_first(char *s) { - if (s != nullptr && *s != '\0') { - *s = a_ucc_table[static_cast(*s)]; - } -} - -std::size_t truncate_offset(std::string_view s, std::size_t max_bytes) { - return max_bytes < s.size() ? max_bytes : s.size(); -} - -std::size_t char_bytes(const char *) { - return 1; -} - -namespace { - -// Byte-wise fold-and-subtract, identical to the open-coded `LOWER(a[i]) - LOWER(b[i])` loops in -// utils_string.cpp: the magnitude of the result (not just its sign) is preserved, since some -// callers propagate it. A string that ended compares as LOWER('\0') against the other's byte. -int compare_bytes(std::string_view a, std::string_view b, std::size_t limit) { - std::size_t i = 0; - while (true) { - if (limit != std::string_view::npos && i >= limit) { - return 0; - } - const bool a_end = i >= a.size(); - const bool b_end = i >= b.size(); - if (a_end && b_end) { - return 0; - } - const unsigned char ca = a_end ? '\0' : static_cast(a[i]); - const unsigned char cb = b_end ? '\0' : static_cast(b[i]); - const int chk = a_lcc_table[ca] - a_lcc_table[cb]; - if (chk != 0) { - return chk; - } - ++i; - } -} - -} // namespace - -int compare_ci(std::string_view a, std::string_view b) { - return compare_bytes(a, b, std::string_view::npos); -} - -int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { - return compare_bytes(a, b, n); -} - -bool is_alnum_char(const char *s) { - return a_isalnum_table[static_cast(*s)]; -} - -bool is_alpha_char(const char *s) { - return a_isalpha_table[static_cast(*s)]; -} - -bool is_upper_char(const char *s) { - return a_isupper_table[static_cast(*s)]; -} - -bool chars_equal_ci(const char *a, const char *b) { - return a_lcc_table[static_cast(*a)] == a_lcc_table[static_cast(*b)]; -} - -std::size_t copy_lower_char(const char *src, char *dst) { - *dst = a_lcc_table[static_cast(*src)]; - return 1; -} - -std::size_t copy_upper_char(const char *src, char *dst) { - *dst = a_ucc_table[static_cast(*src)]; - return 1; -} - - -char32_t first_char_code(const char *s) { - return (s == nullptr || *s == '\0') ? 0 : static_cast(*s); -} - -char32_t first_char_code_lower(const char *s) { - return (s == nullptr || *s == '\0') - ? 0 : static_cast(a_lcc_table[static_cast(*s)]); -} - -char32_t first_char_code_upper(const char *s) { - return (s == nullptr || *s == '\0') - ? 0 : static_cast(a_ucc_table[static_cast(*s)]); -} - -void to_lower(std::string &s) { - for (char &c : s) { - c = a_lcc_table[static_cast(c)]; - } -} - -void to_upper(std::string &s) { - for (char &c : s) { - c = a_ucc_table[static_cast(c)]; - } -} - -void to_lower(char *s) { - for (; *s; ++s) { - *s = a_lcc_table[static_cast(*s)]; - } -} - -void to_upper(char *s) { - for (; *s; ++s) { - *s = a_ucc_table[static_cast(*s)]; - } -} - - -std::string from_koi8(const std::string &text) { - return text; // the native encoding already is KOI8-R -} - -std::string to_koi8(const std::string &text) { - return text; // the native encoding already is KOI8-R -} - -std::string from_utf8(const std::string &text) { - // The native encoding is KOI8-R, so this is the real conversion: reduce what KOI8-R lacks - // (see translit_koi8.h), then transcode. - return codepages::Utf8ToKoi8(text); -} - -std::string translit_to_filename(std::string_view name) { - // Byte-wise, exactly as get_filename() did before this was extracted: transliterate through - // the Latin table, then lowercase. Yo is not in that table and had its own special case. - std::string out; - out.reserve(name.size()); - for (const char ch : name) { - const unsigned char b = static_cast(ch); - if (b == kYoLowerByte || b == kYoUpperByte) { - out.push_back('9'); - } else { - out.push_back(a_lcc_table[static_cast(codepages::AtoL(ch))]); - } - } - return out; -} - -#endif // --------------------------------------------------------------------------------------------- // Encoding-independent helpers: expressed purely in terms of the primitives above, so they need @@ -690,7 +514,6 @@ inline std::size_t lead_len_shared(unsigned char c) { // Byte length of the character at `pos`, clamped to the end of `s`. std::size_t char_bytes_at(std::string_view s, std::size_t pos) { -#ifdef INTERNAL_ENCODING_UTF8 const unsigned char lead = static_cast(s[pos]); if (lead < 0x80) { return 1; @@ -701,11 +524,6 @@ std::size_t char_bytes_at(std::string_view s, std::size_t pos) { ++n; } return n; -#else - (void) s; - (void) pos; - return 1; -#endif } } // namespace @@ -749,34 +567,22 @@ bool list_contains_char(std::string_view list, std::string_view ch) { } std::string from_disk_line(const char *line) { -#ifdef INTERNAL_ENCODING_UTF8 if (line == nullptr || *line == '\0') { return {}; } const std::string_view view(line); return utf8::is_valid(view) ? std::string(view) : from_koi8(std::string(view)); -#else - return line == nullptr ? std::string() : std::string(line); -#endif } std::string from_disk_text(const std::string &text) { -#ifdef INTERNAL_ENCODING_UTF8 // Same discriminator as from_disk_line, applied to the whole file: well-formed UTF-8 is taken // as already native, anything else as KOI8-R. Cyrillic in KOI8-R is almost never valid UTF-8, // which makes validity a reliable test, and it is what keeps a load/save cycle idempotent. return utf8::is_valid(text) ? text : from_koi8(text); -#else - return text; -#endif } std::size_t char_offset(std::string_view s, std::size_t chars) { -#ifdef INTERNAL_ENCODING_UTF8 return utf8::byte_offset(s, chars); -#else - return std::min(chars, s.size()); -#endif } std::string pad_right(std::string_view s, std::size_t width) { @@ -789,11 +595,7 @@ std::string pad_right(std::string_view s, std::size_t width) { } std::string to_disk(const std::string &text) { -#ifdef INTERNAL_ENCODING_UTF8 return to_koi8(text); -#else - return text; -#endif } std::string read_data_file(const std::string &path) { diff --git a/src/utils/russian_keys.h b/src/utils/russian_keys.h index 85f21c368..86cef4852 100644 --- a/src/utils/russian_keys.h +++ b/src/utils/russian_keys.h @@ -29,24 +29,13 @@ U'...' literals. namespace rus { -#ifdef INTERNAL_ENCODING_UTF8 // Unicode code points: U+0410..U+042F (upper), U+0430..U+044F (lower), U+0401/U+0451 (Yo). #define BYLINS_RUS_LETTER(name, upper_cp, lower_cp) \ constexpr char32_t name##Upper = upper_cp; \ constexpr char32_t name = lower_cp -#else -// KOI8-R byte values. The alphabet is not laid out alphabetically in KOI8-R, so every letter is -// spelled out rather than derived from an offset. -#define BYLINS_RUS_LETTER(name, upper_byte, lower_byte) \ - constexpr char32_t name##Upper = upper_byte; \ - constexpr char32_t name = lower_byte - -#endif - -#ifdef INTERNAL_ENCODING_UTF8 BYLINS_RUS_LETTER(kA, 0x0410, 0x0430); // А а BYLINS_RUS_LETTER(kBe, 0x0411, 0x0431); // Б б BYLINS_RUS_LETTER(kVe, 0x0412, 0x0432); // В в @@ -80,41 +69,6 @@ BYLINS_RUS_LETTER(kSoft,0x042C, 0x044C); // Ь ь BYLINS_RUS_LETTER(kE, 0x042D, 0x044D); // Э э BYLINS_RUS_LETTER(kYu, 0x042E, 0x044E); // Ю ю BYLINS_RUS_LETTER(kYa, 0x042F, 0x044F); // Я я -#else -BYLINS_RUS_LETTER(kA, 0xE1, 0xC1); // А а -BYLINS_RUS_LETTER(kBe, 0xE2, 0xC2); // Б б -BYLINS_RUS_LETTER(kVe, 0xF7, 0xD7); // В в -BYLINS_RUS_LETTER(kGe, 0xE7, 0xC7); // Г г -BYLINS_RUS_LETTER(kDe, 0xE4, 0xC4); // Д д -BYLINS_RUS_LETTER(kIe, 0xE5, 0xC5); // Е е -BYLINS_RUS_LETTER(kYo, 0xB3, 0xA3); // Ё ё -BYLINS_RUS_LETTER(kZhe, 0xF6, 0xD6); // Ж ж -BYLINS_RUS_LETTER(kZe, 0xFA, 0xDA); // З з -BYLINS_RUS_LETTER(kI, 0xE9, 0xC9); // И и -BYLINS_RUS_LETTER(kIi, 0xEA, 0xCA); // Й й -BYLINS_RUS_LETTER(kKa, 0xEB, 0xCB); // К к -BYLINS_RUS_LETTER(kEl, 0xEC, 0xCC); // Л л -BYLINS_RUS_LETTER(kEm, 0xED, 0xCD); // М м -BYLINS_RUS_LETTER(kEn, 0xEE, 0xCE); // Н н -BYLINS_RUS_LETTER(kO, 0xEF, 0xCF); // О о -BYLINS_RUS_LETTER(kPe, 0xF0, 0xD0); // П п -BYLINS_RUS_LETTER(kEr, 0xF2, 0xD2); // Р р -BYLINS_RUS_LETTER(kEs, 0xF3, 0xD3); // С с -BYLINS_RUS_LETTER(kTe, 0xF4, 0xD4); // Т т -BYLINS_RUS_LETTER(kU, 0xF5, 0xD5); // У у -BYLINS_RUS_LETTER(kEf, 0xE6, 0xC6); // Ф ф -BYLINS_RUS_LETTER(kHa, 0xE8, 0xC8); // Х х -BYLINS_RUS_LETTER(kTse, 0xE3, 0xC3); // Ц ц -BYLINS_RUS_LETTER(kChe, 0xFE, 0xDE); // Ч ч -BYLINS_RUS_LETTER(kSha, 0xFB, 0xDB); // Ш ш -BYLINS_RUS_LETTER(kScha,0xFD, 0xDD); // Щ щ -BYLINS_RUS_LETTER(kHard,0xFF, 0xDF); // Ъ ъ -BYLINS_RUS_LETTER(kYery,0xF9, 0xD9); // Ы ы -BYLINS_RUS_LETTER(kSoft,0xF8, 0xD8); // Ь ь -BYLINS_RUS_LETTER(kE, 0xFC, 0xDC); // Э э -BYLINS_RUS_LETTER(kYu, 0xE0, 0xC0); // Ю ю -BYLINS_RUS_LETTER(kYa, 0xF1, 0xD1); // Я я -#endif #undef BYLINS_RUS_LETTER diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 2eeae02f7..7d729ae6c 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -574,151 +574,59 @@ char *delete_doubledollar(char *string) { // byte-wise LOWER() loop kept verbatim below, under UTF-8 it is native_text's code-point fold // (issue #3681). The KOI8-R path is untouched so behaviour is bit-identical until the flip. int str_cmp(const char *arg1, const char *arg2) { - int chk, i; if (arg1 == nullptr || arg2 == nullptr) { log("SYSERR: str_cmp() passed a nullptr pointer, %p or %p.", arg1, arg2); return (0); } - if (native_text::native_is_utf8()) { - return native_text::compare_ci(arg1, arg2); - } - for (i = 0; arg1[i] || arg2[i]; i++) - if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) - return (chk); - return (0); + return native_text::compare_ci(arg1, arg2); } int str_cmp(const std::string &arg1, const char *arg2) { - int chk; - std::string::size_type i; if (arg2 == nullptr) { log("SYSERR: str_cmp() passed a NULL pointer, %p.", arg2); return (0); } - if (native_text::native_is_utf8()) { - return native_text::compare_ci(arg1, arg2); - } - for (i = 0; i != arg1.length() && *arg2; i++, arg2++) - if ((chk = LOWER(arg1[i]) - LOWER(*arg2)) != 0) - return (chk); - if (i == arg1.length() && !*arg2) - return (0); - if (*arg2) - return (LOWER('\0') - LOWER(*arg2)); - else - return (LOWER(arg1[i]) - LOWER('\0')); + return native_text::compare_ci(arg1, arg2); } int str_cmp(const char *arg1, const std::string &arg2) { - int chk; - std::string::size_type i; if (arg1 == nullptr) { log("SYSERR: str_cmp() passed a NULL pointer, %p.", arg1); return (0); } - if (native_text::native_is_utf8()) { - return native_text::compare_ci(arg1, arg2); - } - for (i = 0; *arg1 && i != arg2.length(); i++, arg1++) - if ((chk = LOWER(*arg1) - LOWER(arg2[i])) != 0) - return (chk); - if (!*arg1 && i == arg2.length()) - return (0); - if (*arg1) - return (LOWER(*arg1) - LOWER('\0')); - else - return (LOWER('\0') - LOWER(arg2[i])); + return native_text::compare_ci(arg1, arg2); } int str_cmp(const std::string &arg1, const std::string &arg2) { - int chk; - std::string::size_type i; - if (native_text::native_is_utf8()) { - return native_text::compare_ci(arg1, arg2); - } - for (i = 0; i != arg1.length() && i != arg2.length(); i++) - if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) - return (chk); - if (arg1.length() == arg2.length()) - return (0); - if (i == arg1.length()) - return (LOWER('\0') - LOWER(arg2[i])); - else - return (LOWER(arg1[i]) - LOWER('\0')); + return native_text::compare_ci(arg1, arg2); } int strn_cmp(const char *arg1, const char *arg2, size_t n) { - int chk, i; if (arg1 == nullptr || arg2 == nullptr) { log("SYSERR: strn_cmp() passed a NULL pointer, %p or %p.", arg1, arg2); return (0); } - if (native_text::native_is_utf8()) { - return native_text::ncompare_ci(arg1, arg2, n); - } - for (i = 0; (arg1[i] || arg2[i]) && (n > 0); i++, n--) - if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) - return (chk); - return (0); + return native_text::ncompare_ci(arg1, arg2, n); } int strn_cmp(const std::string &arg1, const char *arg2, size_t n) { - int chk; - std::string::size_type i; if (arg2 == nullptr) { log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg2); return (0); } - if (native_text::native_is_utf8()) { - return native_text::ncompare_ci(arg1, arg2, n); - } - for (i = 0; i != arg1.length() && *arg2 && (n > 0); i++, arg2++, n--) - if ((chk = LOWER(arg1[i]) - LOWER(*arg2)) != 0) - return (chk); - if (i == arg1.length() && (!*arg2 || n == 0)) - return (0); - if (*arg2) - return (LOWER('\0') - LOWER(*arg2)); - else - return (LOWER(arg1[i]) - LOWER('\0')); + return native_text::ncompare_ci(arg1, arg2, n); } int strn_cmp(const char *arg1, const std::string &arg2, size_t n) { - int chk; - std::string::size_type i; if (arg1 == nullptr) { log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg1); return (0); } - if (native_text::native_is_utf8()) { - return native_text::ncompare_ci(arg1, arg2, n); - } - for (i = 0; *arg1 && i != arg2.length() && (n > 0); i++, arg1++, n--) - if ((chk = LOWER(*arg1) - LOWER(arg2[i])) != 0) - return (chk); - if (!*arg1 && (i == arg2.length() || n == 0)) - return (0); - if (*arg1) - return (LOWER(*arg1) - LOWER('\0')); - else - return (LOWER('\0') - LOWER(arg2[i])); + return native_text::ncompare_ci(arg1, arg2, n); } int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n) { - int chk; - std::string::size_type i; - if (native_text::native_is_utf8()) { - return native_text::ncompare_ci(arg1, arg2, n); - } - for (i = 0; i != arg1.length() && i != arg2.length() && (n > 0); i++, n--) - if ((chk = LOWER(arg1[i]) - LOWER(arg2[i])) != 0) - return (chk); - if (arg1.length() == arg2.length() || (n == 0)) - return (0); - if (i == arg1.length()) - return (LOWER('\0') - LOWER(arg2[i])); - else - return (LOWER(arg1[i]) - LOWER('\0')); + return native_text::ncompare_ci(arg1, arg2, n); } void StringReplace(std::string &buffer, char s, const std::string &d) { From 0100e084d47fe9f805c80354743bbae721db6472 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 05:40:48 +0200 Subject: [PATCH 057/110] =?UTF-8?q?refactor(utf8):=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20native=5Fis=5Futf8=20--=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B8=D0=B9=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=BA=20=D1=80=D0=B0=D0=B7=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=BA=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кодировка одна, спрашивать больше не о чем. Функция ушла из API, вместе с ней: - в тестах свёрнуты 16 ветвлений "если UTF-8 -- одно, иначе другое" и три GTEST_SKIP, которые теперь никогда не срабатывали; - удалён тест CompareCiMatchesLegacyByteLoopUnderKoi8r: он сверял байтовую ветку с исходным циклом на LOWER(), а байтовой ветки больше нет; - в таблицах русских букв и транслитерации остался только столбец UTF-8; - в выводе версии кодировка стала строкой, а не вызовом; - из meson убран -DINTERNAL_ENCODING_UTF8: его больше никто не читает. Заодно переписаны шапки native_text.h/.cpp -- они всё ещё объясняли устройство из двух реализаций, выбираемых макросом. --- meson.build | 6 - src/engine/scripting/dg_scripts.cpp | 2 +- src/utils/native_text.cpp | 9 +- src/utils/native_text.h | 25 ++- src/version.cpp.in | 11 +- tests/boards.encoding.cpp | 2 +- tests/native_text.cpp | 242 +++++++++------------------- tests/translit_koi8.cpp | 12 +- 8 files changed, 91 insertions(+), 218 deletions(-) diff --git a/meson.build b/meson.build index 1e12107ea..366768c20 100644 --- a/meson.build +++ b/meson.build @@ -112,12 +112,6 @@ if host_system == 'windows' endif endif -# issue #3681: движок работает с текстом в UTF-8 и посимвольно. Исходники тоже UTF-8 (см. -# .gitattributes), поэтому строковые литералы и рантайм совпадают по кодировке -- это одно -# состояние, а не два: собрать байтовый вариант из UTF-8-исходников нельзя, и опции для этого -# больше нет. Байтовые ветки под #else в native_text ещё остались и будут вычищены отдельно. -project_args += '-DINTERNAL_ENCODING_UTF8' - linker_choice = get_option('linker') if linker_choice != '' add_project_link_arguments('-fuse-ld=' + linker_choice, language: 'cpp') diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 07adb6ae2..21085f79f 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -1564,7 +1564,7 @@ void find_replacement(void *go, ObjData *tmp_obj = nullptr, *obj = nullptr; RoomData *tmp_room = nullptr, *room = nullptr; std::string name; - int num = 0, count = 0, i; + int num = 0, count = 0; char uid_type = '\0'; char tmp[kMaxTrglineLength] = {}; const char *send_cmd[] = {"msend", "osend", "wsend"}; diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 84142f436..98e0f57a4 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -2,9 +2,8 @@ \file native_text.cpp - a part of the Bylins engine. \brief Native-encoding character helpers declared in native_text.h (issue #3681). -Two implementations selected by the INTERNAL_ENCODING_UTF8 build macro. The KOI8-R branch is kept -byte-for-byte identical to the open-coded logic these helpers replace so that routing call sites -through them changes nothing until the encoding flip. +The hot paths (case folding, comparison) walk bytes directly instead of decoding to code points +and back: the straightforward version cost 17x on a string-heavy benchmark. */ #include "native_text.h" @@ -24,10 +23,6 @@ through them changes nothing until the encoding flip. namespace native_text { -bool native_is_utf8() { - return true; -} - std::size_t char_count(const char *begin, const char *end) { return utf8::length(std::string_view(begin, static_cast(end - begin))); } diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 962b41a1d..f8366684d 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -2,18 +2,15 @@ \file native_text.h - a part of the Bylins engine. \brief Character-semantic operations in the engine's *native runtime encoding* (issue #3681). -The migration flips the engine's internal string encoding from KOI8-R (1 byte == 1 character) to -UTF-8 (multibyte). Code that must reason about characters -- counting display width, capitalising -a letter, truncating without splitting a character -- should go through this thin dispatch layer -instead of assuming bytes. The active encoding is chosen at build time by the `internal_encoding` -Meson option, which defines INTERNAL_ENCODING_UTF8 for the UTF-8 build: - - * KOI8-R (default, current behaviour): every helper is byte-for-byte identical to the open-coded - byte logic it replaces, so routing call sites through it is a no-op. - * UTF-8: helpers operate on code points via the utf8:: primitives. - -This lets the whole byte-vs-char refactor land and ship on KOI8-R (safely, as a no-op) ahead of -the encoding flip. Once the flip is permanent the KOI8-R branch and this indirection are removed. +The engine holds text in UTF-8, where one character is one to four bytes. Code that reasons about +characters -- counting width, capitalising a letter, truncating without splitting a character, +comparing case-insensitively -- goes through here instead of touching bytes directly: a plain +LOWER(*s) or s[0] = UPPER(s[0]) is wrong on a multibyte letter and was the single largest source +of bugs in the migration. + +The conversions at the bottom of this header are boundaries, not helpers for everyday code: +from_disk_* / to_disk for the world files (still KOI8-R on disk), to_koi8 for legacy client code +pages (their tables are indexed by KOI8-R bytes). */ #ifndef BYLINS_SRC_UTILS_NATIVE_TEXT_H_ @@ -25,10 +22,6 @@ the encoding flip. Once the flip is permanent the KOI8-R branch and this indirec namespace native_text { -// True iff the engine was built with UTF-8 as its native runtime encoding. Reflects the flag the -// library itself was compiled with (not the translation unit that calls this), so callers/tests -// can branch reliably regardless of their own compile flags. -bool native_is_utf8(); // Number of display characters in a byte range / view. KOI8-R: the byte count. UTF-8: the number // of code points (malformed bytes counted as one each, so it never stalls on legacy data). diff --git a/src/version.cpp.in b/src/version.cpp.in index 97a4b8237..4aeab4323 100644 --- a/src/version.cpp.in +++ b/src/version.cpp.in @@ -9,7 +9,6 @@ #include "engine/entities/char_data.h" #include "engine/structs/structs.h" #include "utils/logger.h" -#include "utils/native_text.h" const char* REVISION = "$ Build revision: ${REVISION} $"; const char* revision = "${REVISION}"; @@ -22,25 +21,19 @@ const char* build_datetime = "${BUILD_DATETIME}"; const char* build_compiler = "${BUILD_COMPILER}"; const char* build_features = "${BUILD_FEATURES}"; -// Кодировка берётся из рантайма, а не из строки сборки: так она не может разойтись с тем, -// как движок на самом деле работает с текстом (issue #3681). -static const char *InternalEncodingName() { - return native_text::native_is_utf8() ? "UTF-8" : "KOI8-R"; -} - void ShowBuildInfo(CharData *ch) { SendMsgToChar(ch, "%s %s, build from %s, revision %s\n", engine_name, engine_version, build_datetime, revision); SendMsgToChar(ch, "Based on CircleMUD, version 3.00 beta patchlevel 16\n"); if (privilege::IsImmortal(ch)) { SendMsgToChar(ch, "Compiler: %s; internal encoding: %s\nEnabled features: %s\n", - build_compiler, InternalEncodingName(), build_features); + build_compiler, "UTF-8", build_features); } } void LogBuildInfo() { log("%s %s, build from %s, revision: %s\r\nCompiler: %s; internal encoding: %s\r\nEnabled features: %s", engine_name, engine_version, build_datetime, revision, - build_compiler, InternalEncodingName(), build_features); + build_compiler, "UTF-8", build_features); } // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/boards.encoding.cpp b/tests/boards.encoding.cpp index 66921192d..0d32e01d9 100644 --- a/tests/boards.encoding.cpp +++ b/tests/boards.encoding.cpp @@ -48,7 +48,7 @@ TEST(BoardsEncoding, MessagesLoadInTheNativeEncoding) { seen_cyrillic = true; // В UTF-8-сборке текст обязан быть корректным UTF-8; в KOI8-R-сборке -- наоборот, // это KOI8-R, который валидным UTF-8 не является. - EXPECT_EQ(utf8::is_valid(*field), native_text::native_is_utf8()) + EXPECT_TRUE(utf8::is_valid(*field)) << "поле не в нативной кодировке: " << *field; } } diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 203a437c6..b7250cf1f 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -1,8 +1,7 @@ // Unit tests for the native-encoding character helpers (src/utils/native_text.*, issue #3681). // -// Pure ASCII: non-ASCII fixtures are spelled as UTF-8 byte escapes. Expectations that differ -// between the KOI8-R and UTF-8 builds branch on native_text::native_is_utf8() (which reflects the -// flag the library was built with), so this one test file is correct under either build. +// Pure ASCII: non-ASCII fixtures are spelled as UTF-8 byte escapes, so the test data does not +// depend on how an editor happens to save this file. #include "utils/native_text.h" #include "utils/utf8.h" @@ -23,31 +22,20 @@ const char *const kNtPrivet = "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" } // namespace -// The legacy KOI8-R lowercase table (utils.cpp). Declared here rather than including utils.h, -// which pulls in fmt/ and the rest of the engine headers. -extern const char a_lcc_table[]; namespace { -int legacy_lower(unsigned char c) { - return a_lcc_table[c]; -} } // namespace TEST(NativeText, CharCountAscii) { EXPECT_EQ(native_text::char_count("Hello"), 5u); - EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 4), native_text::native_is_utf8() ? 2u : 4u); + EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 4), 2u); } TEST(NativeText, CharCountReflectsEncoding) { - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::char_count(kNtPrivet), 6u); - EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 12), 6u); - } else { - EXPECT_EQ(native_text::char_count(kNtPrivet), 12u); - EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 12), 12u); - } + EXPECT_EQ(native_text::char_count(kNtPrivet), 6u); + EXPECT_EQ(native_text::char_count(kNtPrivet, kNtPrivet + 12), 6u); } TEST(NativeText, CapitalizeAscii) { @@ -64,10 +52,7 @@ TEST(NativeText, CapitalizeAscii) { EXPECT_STREQ(already, "X"); } -TEST(NativeText, CapitalizeCyrillicUtf8Only) { - if (!native_text::native_is_utf8()) { - GTEST_SKIP() << "Cyrillic-as-UTF-8 fixtures are only meaningful under the UTF-8 build"; - } +TEST(NativeText, CapitalizeCyrillic) { char buf[] = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; // "privet" native_text::capitalize_first(buf); EXPECT_STREQ(buf, "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); // "Privet" @@ -75,26 +60,18 @@ TEST(NativeText, CapitalizeCyrillicUtf8Only) { TEST(NativeText, CharBytes) { EXPECT_EQ(native_text::char_bytes("A"), 1u); - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::char_bytes(kNtPrivet), 2u); // Cyrillic lead -> 2 bytes - EXPECT_EQ(native_text::char_bytes("\xF0\x9F\x98\x80"), 4u); // 4-byte code point - EXPECT_EQ(native_text::char_bytes("\xD0"), 1u); // truncated lead: 1 byte present - } else { - EXPECT_EQ(native_text::char_bytes(kNtPrivet), 1u); // KOI8-R: every byte is a char - } + EXPECT_EQ(native_text::char_bytes(kNtPrivet), 2u); // Cyrillic lead -> 2 bytes + EXPECT_EQ(native_text::char_bytes("\xF0\x9F\x98\x80"), 4u); // 4-byte code point + EXPECT_EQ(native_text::char_bytes("\xD0"), 1u); // truncated lead: 1 byte present } TEST(NativeText, TruncateOffset) { const std::string_view p(kNtPrivet, 12); EXPECT_EQ(native_text::truncate_offset(p, 100), 12u); // past end -> full size EXPECT_EQ(native_text::truncate_offset(p, 0), 0u); - if (native_text::native_is_utf8()) { - // 5 bytes lands mid-character; back up to the boundary after 2 code points (4 bytes). - EXPECT_EQ(native_text::truncate_offset(p, 5), 4u); - EXPECT_EQ(native_text::truncate_offset(p, 4), 4u); - } else { - EXPECT_EQ(native_text::truncate_offset(p, 5), 5u); - } + // 5 bytes lands mid-character; back up to the boundary after 2 code points (4 bytes). + EXPECT_EQ(native_text::truncate_offset(p, 5), 4u); + EXPECT_EQ(native_text::truncate_offset(p, 4), 4u); } TEST(NativeText, CompareCiAscii) { @@ -123,42 +100,9 @@ TEST(NativeText, CompareCiCyrillicIsCaseInsensitive) { // "just compare bytes" UTF-8 migration would silently lose. const char *const upper = "\xD0\x9F\xD0\xA0\xD0\x98\xD0\x92\xD0\x95\xD0\xA2"; const char *const lower = "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"; - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::compare_ci(upper, lower), 0); - EXPECT_EQ(native_text::compare_ci(upper, upper), 0); - EXPECT_NE(native_text::compare_ci(upper, "\xD0\xBF\xD1\x80\xD0\xB8"), 0); // prefix differs - } else { - // Under KOI8-R these UTF-8 bytes are not Cyrillic; just assert self-equality and that - // the comparison stays reflexive/antisymmetric on arbitrary high bytes. - EXPECT_EQ(native_text::compare_ci(upper, upper), 0); - EXPECT_EQ(native_text::compare_ci(lower, lower), 0); - } -} - -TEST(NativeText, CompareCiMatchesLegacyByteLoopUnderKoi8r) { - if (native_text::native_is_utf8()) { - GTEST_SKIP() << "this pins the KOI8-R branch against the original LOWER() byte loop"; - } - // Reference implementation: the exact loop str_cmp() used before the migration. - auto legacy = [](const char *a, const char *b) { - for (int i = 0;; ++i) { - if (!a[i] && !b[i]) { - return 0; - } - const int chk = legacy_lower(static_cast(a[i])) - - legacy_lower(static_cast(b[i])); - if (chk != 0) { - return chk; - } - } - }; - const char *const samples[] = {"", "a", "A", "abc", "ABC", "abd", "ab", "zzz", "\xC1\xC2", "\xE1\xE2"}; - for (const char *x : samples) { - for (const char *y : samples) { - EXPECT_EQ(native_text::compare_ci(x, y), legacy(x, y)) - << "mismatch for \"" << x << "\" vs \"" << y << "\""; - } - } + EXPECT_EQ(native_text::compare_ci(upper, lower), 0); + EXPECT_EQ(native_text::compare_ci(upper, upper), 0); + EXPECT_NE(native_text::compare_ci(upper, "\xD0\xBF\xD1\x80\xD0\xB8"), 0); // prefix differs } TEST(NativeText, IsAlnumChar) { @@ -169,13 +113,11 @@ TEST(NativeText, IsAlnumChar) { EXPECT_FALSE(native_text::is_alnum_char("!")); EXPECT_FALSE(native_text::is_alnum_char(".")); EXPECT_FALSE(native_text::is_alnum_char("")); // terminator is not alphanumeric - if (native_text::native_is_utf8()) { - // A Cyrillic letter is ONE alphanumeric character; its trail byte must not be read as - // punctuation (which is what the raw byte table would do and what breaks tokenisation). - EXPECT_TRUE(native_text::is_alnum_char(kNtPrivet)); - EXPECT_TRUE(native_text::is_alnum_char("\xD0\x81")); // Yo - EXPECT_TRUE(native_text::is_alnum_char("\xD1\x91")); // yo - } + // A Cyrillic letter is ONE alphanumeric character; its trail byte must not be read as + // punctuation (which is what the raw byte table would do and what breaks tokenisation). + EXPECT_TRUE(native_text::is_alnum_char(kNtPrivet)); + EXPECT_TRUE(native_text::is_alnum_char("\xD0\x81")); // Yo + EXPECT_TRUE(native_text::is_alnum_char("\xD1\x91")); // yo } TEST(NativeText, IsAlphaChar) { @@ -184,10 +126,8 @@ TEST(NativeText, IsAlphaChar) { EXPECT_FALSE(native_text::is_alpha_char("7")); // digit: alnum but not alpha EXPECT_FALSE(native_text::is_alpha_char(" ")); EXPECT_FALSE(native_text::is_alpha_char("")); - if (native_text::native_is_utf8()) { - EXPECT_TRUE(native_text::is_alpha_char(kNtPrivet)); - EXPECT_TRUE(native_text::is_alpha_char("\xD0\x81")); // Yo - } + EXPECT_TRUE(native_text::is_alpha_char(kNtPrivet)); + EXPECT_TRUE(native_text::is_alpha_char("\xD0\x81")); // Yo } TEST(NativeText, IsUpperChar) { @@ -195,14 +135,12 @@ TEST(NativeText, IsUpperChar) { EXPECT_FALSE(native_text::is_upper_char("a")); EXPECT_FALSE(native_text::is_upper_char("7")); EXPECT_FALSE(native_text::is_upper_char("")); - if (native_text::native_is_utf8()) { - // The byte table cannot see these: a UTF-8 Cyrillic lead byte is outside its uppercase - // range, which is why the anti-caps filter stopped working for Russian. - EXPECT_TRUE(native_text::is_upper_char("\xD0\x9F")); // P - EXPECT_FALSE(native_text::is_upper_char("\xD0\xBF")); // p - EXPECT_TRUE(native_text::is_upper_char("\xD0\x81")); // Yo - EXPECT_FALSE(native_text::is_upper_char("\xD1\x91")); // yo - } + // The byte table cannot see these: a UTF-8 Cyrillic lead byte is outside its uppercase + // range, which is why the anti-caps filter stopped working for Russian. + EXPECT_TRUE(native_text::is_upper_char("\xD0\x9F")); // P + EXPECT_FALSE(native_text::is_upper_char("\xD0\xBF")); // p + EXPECT_TRUE(native_text::is_upper_char("\xD0\x81")); // Yo + EXPECT_FALSE(native_text::is_upper_char("\xD1\x91")); // yo } TEST(NativeText, CharsEqualCi) { @@ -211,13 +149,11 @@ TEST(NativeText, CharsEqualCi) { EXPECT_TRUE(native_text::chars_equal_ci("Z", "z")); EXPECT_FALSE(native_text::chars_equal_ci("a", "b")); EXPECT_FALSE(native_text::chars_equal_ci("a", "")); - if (native_text::native_is_utf8()) { - // The regression this whole step exists for: with the raw KOI8-R byte table the lead - // bytes of "P"/"p" fold equal but the trail bytes differ, so the match was lost. - EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x9F", "\xD0\xBF")); // P vs p - EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x81", "\xD1\x91")); // Yo vs yo - EXPECT_FALSE(native_text::chars_equal_ci("\xD0\x9F", "\xD1\x80")); // P vs r - } + // The regression this whole step exists for: with the raw KOI8-R byte table the lead + // bytes of "P"/"p" fold equal but the trail bytes differ, so the match was lost. + EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x9F", "\xD0\xBF")); // P vs p + EXPECT_TRUE(native_text::chars_equal_ci("\xD0\x81", "\xD1\x91")); // Yo vs yo + EXPECT_FALSE(native_text::chars_equal_ci("\xD0\x9F", "\xD1\x80")); // P vs r } TEST(NativeText, CopyLowerChar) { @@ -228,32 +164,26 @@ TEST(NativeText, CopyLowerChar) { EXPECT_STREQ(buf, "z"); EXPECT_EQ(native_text::copy_lower_char("7", buf), 1u); EXPECT_STREQ(buf, "7"); - if (native_text::native_is_utf8()) { - std::memset(buf, 0, sizeof(buf)); - EXPECT_EQ(native_text::copy_lower_char("\xD0\x9F", buf), 2u); // P -> p - EXPECT_STREQ(buf, "\xD0\xBF"); - std::memset(buf, 0, sizeof(buf)); - EXPECT_EQ(native_text::copy_lower_char("\xD0\x81", buf), 2u); // Yo -> yo - EXPECT_STREQ(buf, "\xD1\x91"); - // In-place folding must be safe (the lowercase form keeps the byte length). - char inplace[] = "\xD0\x9F"; - EXPECT_EQ(native_text::copy_lower_char(inplace, inplace), 2u); - EXPECT_STREQ(inplace, "\xD0\xBF"); - } + std::memset(buf, 0, sizeof(buf)); + EXPECT_EQ(native_text::copy_lower_char("\xD0\x9F", buf), 2u); // P -> p + EXPECT_STREQ(buf, "\xD0\xBF"); + std::memset(buf, 0, sizeof(buf)); + EXPECT_EQ(native_text::copy_lower_char("\xD0\x81", buf), 2u); // Yo -> yo + EXPECT_STREQ(buf, "\xD1\x91"); + // In-place folding must be safe (the lowercase form keeps the byte length). + char inplace[] = "\xD0\x9F"; + EXPECT_EQ(native_text::copy_lower_char(inplace, inplace), 2u); + EXPECT_STREQ(inplace, "\xD0\xBF"); } TEST(NativeText, LastCharOffset) { EXPECT_EQ(native_text::last_char_offset(""), 0u); EXPECT_EQ(native_text::last_char_offset("a"), 0u); EXPECT_EQ(native_text::last_char_offset("abc"), 2u); - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::last_char_offset(kNtPrivet), 10u); // 6 chars, last starts at byte 10 - const std::string_view s(kNtPrivet, 12); - EXPECT_EQ(s.substr(native_text::last_char_offset(s)), "\xD1\x82"); // final char "t" - EXPECT_EQ(native_text::last_char_offset("\xF0\x9F\x98\x80"), 0u); // single 4-byte char - } else { - EXPECT_EQ(native_text::last_char_offset(kNtPrivet), 11u); // 12 bytes -> last byte - } + EXPECT_EQ(native_text::last_char_offset(kNtPrivet), 10u); // 6 chars, last starts at byte 10 + const std::string_view s(kNtPrivet, 12); + EXPECT_EQ(s.substr(native_text::last_char_offset(s)), "\xD1\x82"); // final char "t" + EXPECT_EQ(native_text::last_char_offset("\xF0\x9F\x98\x80"), 0u); // single 4-byte char } TEST(NativeText, ListContainsChar) { @@ -262,14 +192,12 @@ TEST(NativeText, ListContainsChar) { EXPECT_FALSE(native_text::list_contains_char("abc", "")); EXPECT_FALSE(native_text::list_contains_char("", "a")); EXPECT_FALSE(native_text::list_contains_char("abc", "A")); // case-sensitive, like strchr - if (native_text::native_is_utf8()) { - // A multibyte character must match as a whole and never on a partial byte sequence. - const char *const list = "\xD1\x88\xD1\x89\xD0\xB6\xD1\x87"; // sh shch zh ch - EXPECT_TRUE(native_text::list_contains_char(list, "\xD1\x89")); - EXPECT_TRUE(native_text::list_contains_char(list, "\xD0\xB6")); - EXPECT_FALSE(native_text::list_contains_char(list, "\xD1\x82")); - EXPECT_FALSE(native_text::list_contains_char(list, "\xD1")); // lead byte alone - } + // A multibyte character must match as a whole and never on a partial byte sequence. + const char *const list = "\xD1\x88\xD1\x89\xD0\xB6\xD1\x87"; // sh shch zh ch + EXPECT_TRUE(native_text::list_contains_char(list, "\xD1\x89")); + EXPECT_TRUE(native_text::list_contains_char(list, "\xD0\xB6")); + EXPECT_FALSE(native_text::list_contains_char(list, "\xD1\x82")); + EXPECT_FALSE(native_text::list_contains_char(list, "\xD1")); // lead byte alone } TEST(NativeText, CharRangeIteratesWholeCharacters) { @@ -277,13 +205,9 @@ TEST(NativeText, CharRangeIteratesWholeCharacters) { for (auto c : native_text::chars(kNtPrivet)) { got.emplace_back(c); } - if (native_text::native_is_utf8()) { - ASSERT_EQ(got.size(), 6u); // 6 letters, not 12 bytes - EXPECT_EQ(got.front(), "\xD0\x9F"); // whole "P", both bytes - EXPECT_EQ(got.back(), "\xD1\x82"); // whole "t" - } else { - ASSERT_EQ(got.size(), 12u); // KOI8-R: every byte is a character - } + ASSERT_EQ(got.size(), 6u); // 6 letters, not 12 bytes + EXPECT_EQ(got.front(), "\xD0\x9F"); // whole "P", both bytes + EXPECT_EQ(got.back(), "\xD1\x82"); // whole "t" got.clear(); for (auto c : native_text::chars("abc")) { @@ -309,13 +233,11 @@ TEST(NativeText, WholeStringCaseTransforms) { native_text::to_lower(buf); EXPECT_STREQ(buf, "mixed"); - if (native_text::native_is_utf8()) { - std::string ru = "\xD0\x9F\xD0\xA0\xD0\x98"; // "PRI" in Cyrillic - native_text::to_lower(ru); - EXPECT_EQ(ru, "\xD0\xBF\xD1\x80\xD0\xB8"); // "pri" - native_text::to_upper(ru); - EXPECT_EQ(ru, "\xD0\x9F\xD0\xA0\xD0\x98"); - } + std::string ru = "\xD0\x9F\xD0\xA0\xD0\x98"; // "PRI" in Cyrillic + native_text::to_lower(ru); + EXPECT_EQ(ru, "\xD0\xBF\xD1\x80\xD0\xB8"); // "pri" + native_text::to_upper(ru); + EXPECT_EQ(ru, "\xD0\x9F\xD0\xA0\xD0\x98"); } TEST(NativeText, RussianKeysMatchFirstCharCode) { @@ -332,7 +254,7 @@ TEST(NativeText, RussianKeysMatchFirstCharCode) { {"\xD7", "\xD0\xB2", rus::kVe}, {"\xC8", "\xD1\x85", rus::kHa}, }; for (const auto &c : cases) { - const char *const input = native_text::native_is_utf8() ? c.utf8 : c.koi8; + const char *const input = c.utf8; EXPECT_EQ(native_text::first_char_code(input), c.expected); } @@ -418,9 +340,9 @@ TEST(NativeText, TransliterationIsStableAcrossTheFlip) { {"\xF1", "\xD0\xAF", 'q'}, }; for (const auto &r : kRows) { - const char *const input = native_text::native_is_utf8() ? r.utf8 : r.koi8; + const char *const input = r.utf8; EXPECT_EQ(native_text::translit_to_filename(input), std::string(1, r.expected)) - << "transliteration drifted for " << (native_text::native_is_utf8() ? r.utf8 : r.koi8); + << "transliteration drifted for " << r.utf8; } // ASCII is lowercased and digits pass through, as before. @@ -429,8 +351,7 @@ TEST(NativeText, TransliterationIsStableAcrossTheFlip) { EXPECT_EQ(native_text::translit_to_filename(""), ""); // A whole name: "Vasya" in Cyrillic must give the same file name in both encodings. - const char *const name = native_text::native_is_utf8() - ? "\xD0\x92\xD0\xB0\xD1\x81\xD1\x8F" : "\xF7\xC1\xD3\xD1"; + const char *const name = "\xD0\x92\xD0\xB0\xD1\x81\xD1\x8F"; // "Vasya" EXPECT_EQ(native_text::translit_to_filename(name), "vasq"); } @@ -445,14 +366,10 @@ TEST(NativeText, FromKoi8BringsDiskTextIntoTheNativeEncoding) { EXPECT_EQ(native_text::from_koi8(""), ""); EXPECT_EQ(native_text::from_koi8("plain ascii 123"), "plain ascii 123"); // ASCII never changes - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::from_koi8(koi8_privet), utf8_privet); - // The result must be well-formed UTF-8 -- libfort aborts the process on anything else. - EXPECT_TRUE(utf8::is_valid(native_text::from_koi8(koi8_privet))); - EXPECT_EQ(native_text::char_count(native_text::from_koi8(koi8_privet)), 6u); - } else { - EXPECT_EQ(native_text::from_koi8(koi8_privet), koi8_privet); // identity under KOI8-R - } + EXPECT_EQ(native_text::from_koi8(koi8_privet), utf8_privet); + // The result must be well-formed UTF-8 -- libfort aborts the process on anything else. + EXPECT_TRUE(utf8::is_valid(native_text::from_koi8(koi8_privet))); + EXPECT_EQ(native_text::char_count(native_text::from_koi8(koi8_privet)), 6u); } TEST(NativeText, ToKoi8IsTheInverseBoundary) { @@ -464,13 +381,9 @@ TEST(NativeText, ToKoi8IsTheInverseBoundary) { EXPECT_EQ(native_text::to_koi8(""), ""); EXPECT_EQ(native_text::to_koi8("plain ascii 123"), "plain ascii 123"); - if (native_text::native_is_utf8()) { - EXPECT_EQ(native_text::to_koi8(utf8_privet), koi8_privet); - // Round trip through the boundary must return the original text. - EXPECT_EQ(native_text::from_koi8(native_text::to_koi8(utf8_privet)), utf8_privet); - } else { - EXPECT_EQ(native_text::to_koi8(koi8_privet), koi8_privet); - } + EXPECT_EQ(native_text::to_koi8(utf8_privet), koi8_privet); + // Round trip through the boundary must return the original text. + EXPECT_EQ(native_text::from_koi8(native_text::to_koi8(utf8_privet)), utf8_privet); } // KOI8-R spellings, so the fixture is built through from_koi8 and comes out native in either @@ -492,9 +405,6 @@ TEST(NativeText, SortKeyOrdersRussianAlphabetically) { const std::string zh = native("\xD6\xC1\xC2\xC1"); const std::string i = native("\xC9\xD7\xC1\xCE"); EXPECT_LT(native_text::sort_key(zh), native_text::sort_key(i)); - if (!native_text::native_is_utf8()) { - EXPECT_GT(zh, i); - } // ASCII keeps its own order and stays below the Cyrillic. EXPECT_LT(native_text::sort_key("abc"), native_text::sort_key("abd")); EXPECT_LT(native_text::sort_key("zzz"), native_text::sort_key(a)); @@ -524,11 +434,7 @@ TEST(NativeText, FromDiskTextIsIdempotent) { } EXPECT_EQ(acc, once); - if (native_text::native_is_utf8()) { - EXPECT_EQ(once, "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); - } else { - EXPECT_EQ(once, koi8); // no conversion at all under KOI8-R - } + EXPECT_EQ(once, "\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"); // ASCII is spelled the same in both encodings and must pass through untouched. EXPECT_EQ(native_text::from_disk_text(""), ""); EXPECT_EQ(native_text::from_disk_text(""), ""); @@ -557,7 +463,7 @@ TEST(NativeText, CharOffsetCutsOnCharacterBoundaries) { const std::string privet = native_text::from_koi8("\xD0\xD2\xC9\xD7\xC5\xD4"); // 6 letters const std::string cut = privet.substr(0, native_text::char_offset(privet, 3)); EXPECT_EQ(native_text::char_count(cut), 3u); - EXPECT_EQ(cut, privet.substr(0, native_text::native_is_utf8() ? 6 : 3)); + EXPECT_EQ(cut, privet.substr(0, 6)); // Asking for more than there is yields the whole string, never past the end. EXPECT_EQ(native_text::char_offset(privet, 100), privet.size()); EXPECT_EQ(native_text::char_offset("", 5), 0u); diff --git a/tests/translit_koi8.cpp b/tests/translit_koi8.cpp index 43b9bce9d..30382d207 100644 --- a/tests/translit_koi8.cpp +++ b/tests/translit_koi8.cpp @@ -1,9 +1,7 @@ // Unit tests for the KOI8-R transliteration table (src/utils/translit_koi8.*, issue #3681). // -// Pure ASCII: every non-ASCII fixture is spelled as UTF-8 byte escapes, so the file means the -// same thing under either source encoding. The end-to-end expectations go through -// native_text::to_koi8, which only converts in the UTF-8 build, so they branch on -// native_text::native_is_utf8(). +// Pure ASCII: every non-ASCII fixture is spelled as UTF-8 byte escapes, so the test data does not +// depend on how an editor happens to save this file. #include "utils/translit_koi8.h" #include "utils/native_text.h" @@ -88,9 +86,6 @@ TEST(TranslitKoi8, TableIsSortedAndUnique) { } TEST(TranslitKoi8, ToKoi8AppliesTheTable) { - if (!native_text::native_is_utf8()) { - GTEST_SKIP() << "to_koi8 is a no-op when the native encoding already is KOI8-R"; - } // "-- privet ..." with an em dash and an ellipsis around Cyrillic text. const std::string source = "\xE2\x80\x94" " \xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" "\xE2\x80\xA6"; @@ -102,9 +97,6 @@ TEST(TranslitKoi8, ToKoi8AppliesTheTable) { } TEST(TranslitKoi8, ToKoi8FallsBackToThePlaceholder) { - if (!native_text::native_is_utf8()) { - GTEST_SKIP() << "to_koi8 is a no-op when the native encoding already is KOI8-R"; - } // An emoji has no KOI8-R equivalent at all, so it becomes the single placeholder character. EXPECT_EQ(native_text::to_koi8("a\xF0\x9F\x98\x80" "b"), "a?b"); } From d31ad24fbe21e36883b5ac1b0a7bca9b15425e8c Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 07:44:36 +0200 Subject: [PATCH 058/110] =?UTF-8?q?fix(utf8):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D1=83=20=D0=BD=D0=B0=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D0=B0=D1=85,=20=D0=B3=D0=B4=D0=B5=20?= =?UTF-8?q?=D0=B5=D1=91=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=82=20(C4,=20#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Четыре места переводили текст движка из KOI8-R в UTF-8 для внешнего потребителя: телега, Admin API (JSON), обсервабилити и запись в SQLite. Движок теперь сам в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь не осталось, а перекодировка разбирала бы готовый UTF-8 как KOI8-R и удваивала каждую букву. Ровно та же ошибка, из-за которой рос obj_sets.xml, только наружу. Заодно добиты шесть последних побайтных циклов лоуэрения (A4): названия рецептов и способностей в кавычках, im.cpp и modify.cpp. Обёртки Koi8rToUtf8 и EngineStringToUtf8 стали тождественными, но остались на месте: у них 78 вызовов, и выкидывать их стоит отдельным механическим проходом, а не вперемешку с исправлением ошибки. --- .../db/sqlite_world_data_source_freshness.cpp | 9 +++---- src/engine/network/admin_api/json_helpers.cpp | 23 ++++++---------- src/engine/observability/event_sink.cpp | 9 +++---- src/engine/ui/cmd/do_telegram.cpp | 6 +++-- src/engine/ui/modify.cpp | 13 ++++++--- src/gameplay/crafting/im.cpp | 27 +++++++++++++------ 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/engine/db/sqlite_world_data_source_freshness.cpp b/src/engine/db/sqlite_world_data_source_freshness.cpp index e8fc6efae..4a34db5d5 100644 --- a/src/engine/db/sqlite_world_data_source_freshness.cpp +++ b/src/engine/db/sqlite_world_data_source_freshness.cpp @@ -25,11 +25,10 @@ void BindTextKoi(sqlite3_stmt *stmt, int col, const char *koi8) sqlite3_bind_null(stmt, col); return; } - const std::string in(koi8); - // koi_to_utf8 can roughly double the byte count; size generously. - std::vector out(in.size() * 2 + 4, '\0'); - codepages::koi_to_utf8(const_cast(in.c_str()), out.data()); - sqlite3_bind_text(stmt, col, out.data(), -1, SQLITE_TRANSIENT); + // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. + // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как + // KOI8-R и удваивала каждую букву (issue #3681). + sqlite3_bind_text(stmt, col, koi8, -1, SQLITE_TRANSIENT); } void SqliteWorldDataSource::EnsureSyncTables() diff --git a/src/engine/network/admin_api/json_helpers.cpp b/src/engine/network/admin_api/json_helpers.cpp index 8bc2100a8..d476baa2e 100644 --- a/src/engine/network/admin_api/json_helpers.cpp +++ b/src/engine/network/admin_api/json_helpers.cpp @@ -43,25 +43,18 @@ std::string Koi8rToUtf8(const char* koi8r) return ""; } - char utf8_buf[kMaxStringLength]; - codepages::koi_to_utf8(const_cast(koi8r), utf8_buf); - return std::string(utf8_buf); + // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. + // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как + // KOI8-R и удваивала каждую букву (issue #3681). + return std::string(koi8r); } std::string Koi8rToUtf8(const std::string& koi8r) { - char utf8_buf[kMaxSockBuf * 6]; - char koi8r_buf[kMaxSockBuf * 6]; - - // Initialize buffers to prevent returning garbage if conversion fails - utf8_buf[0] = '\0'; - - strncpy(koi8r_buf, koi8r.c_str(), sizeof(koi8r_buf) - 1); - koi8r_buf[sizeof(koi8r_buf) - 1] = '\0'; - - codepages::koi_to_utf8(koi8r_buf, utf8_buf); - - return std::string(utf8_buf); + // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. + // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как + // KOI8-R и удваивала каждую букву (issue #3681). + return koi8r; } std::string Utf8ToKoi8r(const std::string& utf8) diff --git a/src/engine/observability/event_sink.cpp b/src/engine/observability/event_sink.cpp index 4f0a4b641..327460f1d 100644 --- a/src/engine/observability/event_sink.cpp +++ b/src/engine/observability/event_sink.cpp @@ -53,11 +53,10 @@ void FlushAllSinks() { } std::string EngineStringToUtf8(const std::string& koi8r) { - std::array buf{}; - std::string mut = koi8r; // codepages::koi_to_utf8 takes char*, not const - mut.push_back('\0'); - codepages::koi_to_utf8(mut.data(), buf.data()); - return std::string(buf.data()); + // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. + // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как + // KOI8-R и удваивала каждую букву (issue #3681). + return koi8r; } } // namespace observability diff --git a/src/engine/ui/cmd/do_telegram.cpp b/src/engine/ui/cmd/do_telegram.cpp index 26a0b3832..e3f311b6f 100644 --- a/src/engine/ui/cmd/do_telegram.cpp +++ b/src/engine/ui/cmd/do_telegram.cpp @@ -44,8 +44,10 @@ void do_telegram([[maybe_unused]] CharData *ch, [[maybe_unused]] char *argument, } snprintf(smallBuf, kMaxInputLength, "Поступила телега от %s, сообщают следующее:\r\n%s", GET_NAME(ch), output); - codepages::koi_to_utf8(const_cast(smallBuf), utfBuf); - if (strlen(utfBuf) < 10) { + // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. + // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как + // KOI8-R и удваивала каждую букву (issue #3681). + if (strlen(smallBuf) < 10) { SendMsgToChar("Ошибочка вышла..\r\n", ch); return; } diff --git a/src/engine/ui/modify.cpp b/src/engine/ui/modify.cpp index 837841248..d47afa9b0 100644 --- a/src/engine/ui/modify.cpp +++ b/src/engine/ui/modify.cpp @@ -930,8 +930,11 @@ void do_featset(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } // Locate the last quote and lowercase the magic words (if any) // - for (qend = 1; argument[qend] && argument[qend] != '\''; qend++) - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && argument[qend] != '\''; + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); + } if (argument[qend] != '\'') { SendMsgToChar("Название способности должно быть заключено в символы : ''\r\n", ch); @@ -1040,8 +1043,10 @@ void do_skillset(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } // Locate the last quote and lowercase the magic words (if any) - for (qend = 1; argument[qend] && argument[qend] != '\''; qend++) { - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && argument[qend] != '\''; + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); } if (argument[qend] != '\'') { diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index e35dc3e95..cf90fb5a2 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -1050,8 +1050,11 @@ void do_rset(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } // Locate the last quote and lowercase the magic words (if any) - for (qend = 1; argument[qend] && argument[qend] != '\''; qend++) - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && argument[qend] != '\''; + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); + } if (argument[qend] != '\'') { SendMsgToChar("Рецепт должен быть заключен в символы : ''\r\n", ch); @@ -1217,8 +1220,11 @@ void do_cook(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar("Рецепт надо заключить в символы : ' * или !\r\n", ch); return; } - for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); qend++) - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); + } if (!IS_RECIPE_DELIM(argument[qend])) { SendMsgToChar("Рецепт должен быть заключен в символы : ' * или !\r\n", ch); return; @@ -1537,8 +1543,10 @@ void compose_recipe(CharData *ch, char *argument, int/* subcmd*/) { return; } - for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); qend++) { - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); } if (!IS_RECIPE_DELIM(argument[qend])) { @@ -1595,8 +1603,11 @@ void forget_recipe(CharData *ch, char *argument, int/* subcmd*/) { SendMsgToChar("Рецепт надо заключить в символы : ' * или !\r\n", ch); return; } - for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); qend++) - argument[qend] = LOWER(argument[qend]); + // Шагаем по символу: под UTF-8 русская буква занимает два байта (issue #3681). + for (qend = 1; argument[qend] && !IS_RECIPE_DELIM(argument[qend]); + qend += static_cast(native_text::char_bytes(argument + qend))) { + native_text::copy_lower_char(argument + qend, argument + qend); + } if (!IS_RECIPE_DELIM(argument[qend])) { SendMsgToChar("Рецепт должен быть заключен в символы : ' * или !\r\n", ch); return; From ea09960062cb9d4f207813392fe7669142b9da7c Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 07:58:40 +0200 Subject: [PATCH 059/110] =?UTF-8?q?fix(utf8):=20=D0=B2=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=8C=20=D0=B2=20UTF-8=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B,=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B1=D1=8D=D0=B9=D0=B7=D0=B5=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Во время ребэйза дерево проходит через состояние до флипа, где .gitattributes ещё перекодировал src/ в KOI8-R при выкладке. Файлы, тронутые на конфликтующих шагах, были записаны в KOI8-R и в таком виде попали в коммиты, оказавшиеся уже после флипа. Содержимое не пострадало -- пострадала кодировка блоба. Двадцать два файла возвращены в UTF-8. Проверено сплошным проходом: не-UTF-8 файлов в src/ и tests/ не осталось. --- src/administration/names.cpp | 90 +- src/engine/boot/boot_data_files.cpp | 84 +- src/engine/db/world_data_source_base.cpp | 4 +- src/engine/scripting/dg_comm.cpp | 36 +- src/engine/scripting/dg_olc.cpp | 138 +- src/engine/scripting/dg_scripts.cpp | 636 +++---- src/engine/ui/cmd/do_statistic.cpp | 18 +- src/engine/ui/cmd/do_toggle.cpp | 116 +- src/engine/ui/cmd_god/do_print_armor.cpp | 114 +- src/engine/ui/cmd_god/do_stat.cpp | 474 ++--- src/engine/ui/commands.cpp | 4 +- src/gameplay/clans/house.cpp | 1690 ++++++++--------- .../boards/boards_changelog_loaders.cpp | 8 +- src/gameplay/communication/parcel.cpp | 252 +-- src/gameplay/crafting/item_creation.cpp | 850 ++++----- src/gameplay/mechanics/groups.cpp | 246 +-- src/gameplay/mechanics/obj_sets_olc.cpp | 418 ++-- src/gameplay/mechanics/sight.cpp | 662 +++---- src/gameplay/statistics/dps.cpp | 104 +- src/gameplay/statistics/mob_stat.cpp | 92 +- src/utils/utils_parse.cpp | 106 +- src/utils/utils_string.cpp | 94 +- 22 files changed, 3118 insertions(+), 3118 deletions(-) diff --git a/src/administration/names.cpp b/src/administration/names.cpp index 63396fafc..43887b5b0 100644 --- a/src/administration/names.cpp +++ b/src/administration/names.cpp @@ -51,7 +51,7 @@ int was_agree_name(DescriptorData *d) { d->character->set_sex(static_cast(sex)); (d->character)->player_specials->saved.NameGod = immlev + 1000; (d->character)->player_specials->saved.NameIDGod = GetPlayerIdByName(immname); - sprintf(buf, "\r\n !\r\n"); + sprintf(buf, "\r\nВаше имя одобрено!\r\n"); iosystem::write_to_output(buf, d); sprintf(buf, "AUTOAGREE: %s was agreed by %s", GET_PC_NAME(d->character), immname); log(buf, d); @@ -75,7 +75,7 @@ int was_disagree_name(DescriptorData *d) { } sscanf(line.c_str(), "%s %s %d", mortname, immname, &immlev); if (!strcmp(mortname, GET_NAME(d->character))) { - sprintf(buf, "\r\n !\r\n"); + sprintf(buf, "\r\nВаше имя запрещено!\r\n"); iosystem::write_to_output(buf, d); sprintf(buf, "AUTOAGREE: %s was disagreed by %s", GET_PC_NAME(d->character), immname); log(buf, d); @@ -102,18 +102,18 @@ void rm_agree_name(CharData *d) { }); } -// , 2 -// , +// список неодобренных имен, дубль2 +// на этот раз ничего перебирать не будем, держа все в памяти и обновляя по необходимости struct NewName { - std::string name0; // + std::string name0; // падежи std::string name1; // --//-- std::string name2; // --//-- std::string name3; // --//-- std::string name4; // --//-- std::string name5; // --//-- - std::string email; // - EGender sex{EGender::kNeutral}; // , + std::string email; // мыло + EGender sex{EGender::kNeutral}; // часто не ясно, для какоо пола падежи вообще }; typedef std::shared_ptr NewNamePtr; @@ -121,7 +121,7 @@ typedef std::map NewNameListType; static NewNameListType NewNameList; -// +// сохранение списка в файл static void NewNames::save() { // issue.misc-migrate: atomic whole-file replace via StateManager. std::vector lines; @@ -129,15 +129,15 @@ static void NewNames::save() { for (const auto &it : NewNameList) { lines.push_back(it.first); } - // : ( KOI8-R), - // (issue #3681). + // Граница записи: имена уходят на диск в кодировке мира (сейчас KOI8-R), зеркально + // чтению (issue #3681). for (auto &line : lines) { line = native_text::to_disk(line); } MUD::StateManager().SaveLines(state::EStateFile::kPendingNames, lines); } -// +// добавить в список без сохранения на диск static void NewNames::cache_add(CharData *ch) { NewNamePtr name(new NewName); @@ -153,13 +153,13 @@ static void NewNames::cache_add(CharData *ch) { NewNameList[GET_NAME(ch)] = name; } -// +// добавление имени в список неодобренных для показа иммам void NewNames::add(CharData *ch) { cache_add(ch); save(); } -// / +// поиск/удаление персонажа из списка неодобренных имен void NewNames::remove(CharData *ch) { NewNameListType::iterator it; it = NewNameList.find(GET_NAME(ch)); @@ -168,19 +168,19 @@ void NewNames::remove(CharData *ch) { save(); } -// +// для удаления через команду имма void NewNames::remove(const std::string &name, CharData *actor) { auto it = NewNameList.find(name); if (it != NewNameList.end()) { NewNameList.erase(it); - SendMsgToChar(" .\r\n", actor); + SendMsgToChar("Запись удалена.\r\n", actor); } else - SendMsgToChar(" .\r\n", actor); + SendMsgToChar("В списке нет такого имени.\r\n", actor); save(); } -// +// лоад списка неодобренных имен void NewNames::load() { // issue.misc-migrate: read names via StateManager (keeping the previous `>> buffer` tokenising), // then re-save to normalise. A missing file just yields an empty list. @@ -188,32 +188,32 @@ void NewNames::load() { std::istringstream iss(line); std::string buffer; while (iss >> buffer) { - // + // сразу проверяем не сделетился ли уже персонаж Player t_tch; Player *tch = &t_tch; if (LoadPlayerCharacter(buffer.c_str(), tch, ELoadCharFlags::kFindId) < 0) continue; - // ... + // не сделетился... cache_add(tch); } } save(); } -// +// вывод списка неодобренных имму bool NewNames::show(CharData *actor) { if (NewNameList.empty()) return false; std::ostringstream buffer; - buffer << "\r\n, ( <> //):\r\n" << kColorWht; + buffer << "\r\nИгроки, ждущие одобрения имени (имя <игрок> одобрить/запретить/удалить):\r\n" << kColorWht; for (NewNameListType::const_iterator it = NewNameList.begin(); it != NewNameList.end(); ++it) { const auto sex = static_cast(to_underlying(it->second->sex)); - buffer << ": " << it->first << " " << it->second->name0 << "/" << it->second->name1 + buffer << "Имя: " << it->first << " " << it->second->name0 << "/" << it->second->name1 << "/" << it->second->name2 << "/" << it->second->name3 << "/" << it->second->name4 << "/" << it->second->name5 << " Email: &S" - << (GET_GOD_FLAG(actor, EGf::kDemigod) ? "" : it->second->email) - << "&s : " << genders[sex] << "\r\n"; + << (GET_GOD_FLAG(actor, EGf::kDemigod) ? "неопределен" : it->second->email) + << "&s Пол: " << genders[sex] << "\r\n"; } buffer << kColorNrm; SendMsgToChar(buffer.str(), actor); @@ -286,41 +286,41 @@ static void go_name(CharData *ch, CharData *vict, int action) { int god_level = ch->IsFlagged(EPrf::kCoderinfo) ? kLvlImplementator : GetRealLevel(ch); if (GetRealLevel(vict) > god_level) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("А он ведь старше вас...\r\n", ch); return; } - // + // одобряем или нет int lev = (vict)->player_specials->saved.NameGod; if (lev > 1000) lev = lev - 1000; if (lev > god_level) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Об этом имени уже позаботился бог старше вас.\r\n", ch); return; } if (lev == god_level) if ((vict)->player_specials->saved.NameIDGod != ch->get_uid()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Об этом имени уже позаботился другой бог вашего уровня.\r\n", ch); if (action == NAME_AGREE) { (vict)->player_specials->saved.NameGod = god_level + 1000; (vict)->player_specials->saved.NameIDGod = ch->get_uid(); - //SendMsgToChar(" !\r\n", ch); - SendMsgToChar(vict, "&G !&n\r\n"); + //SendMsgToChar("Имя одобрено!\r\n", ch); + SendMsgToChar(vict, "&GВаше имя одобрено!&n\r\n"); agree_name(vict, GET_NAME(ch), god_level); - sprintf(buf, "&c%s %s %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); + sprintf(buf, "&c%s одобрил%s имя игрока %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); SendMsgToGods(buf, true); - // + // В этом теперь нет смысла //mudlog(buf, CMP, kLevelGod, SYSLOG, true); } else { (vict)->player_specials->saved.NameGod = god_level; (vict)->player_specials->saved.NameIDGod = ch->get_uid(); - //SendMsgToChar(" !\r\n", ch); - SendMsgToChar(vict, "&R !&n\r\n"); + //SendMsgToChar("Имя запрещено!\r\n", ch); + SendMsgToChar(vict, "&RВаше имя запрещено!&n\r\n"); disagree_name(vict, GET_NAME(ch), god_level); - sprintf(buf, "&c%s %s %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); + sprintf(buf, "&c%s запретил%s имя игрока %s.&n\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), GET_NAME(vict)); SendMsgToGods(buf, true); //mudlog(buf, CMP, kLevelGod, SYSLOG, true); @@ -329,10 +329,10 @@ static void go_name(CharData *ch, CharData *vict, int action) { } const char *MORTAL_DO_TITLE_FORMAT = "\r\n" - " - , , \r\n" - " <> - \r\n" - " <> - \r\n" - " <> - \r\n"; + "имя - вывод списка имен, ждущих одобрения, если они есть\r\n" + "имя <игрок> одобрить - одобрить имя данного игрока\r\n" + "имя <игрок> запретить - запретить имя данного игрока\r\n" + "имя <игрок> удалить - удалить данное имя из списка без запрета или одобрения\r\n"; void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { std::string name, command = argument; @@ -348,11 +348,11 @@ void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { utils::Trim(command); int action = -1; - if (CompareParam(command, "")) + if (CompareParam(command, "одобрить")) action = NAME_AGREE; - else if (CompareParam(command, "")) + else if (CompareParam(command, "запретить")) action = NAME_DISAGREE; - else if (CompareParam(command, "")) + else if (CompareParam(command, "удалить")) action = NAME_DELETE; if (action < 0) { @@ -369,14 +369,14 @@ void do_name(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { CharData *vict; if ((vict = target_resolver::FindPlayerVis(ch, name)) != nullptr) { if (!(vict = target_resolver::FindPlayer(ch, name))) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такого игрока.\r\n", ch); return; } go_name(ch, vict, action); } else { - vict = new Player; // TODO: + vict = new Player; // TODO: переделать на стек if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) < 0) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Такого персонажа не существует.\r\n", ch); delete vict; return; } diff --git a/src/engine/boot/boot_data_files.cpp b/src/engine/boot/boot_data_files.cpp index 1daff92b8..6b2df297b 100644 --- a/src/engine/boot/boot_data_files.cpp +++ b/src/engine/boot/boot_data_files.cpp @@ -58,10 +58,10 @@ void DataFile::get_one_line(char *buf, std::size_t buf_size) { raw[--len] = '\0'; // take off the trailing \r\n } - // (issue #3681): KOI8-R, - // UTF-8 ģ -- , ף - // , . - // (KOI8-R -> ң UTF-8), . + // Граница кодировки (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'; @@ -407,7 +407,7 @@ void WorldFile::parse_room(int virtual_nr) { fatal_log("SYSERR: Room %d is outside of any zone.", virtual_nr); } } - // + // Создаем новую комнату world.push_back(new RoomData); world[room_realnum]->zone_rn = zone; world[room_realnum]->vnum = virtual_nr; @@ -482,7 +482,7 @@ void WorldFile::parse_room(int virtual_nr) { switch (letter) { case 'I': get_line(file(), - line); // , + line); //оставлено для совместимости, удалить после пересохранения комнат break; case 'T': dg_read_trigger(world[room_realnum], WLD_TRIGGER, virtual_nr); @@ -512,7 +512,7 @@ void WorldFile::setup_dir(int room, unsigned dir) { world[room]->dir_option_proto[dir] = std::make_shared(); world[room]->dir_option_proto[dir]->general_description = fread_string(); - // ; , + // парс строки алиаса двери на имя; вининельный падеж, если он есть world[room]->dir_option_proto[dir]->set_keywords(fread_string()); if (!get_line(file(), line)) { @@ -540,9 +540,9 @@ void WorldFile::setup_dir(int room, unsigned dir) { world[room]->dir_option_proto[dir]->key = t[1]; world[room]->dir_option_proto[dir]->to_room(t[2]); - // D- - , - // show errors. - // ( YAML- SQLite-), . issue #3272. + // Полностью пустые D-блоки - мусор из старых редакторов, на рантайм не + // влияют и без причины засоряют show errors. Дропаем их сразу при загрузке + // (симметрично в YAML- и SQLite-загрузчиках), см. issue #3272. if (world[room]->dir_option_proto[dir]->is_empty()) { world[room]->dir_option_proto[dir].reset(); } @@ -615,7 +615,7 @@ void ObjectFile::parse_object(const int nr) { tobj->set_short_description(utils::colorLOW(fread_string())); snprintf(buf, sizeof(buf), "%s", tobj->get_short_description().c_str()); - tobj->set_PName(grammar::ECase::kNom, utils::colorLOW(buf)); // + tobj->set_PName(grammar::ECase::kNom, utils::colorLOW(buf)); //именительный падеж равен короткому описанию for (j = grammar::ECase::kGen; j <= grammar::ECase::kLastCase; j++) { tobj->set_PName(static_cast(j), utils::colorLOW(fread_string())); @@ -659,8 +659,8 @@ void ObjectFile::parse_object(const int nr) { timer = 99999; } tobj->set_timer(timer); - // issue #3581: obj->spell -- ͣ , . - // t[2]/t[3] ӣ ݣ ( - = 4 ), . + // issue #3581: obj->spell и его уровень -- мёртвая пара, ни одна механика их не читает. + // t[2]/t[3] всё ещё разбираются (формат легаси-строки = 4 поля), но объекту не присваиваются. if (!get_line(file(), m_line)) { fatal_log("SYSERR: Expecting *3th* numeric line of %s, but file ended!", m_buffer); @@ -731,7 +731,7 @@ void ObjectFile::parse_object(const int nr) { tobj->set_weight(tobj->get_val(1) + 5); } } - tobj->unset_extraflag(EObjFlag::kTransformed); // + tobj->unset_extraflag(EObjFlag::kTransformed); //от шаловливых ручек tobj->unset_extraflag(EObjFlag::kTicktimer); // *** extra descriptions and affect fields *** @@ -829,7 +829,7 @@ bool ObjectFile::check_object(ObjData *obj) { log("SYSERR: Object #%d (%s) has negative weight (%d).", GET_OBJ_VNUM(obj), obj->get_short_description().c_str(), obj->get_weight()); } -/* +/* спам от антуражных шмоток if (obj->get_rent_off() <= 0) { error = true; log("SYSERR: Object #%d (%s) has negative cost/day (%d).", @@ -1026,14 +1026,14 @@ void MobileFile::parse_mobile(const int nr) { // DG triggers -- script info follows mob S/E section // DG triggers -- script is defined after the end of the room 'T' - // -- 'I' - // - -- 'L' + // Ингредиентная магия -- 'I' + // Объекты загружаемые по-смертно -- 'L' do { letter = fread_letter(file()); ungetc(letter, file()); switch (letter) { - case 'I': // ( ). + case 'I': // Оставлено для совместимости со старым форматиом (с ингредиентами в файлах зон). get_line(file(), line); break; @@ -1074,12 +1074,12 @@ void MobileFile::parse_mobile(const int nr) { } } /* -// - // - " " - // ""/" " -- - // Skill: 189, - - // . kLifeMagic = level * 10 - // (#3267). +//сконвертированно в пфайлы + // Авто-выставление "магии жизни" мобам с заклинанием + // "исцеление"/"групповое исцеление" -- билдеры часто забывают + // прописать Skill: 189, и кастер заваливает каст из-за нулевого + // процента. На загрузке выставляем kLifeMagic = level * 10 для + // любого моба с одним из этих заклинаний в памяти (#3267). if (GET_SPELL_MEM(mob_proto + i, ESpell::kHeal) > 0 || GET_SPELL_MEM(mob_proto + i, ESpell::kGroupHeal) > 0) { mob_proto[i].set_skill(ESkill::kLifeMagic, mob_proto[i].GetLevel() * 10); @@ -1099,7 +1099,7 @@ void MobileFile::parse_simple_mob(int i, int nr) { mob_proto[i].set_con(11); mob_proto[i].set_cha(11); - mob_proto[i].player_specials->saved.NameGod = 1001; // + mob_proto[i].player_specials->saved.NameGod = 1001; // у мобов имя всегда одобрено if (!get_line(file(), line)) { fatal_log("SYSERR: Format error in mob #%d, file ended after S flag!", nr); } @@ -1236,10 +1236,10 @@ void MobileFile::interpret_espec(const char *keyword, const char *value, int i, } -/* , - if (GET_RESIST(mob_proto + i, 4) > 49 && !mob_proto[i].get_role(kBoss)) // +/* заготовка парса резистов у моба при загрузке мада, чтоб в след раз не придумывать + if (GET_RESIST(mob_proto + i, 4) > 49 && !mob_proto[i].get_role(kBoss)) // жизнь и не боссы { - if (zone_table[world[&mob_proto[i]->in_room]->zone].group < 3) // 0-2 + if (zone_table[world[&mob_proto[i]->in_room]->zone].group < 3) // в зонах 0-2 группы log("RESIST LIVE num: %d Vnum: %d Level: %d Name: %s", GET_RESIST(mob_proto + i, 4), mob_index[i].vnum, GetRealLevel(&mob_proto[i]), GET_PAD(&mob_proto[i], 0)); } */ @@ -1254,7 +1254,7 @@ void MobileFile::interpret_espec(const char *keyword, const char *value, int i, SetSave(mob_proto + i, save, std::clamp(t[to_underlying(save)], kMinSaving, kMaxSaving)); } } -// Svent: ? . +// Svent: и что тут за коллекция магик намберов бесконечная? Вынести в настройки. CASE("HPReg") { mob_proto[i].add_abils.hitreg = std::clamp(num_arg, -200, 200); } @@ -1489,7 +1489,7 @@ bool ZoneFile::load_zone() { size_t digits = full_file_name().find_first_of("1234567890"); if (digits <= full_file_name().size()) { if (zone.vnum != atoi(full_file_name().c_str() + digits)) { - fatal_log("SYSERR: %s %d", full_file_name().c_str(), zone.vnum); + fatal_log("SYSERR: файл %s содержит неверный номер зоны %d", full_file_name().c_str(), zone.vnum); } } if (2 == count) { @@ -1538,7 +1538,7 @@ bool ZoneFile::load_regular_zone() { if (zone.typeB_count) { CREATE(zone.typeB_list, zone.typeB_count); CREATE(zone.typeB_flag, zone.typeB_count); - // + // сбрасываем все флаги for (b_number = zone.typeB_count; b_number > 0; b_number--) { zone.typeB_flag[b_number - 1] = false; } @@ -1559,7 +1559,7 @@ bool ZoneFile::load_regular_zone() { } zone.name = buf; - log(" zon : %s", full_file_name().c_str()); + log("Читаем zon файл: %s", full_file_name().c_str()); while (*buf != 'S' && !feof(file())) { line_num += get_line(file(), buf); @@ -1598,9 +1598,9 @@ bool ZoneFile::load_regular_zone() { auto group = 0; const auto count = sscanf(buf, "#%d %d %d %d", &zone.level, &zone.type, &group, &zone.entrance); if (count < 2) { - fatal_log("SYSERR: z.level, z.type, z.group, z.entrance: %s", buf); + fatal_log("SYSERR: ошибка чтения z.level, z.type, z.group, z.entrance: %s", buf); } - zone.group = (group == 0) ? 1 : group; // 0 + zone.group = (group == 0) ? 1 : group; //группы в 0 рыл не бывает line_num += get_line(file(), buf); char t1[80]; @@ -1609,7 +1609,7 @@ bool ZoneFile::load_regular_zone() { *t2 = 0; auto tmp_reset_idle = 0; if (sscanf(buf, " %d %d %d %d %s %s", &zone.top, &zone.lifespan, &zone.reset_mode, &tmp_reset_idle, t1, t2) < 4) { - // , , , -- + // если нет четырех констант, то, возможно, это старый формат -- попробуем прочитать три const auto count = sscanf(buf, " %d %d %d %s %s", &zone.top, &zone.lifespan, @@ -1643,9 +1643,9 @@ bool ZoneFile::load_regular_zone() { } ptr++; - // : - // A _ -- A - // B _ -- B + // Новые параметры формата файла: + // A номер_зоны -- зона типа A из списка + // B номер_зоны -- зона типа B из списка if (zone.cmd[cmd_no].command == 'A') { sscanf(ptr, " %d", &zone.typeA_list[a_number]); a_number++; @@ -1744,8 +1744,8 @@ class HelpFile : public DataFile { }; bool HelpFile::load_help() { - // (KOI8-R - // UTF-8 ), (issue #3681). + // В нативной кодировке строка может быть втрое длиннее прочитанной с диска (KOI8-R байт + // разворачивается в UTF-8 максимум в три), поэтому буферы с запасом (issue #3681). constexpr size_t kLineBufSize = 3 * READ_SIZE + 1; #if defined(CIRCLE_MACINTOSH) static char key[kLineBufSize], next_key[kLineBufSize], entry[32384]; // ? @@ -1762,8 +1762,8 @@ bool HelpFile::load_help() { snprintf(entry, sizeof(entry), "%s\r\n", key); get_one_line(line, sizeof(line)); while (*line != '#') { - // '#' - // + // если вдруг файл внезапно закончился и '#' так и не встретился + // логаем ошибку и заканчиваем парсинг во избежание зацикливания if ((*line == '$') && (*(line + 1) == 0)) { std::stringstream str_log; str_log << "SYSERR: unexpected EOF in help file: \"" << file_name() << "\""; diff --git a/src/engine/db/world_data_source_base.cpp b/src/engine/db/world_data_source_base.cpp index 6fb4bb2e6..32dd51937 100644 --- a/src/engine/db/world_data_source_base.cpp +++ b/src/engine/db/world_data_source_base.cpp @@ -119,8 +119,8 @@ IndexData* WorldDataSourceBase::CreateTriggerIndex(int vnum, Trigger *trig) trig_index[top_of_trigt++] = index; - // 'vnum trig': . - // -- . + // Индекс для 'vnum trig': какие триггеры грузят данный предмет. Строим его + // здесь -- через эту функцию проходит каждый триггер любого формата мира. IndexTriggerObjLoads(vnum, trig); return index; diff --git a/src/engine/scripting/dg_comm.cpp b/src/engine/scripting/dg_comm.cpp index 1e2825997..01335702a 100644 --- a/src/engine/scripting/dg_comm.cpp +++ b/src/engine/scripting/dg_comm.cpp @@ -28,12 +28,12 @@ char *any_one_name(char *argument, char *first_arg) { // Find length of first word /* - * - !!! - * ispunct() - * ( ). . + * Тут нужно что-то делать!!! + * Библиотечная функция ispunct() неправильно работает для русского языка + * (по крайней мере у меня). Пока закоментировал. */ - // , : UTF-8 , - // ţ (issue #3681). + // Шагаем по символу, а не по байту: под UTF-8 русская буква занимает два, и побайтовое + // приведение к нижнему регистру ломало её (issue #3681). for (arg = first_arg; *argument && !isspace(*argument) && (!ispunct(*argument) || *argument == '#' || *argument == '-');) { const std::size_t written = native_text::copy_lower_char(argument, arg); @@ -56,18 +56,18 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ switch (type[i]) { case '~': if (!otokens[i]) - strcat(sb, "-"); + strcat(sb, "Кто-то"); else if ((CharData *) otokens[i] == ch) - strcat(sb, ""); + strcat(sb, "Вы"); else strcat(sb, sight::PersonName((CharData *) otokens[i], ch, 0)); break; case '@': if (!otokens[i]) - strcat(sb, "-"); + strcat(sb, "чей-то"); else if ((CharData *) otokens[i] == ch) - strcat(sb, ""); + strcat(sb, "ваш"); else { strcat(sb, sight::PersonName((CharData *) otokens[i], ch, 1)); } @@ -75,34 +75,34 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ case '^': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, "-"); + strcat(sb, "чей-то"); else if (otokens[i] == ch) - strcat(sb, ""); + strcat(sb, "ваш"); else strcat(sb, grammar::PossessivePronoun(((CharData *) otokens[i])->get_sex())); break; case '}': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, ""); + strcat(sb, "Он"); else if (otokens[i] == ch) - strcat(sb, ""); + strcat(sb, "Вы"); else strcat(sb, grammar::PersonalPronoun(((CharData *) otokens[i])->get_sex())); break; case '*': if (!otokens[i] || !sight::CanSee(ch, (CharData *) otokens[i])) - strcat(sb, ""); + strcat(sb, "ему"); else if (otokens[i] == ch) - strcat(sb, ""); + strcat(sb, "вам"); else strcat(sb, grammar::DativePronoun(((CharData *) otokens[i])->get_sex())); break; case '`': if (!otokens[i]) - strcat(sb, "-"); + strcat(sb, "что-то"); else strcat(sb, OBJS(((ObjData *) otokens[i]), ch)); break; @@ -111,8 +111,8 @@ void sub_write_to_char(CharData *ch, char *tokens[], void *otokens[], char type[ strcat(sb, tokens[i]); strcat(sb, "\n\r"); -// , , . -// sb[0] = UPPER(sb[0]); // toupper() +// Ибо нефиг, достали прочерки ставить чтоб не аперкейсило в многостроковых сказаниях, пусть билдеры руками. +// sb[0] = UPPER(sb[0]); // Библиотечный вызов toupper() глючит для русского SendMsgToChar(sb, ch); } diff --git a/src/engine/scripting/dg_olc.cpp b/src/engine/scripting/dg_olc.cpp index 8e4650841..0cdaf5767 100644 --- a/src/engine/scripting/dg_olc.cpp +++ b/src/engine/scripting/dg_olc.cpp @@ -80,14 +80,14 @@ bool TrigeditQueueLuaFormat(DescriptorData *d, bool save_on_completion) const std::string source = OLC_STORAGE(d) ? OLC_STORAGE(d) : ""; const auto request_id = lua_scripting::QueueLuaFormat(source); if (request_id == 0) { - SendMsgToChar(" Lua . .\r\n", d->character.get()); + SendMsgToChar("Очередь форматтера Lua переполнена. Повторите позже.\r\n", d->character.get()); return false; } d->olc->lua_format_request_id = request_id; d->olc->lua_format_save_on_completion = save_on_completion; SendMsgToChar(save_on_completion - ? " Lua- . .\r\n" - : " Lua- . .\r\n", + ? "Проверка и форматирование Lua-скрипта перед сохранением запущены. Дождитесь результата.\r\n" + : "Форматирование Lua-скрипта запущено. Дождитесь результата.\r\n", d->character.get()); return true; } @@ -105,17 +105,17 @@ bool TrigeditFormatLuaBeforeSave(DescriptorData *d) bool TrigeditApplyFormattedLua(DescriptorData *d, const lua_scripting::LuaFormatResult& result) { if (!result.success) { - SendMsgToChar(d->character.get(), " / Lua: %s\r\n", result.error.c_str()); + SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: %s\r\n", result.error.c_str()); return false; } if (result.formatted.size() >= MAX_CMD_LENGTH) { - SendMsgToChar(d->character.get(), " / Lua: .\r\n"); + SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: результат слишком большой.\r\n"); return false; } RECREATE(OLC_STORAGE(d), MAX_CMD_LENGTH); memcpy(OLC_STORAGE(d), result.formatted.c_str(), result.formatted.size() + 1); OLC_VAL(d)++; - SendMsgToChar(d->character.get(), "Lua- .\r\n"); + SendMsgToChar(d->character.get(), "Lua-скрипт отформатирован.\r\n"); return true; } @@ -268,7 +268,7 @@ void ProcessLuaFormatterResults() d->olc->lua_format_save_on_completion = false; if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { if (save_on_completion) { - SendMsgToChar(" . .\r\n", + SendMsgToChar("Форматирование не применено. Исходный код триггера сохраняется.\r\n", d->character.get()); TrigeditSaveAndExit(d); } else { @@ -279,7 +279,7 @@ void ProcessLuaFormatterResults() const bool formatted = TrigeditApplyFormattedLua(d, result); if (save_on_completion) { if (!formatted) { - SendMsgToChar(" Lua- .\r\n", d->character.get()); + SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); } TrigeditSaveAndExit(d); continue; @@ -310,7 +310,7 @@ void script_save_to_disk(FILE *fp, const void *item, int type) { } /************************************************************************** - * + * Редактирование ТРИГГЕРОВ * trigedit **************************************************************************/ @@ -360,24 +360,24 @@ void trigedit_disp_menu(DescriptorData *d) { trgtypes[0] = '\0'; } std::stringstream out; - out << " " << "[&y" << OLC_NUM(d) << "&n]\r\n\r\n" - << "&g1)&n : " << "&y" <narg << "\r\n" - << "&g5)&n : " << "&y" << trig->arglist.c_str() << "\r\n" - << "&g6)&n " << (TrigeditIsLuaTrigger(trig) ? "Lua-" : "") << ":\r\n" + out << "Редактирование триггера " << "[&y" << OLC_NUM(d) << "&n]\r\n\r\n" + << "&g1)&n Название : " << "&y" <narg << "\r\n" + << "&g5)&n Аргументы : " << "&y" << trig->arglist.c_str() << "\r\n" + << "&g6)&n " << (TrigeditIsLuaTrigger(trig) ? "Lua-скрипт" : "Команды") << ":\r\n" << "&c" << OLC_STORAGE(d); if (trig->get_attach_type() == MOB_TRIGGER) { - out << "&g7)&n ? : &y" << (trig->add_flag ? "" : "") << "&n\r\n"; + out << "&g7)&n Обрабатывать команды моба в стане? : &y" << (trig->add_flag ? "ДА" : "НЕТ") << "&n\r\n"; } #if defined(WITH_LUAJIT_PROTOTYPE) - out << "&g8)&n : &y" << (TrigeditIsLuaTrigger(trig) ? "Lua" : "DG") << "&n\r\n"; + out << "&g8)&n Язык скрипта : &y" << (TrigeditIsLuaTrigger(trig) ? "Lua" : "DG") << "&n\r\n"; #endif if (TrigeditIsLuaTrigger(trig) && lua_scripting::LuaFormatterAvailable()) { - out << "&g9)&n Lua\r\n"; + out << "&g9)&n Проверить синтаксис и отформатировать Lua\r\n"; } - out << "&gQ)&n \r\n" " :"; + out << "&gQ)&n Завершить редактирование\r\n" "Введите Выбранное :"; SendMsgToChar(out.str(), d->character.get()); OLC_MODE(d) = TRIGEDIT_MAIN_MENU; } @@ -417,12 +417,12 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (tolower(*arg) == 'q') { d->olc->lua_format_request_id = 0; d->olc->lua_format_save_on_completion = false; - SendMsgToChar(" . Lua- .\r\n", + SendMsgToChar("Форматирование отменено. Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); TrigeditSaveAndExit(d); } else { - SendMsgToChar(" Lua- . " - " Q.\r\n", + SendMsgToChar("Проверка и форматирование Lua-скрипта перед сохранением еще выполняются. " + "Для отмены форматирования и сохранения исходного кода используйте Q.\r\n", d->character.get()); } return; @@ -435,7 +435,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { d->olc->lua_format_request_id = 0; d->olc->lua_format_save_on_completion = false; } else { - SendMsgToChar(" Lua- . Q.\r\n", + SendMsgToChar("Форматирование Lua-скрипта еще выполняется. Для выхода используйте Q.\r\n", d->character.get()); trigedit_disp_menu(d); return; @@ -448,7 +448,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (!GET_TRIG_TYPE(OLC_TRIG(d))) { SendMsgToChar("Invalid Trigger Type! Answer a to abort quit!\r\n", d->character.get()); } - SendMsgToChar(" ? (y/n): ", d->character.get()); + SendMsgToChar("Желаете ли вы сохранить изменения в триггере? (y/n): ", d->character.get()); OLC_MODE(d) = TRIGEDIT_CONFIRM_SAVESTRING; } else { cleanup_olc(d, CLEANUP_ALL); @@ -456,35 +456,35 @@ void trigedit_parse(DescriptorData *d, char *arg) { return; case '1': OLC_MODE(d) = TRIGEDIT_NAME; - SendMsgToChar(": ", d->character.get()); + SendMsgToChar("Название: ", d->character.get()); break; case '2': OLC_MODE(d) = TRIGEDIT_INTENDED; - SendMsgToChar("0: , 1: , 2: : ", d->character.get()); + SendMsgToChar("0: Мобы, 1: Объекты, 2: Комнаты: ", d->character.get()); break; case '3': OLC_MODE(d) = TRIGEDIT_TYPES; trigedit_disp_types(d); break; case '4': OLC_MODE(d) = TRIGEDIT_NARG; - SendMsgToChar(" : ", d->character.get()); + SendMsgToChar("Числовой аргумент: ", d->character.get()); break; case '5': OLC_MODE(d) = TRIGEDIT_ARGUMENT; - SendMsgToChar(": ", d->character.get()); + SendMsgToChar("Аргументы: ", d->character.get()); break; case '6': if (TrigeditIsLuaTrigger(OLC_TRIG(d)) && !TrigeditLuaEditingEnabled()) { - SendMsgToChar(" Lua- .\r\n", + SendMsgToChar("Редактирование Lua-скриптов недоступно в этой сборке сервера.\r\n", d->character.get()); trigedit_disp_menu(d); return; } OLC_MODE(d) = TRIGEDIT_COMMANDS; SendMsgToChar(TrigeditIsLuaTrigger(OLC_TRIG(d)) - ? " Lua- : (/s saves /h for help)\r\n\r\n" - : " : (/s saves /h for help)\r\n\r\n", + ? "Введите Lua-скрипт триггера: (/s saves /h for help)\r\n\r\n" + : "Введите команды триггера: (/s saves /h for help)\r\n\r\n", d->character.get()); d->backstr = nullptr; if (OLC_STORAGE(d)) { @@ -499,7 +499,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { case '7': if (OLC_TRIG(d)->get_attach_type() == MOB_TRIGGER) { OLC_MODE(d) = TRIGEDIT_ADDFLAG; - SendMsgToChar(" (1-): ", d->character.get()); + SendMsgToChar("Флаг (1-ДА): ", d->character.get()); } else { trigedit_disp_menu(d); } @@ -514,9 +514,9 @@ void trigedit_parse(DescriptorData *d, char *arg) { case '9': if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { - SendMsgToChar(" Lua-.\r\n", d->character.get()); + SendMsgToChar("Автоформат доступен только для Lua-триггеров.\r\n", d->character.get()); } else if (!lua_scripting::LuaFormatterAvailable()) { - SendMsgToChar(" Lua .\r\n", d->character.get()); + SendMsgToChar("Форматтер Lua недоступен в этой сборке.\r\n", d->character.get()); } else if (TrigeditFormatLua(d)) { return; } @@ -535,7 +535,7 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (TrigeditFormatLuaBeforeSave(d)) { return; } - SendMsgToChar(" Lua- .\r\n", d->character.get()); + SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); } TrigeditSaveAndExit(d); return; @@ -546,8 +546,8 @@ void trigedit_parse(DescriptorData *d, char *arg) { case 'a': // abort quitting break; - default: SendMsgToChar(" !\r\n", d->character.get()); - SendMsgToChar(" ? : ", d->character.get()); + default: SendMsgToChar("Неверный выбор!\r\n", d->character.get()); + SendMsgToChar("Желаете ли вы сохранить изменения в триггере? : ", d->character.get()); return; } break; @@ -581,17 +581,17 @@ void trigedit_parse(DescriptorData *d, char *arg) { if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { OLC_TRIG(d)->set_script_language(TriggerScriptLanguage::Lua); strcpy(OLC_STORAGE(d), kDefaultLuaTriggerTemplate); - SendMsgToChar(" Lua.\r\n", d->character.get()); + SendMsgToChar("Тело триггера заменено на шаблон Lua.\r\n", d->character.get()); OLC_VAL(d)++; } } else if (TrigeditIsLuaTrigger(OLC_TRIG(d))) { OLC_TRIG(d)->set_script_language(TriggerScriptLanguage::Dg); strcpy(OLC_STORAGE(d), kDefaultDgTriggerTemplate); - SendMsgToChar(" DG.\r\n", d->character.get()); + SendMsgToChar("Тело триггера заменено на шаблон DG.\r\n", d->character.get()); OLC_VAL(d)++; } #else - SendMsgToChar("Lua- .\r\n", d->character.get()); + SendMsgToChar("Lua-триггеры недоступны в этой сборке сервера.\r\n", d->character.get()); #endif break; @@ -629,7 +629,7 @@ void sprintbyts(int data, char *dest) { } void TriggerDistribution(DescriptorData *d) { switch (OLC_TRIG(d)->get_attach_type()) { - case WLD_TRIGGER: // + case WLD_TRIGGER: // в комнатах ссылка на прототип for (RoomRnum nr = kFirstRoom; nr <= top_of_world; nr++) { if (!SCRIPT(world[nr])->has_triggers()) continue; @@ -689,7 +689,7 @@ void trigedit_save(DescriptorData *d) { DescriptorData *dsc; if (TrigeditIsLuaTrigger(trig) && !TrigeditLuaEditingEnabled()) { - SendMsgToChar(": Lua- .\r\n", d->character.get()); + SendMsgToChar("ОШИБКА: Lua-триггеры недоступны в этой сборке сервера.\r\n", d->character.get()); return; } @@ -698,9 +698,9 @@ void trigedit_save(DescriptorData *d) { TrigeditApplyStorageToTrigger(trig, storage_str); if ((trig_rnum = GetTriggerRnum(OLC_NUM(d))) != -1) { - // . + // Этот триггер уже есть. - // + // Очистка прототипа proto = trig_index[trig_rnum]->proto; proto->cmdlist.reset(); @@ -718,14 +718,14 @@ void trigedit_save(DescriptorData *d) { trigger->arglist.clear(); trigger->set_name(""); if (GET_TRIG_WAIT(trigger).time_remaining > 0) { - free(GET_TRIG_WAIT(trigger).info); // + free(GET_TRIG_WAIT(trigger).info); // Причина уже обсуждалась remove_event(GET_TRIG_WAIT(trigger)); } #if defined(WITH_LUAJIT_PROTOTYPE) - // issue #3568: Lua- - // (LuaWaitState Trigger*). (*trigger = *proto), - // Lua->DG, - Σ - // -> UAF/ . Lua-, ExtractTrigger. + // issue #3568: у живого инстанса может висеть приостановленная Lua-корутина + // (LuaWaitState хранит Trigger*). Перезапись тела ниже (*trigger = *proto), + // особенно при смене Lua->DG, оставила бы вейт-реестр ссылаться на изменённый + // триггер -> UAF/порча кучи. Отменяем Lua-вейты, как это делает ExtractTrigger. lua_scripting::LuaScriptEngine::CancelWaitsForTrigger(trigger); #endif @@ -752,12 +752,12 @@ void trigedit_save(DescriptorData *d) { new_index[trig_rnum]->func = nullptr; new_index[trig_rnum]->proto = new Trigger(*trig); --i; - continue; // , - + continue; // повторить копирование еще раз, но уже по-другому } else { new_index[i] = trig_index[i]; } } else { - // + // докопировать new_index[i + 1] = trig_index[i]; proto = trig_index[i]->proto; proto->set_rnum(i + 1); @@ -794,9 +794,9 @@ void trigedit_save(DescriptorData *d) { } } - // " -> " , - // -- 'vnum trig' - // . + // Индекс "предмет -> грузящие его триггеры" строится при загрузке мира, + // так что для правленого скрипта его надо пересчитать -- иначе 'vnum trig' + // будет показывать старую картину до ближайшей перезагрузки. ReindexTriggerObjLoads(OLC_NUM(d), trig_index[trig_rnum]->proto); // Save trigger to disk using data source abstraction (YAML/SQLite/Legacy) @@ -805,11 +805,11 @@ void trigedit_save(DescriptorData *d) { auto* data_source = world_loader::WorldDataSourceManager::Instance().GetDataSource(); if (!data_source->SaveTriggers(OLC_ZNUM(d), OLC_NUM(d), notify_level)) { - SendMsgToChar(": !\r\n", d->character.get()); + SendMsgToChar("ОШИБКА: Не удалось сохранить триггер!\r\n", d->character.get()); return; } - SendMsgToChar(" .\r\n", d->character.get()); + SendMsgToChar("Триггер сохранен.\r\n", d->character.get()); } // Save all triggers for a zone to disk (without requiring DescriptorData) @@ -832,7 +832,7 @@ bool trigedit_save_to_disk(int zone_rnum, int notify_level) { top = zone_table[zone_rnum].top; if (zone >= dungeons::kZoneStartDungeons) { - snprintf(buf, sizeof(buf), " %d .", zone); + snprintf(buf, sizeof(buf), "Отказ сохранения зоны %d на диск.", zone); mudlog(buf, CMP, kLvlGreatGod, SYSLOG, true); return false; } @@ -948,19 +948,19 @@ void trigedit_create_index(int znum, const char *type) { /************************************************************************** - * + * Редактирование ПРОТОТИПОВ СКРИПТОВ * trigedit **************************************************************************/ void dg_olc_script_free(DescriptorData *d) -// OLC_SCRIPT +// Удаление прототипа в OLC_SCRIPT { OLC_SCRIPT(d).clear(); } void dg_olc_script_copy(DescriptorData *d) -// // +// Создание копии прототипа скрипта для текщего редактируемого моба/объекта/комнаты { switch (OLC_ITEM_TYPE(d)) { case MOB_TRIGGER: OLC_SCRIPT(d) = *OLC_MOB(d)->proto_script; @@ -1006,11 +1006,11 @@ void dg_script_menu(DescriptorData *d) { } snprintf(buf, sizeof(buf), "\r\n" - " %sN%s) \r\n" - " %sD%s) \r\n" - " %sX%s) \r\n" - " %sQ%s) ( ) \r\n\r\n" - " :", grn, nrm, grn, nrm, grn, nrm, grn, nrm); + " %sN%s) Новый триггер для этого скрипта\r\n" + " %sD%s) Удалить триггер в этом скрипте\r\n" + " %sX%s) Выйти из редактора скрипта\r\n" + " %sQ%s) Выйти из редактора скрипта (без сохранения) \r\n\r\n" + " Введите выбранное :", grn, nrm, grn, nrm, grn, nrm, grn, nrm); SendMsgToChar(buf, d->character.get()); } @@ -1028,7 +1028,7 @@ int dg_script_edit_parse(DescriptorData *d, char *arg) { } else { OLC_SCRIPT(d).swap(*OLC_ROOM(d)->proto_script); } - // break + // тут break не нужен // fall through case 'q': dg_olc_script_free(d); @@ -1063,8 +1063,8 @@ int dg_script_edit_parse(DescriptorData *d, char *arg) { } if (GetTriggerRnum(vnum) < 0) { - SendMsgToChar(" VNUM !\r\n" - ", , vnum (ex: 1, 200):", d->character.get()); + SendMsgToChar("Неверный VNUM триггера!\r\n" + "Пожалуйста, выберите позицию, vnum (ex: 1, 200):", d->character.get()); return 1; } diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 21085f79f..6a8b56ce7 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -72,7 +72,7 @@ constexpr long long kPulsesPerMudHour = kSecsPerMudHour*kPassesPerSec; inline bool IS_CHARMED(CharData* ch) {return (mount::IsHorse(ch) || AFF_FLAGGED(ch, EAffect::kCharmed));} -// DGScript +// Вывод сообщений о неверных управляющих конструкциях DGScript #define DG_CODE_ANALYZE // external vars from triggers.cpp @@ -83,7 +83,7 @@ int last_trig_vnum = 0; int curr_trig_vnum = 0; int last_trig_line_num = 0; -// +// списки сущностей с триггерами смены времени std::unordered_set timechange_mobs; std::unordered_set timechange_objs; std::unordered_set timechange_rooms; @@ -132,7 +132,7 @@ int trgvar_in_room(int vnum); void do_worldecho(char *msg); /* -, . +Костыль, но всеж. */ bool CharacterLinkDrop = false; @@ -193,7 +193,7 @@ void script_log(const char *msg, LogMode type) { */ void trig_log(Trigger *trig, std::string msg, LogMode type) { char tmpbuf[kMaxStringLength]; - snprintf(tmpbuf, kMaxStringLength, "(Trigger: %s, VNum: %d) : %s [: %d]", GET_TRIG_NAME(trig), + snprintf(tmpbuf, kMaxStringLength, "(Trigger: %s, VNum: %d) : %s [строка: %d]", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig), msg.c_str(), last_trig_line_num); script_log(tmpbuf, type); } @@ -206,7 +206,7 @@ cmdlist_element::shared_ptr find_continue_done(cmdlist_element::shared_ptr cl); std::list worlds_vars; -// "wat" "mat" +// Для отслеживания работы команд "wat" и "mat" int reloc_target = -1; Trigger *cur_trig = nullptr; @@ -411,8 +411,8 @@ RoomData *find_room(long n) { ************************************************************/ /** - * id . - * \param num - 0 - id , . + * Возвращает id моба указанного внума. + * \param num - если есть и больше 0 - возвращает id не первого моба, а указанного порядкового номера. */ int find_id_by_char_vnum(int vnum, int num = 0) { int count = 0; @@ -430,8 +430,8 @@ int find_id_by_char_vnum(int vnum, int num = 0) { } /** - * id . - * \param num - 0 - id , . + * Возвращает id объекта указанного внума. + * \param num - если есть и больше 0 - возвращает id не первого моба, а указанного порядкового номера. */ int find_id_by_obj_vnum(int vnum, int num = 0) { int count = 0; @@ -449,8 +449,8 @@ int find_id_by_obj_vnum(int vnum, int num = 0) { } // return room with VNUM n -// ! UID = ROOM_ID_BASE+VNUM, .. -// RNUM OLC +// Внимание! Для комнаты UID = ROOM_ID_BASE+VNUM, т.к. +// RNUM может быть независимо изменен с помощью OLC int find_vnumum(long n) { // return (GetRoomRnum (n) != kNowhere) ? ROOM_ID_BASE + n : -1; return (GetRoomRnum(n) != kNowhere) ? n : -1; @@ -464,11 +464,11 @@ int find_room_uid(long n) { * generic searches based only on name ************************************************************/ -// - ( UID-) -- vnum, /UID. -// process_attach/detach/run -// "detach %vnum%" -- O(N) -// character_list / object_list, get_room. -// . issue #3232 (488 4). +// Чисто-цифровая строка (без UID-префикса) -- это vnum, а не имя/UID. +// Используется в process_attach/detach/run для предупреждения билдеров +// о паттернах вида "detach %vnum%" -- они проваливаются в O(N) +// сканы по character_list / object_list, прежде чем дойти до get_room. +// См. issue #3232 (488мс на одну зону вместо 4мс). bool is_plain_vnum_string(const char *name) { if (!name || !*name) { return false; @@ -485,7 +485,7 @@ bool is_plain_vnum_string(const char *name) { CharData *get_char(const char *name) { CharData *i; - // UID-. + // Отсекаем поиск левых UID-ов. if ((*name == UID_OBJ) || (*name == UID_ROOM)) return nullptr; @@ -794,8 +794,8 @@ void script_trigger_check(int mode) { } } -// -// +// проверка каждый час на триги изменении времени +// итерация по спискам зарегистрированных сущностей вместо перебора всего мира void script_timechange_trigger_check(const int time, const int time_day) { utils::CExecutionTimer timercheck; @@ -831,16 +831,16 @@ EVENT(trig_wait_event) { go = wait_event_obj->go; type = wait_event_obj->type; GET_TRIG_WAIT(trig).time_remaining = 0; - // issue #3523: from_current - - // . , - // " " . script_driver: - // trig ̣ (remove_trigger -> ExtractTrigger). - // - script_driver ģ (curr_line), - // ף (GET_TRIG_DEPTH) (is_halted); - // - (in_room != kNowhere: extract_char - // , script_driver ); - // - : wait - // . + // issue #3523: from_current выставляется только при паузе триггера из-за стана + // моба. Логируем возобновление лишь когда триггер РЕАЛЬНО продолжится, иначе + // лог о "продолжил работу" был бы ложным. Проверять надо ДО script_driver: + // после него trig может быть уже удалён (remove_trigger -> ExtractTrigger). + // - цикл script_driver зайдёт только если есть строка (curr_line), триггер + // не свёрнут (GET_TRIG_DEPTH) и не остановлен (is_halted); + // - моб не выведен из мира (in_room != kNowhere: extract_char уже вынул бы + // его из комнаты, а script_driver ничего не выполнит); + // - моб вышел из стана: иначе интерпретатор снова повесит wait и команда + // опять не выполнится. if (wait_event_obj->from_current && type == MOB_TRIGGER && go && trig && trig->curr_line && GET_TRIG_DEPTH(trig) && !trig->is_halted()) { auto *mob = static_cast(go); @@ -848,7 +848,7 @@ EVENT(trig_wait_event) { && !AFF_FLAGGED(mob, EAffect::kHold) && !AFF_FLAGGED(mob, EAffect::kStopFight) && !AFF_FLAGGED(mob, EAffect::kMagicStopFight)) { - mudlog(fmt::format("DG: {} #{} {} #{}", + mudlog(fmt::format("DG: триггер {} #{} продолжил работу после лага моба {} #{}", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig), GET_SHORT(mob), GET_MOB_VNUM(mob)), CMP, kLvlGod, SYSLOG, true); } @@ -888,7 +888,7 @@ void do_stat_trigger(CharData *ch, Trigger *trig, bool need_num) { if (trig->get_attach_type() == MOB_TRIGGER) { snprintf(sb, sizeof(sb), "Trigger Type: %s, Numeric Arg: %d, Execute mob command: %s, Arg list: %s\r\n", - buf, GET_TRIG_NARG(trig), trig->add_flag ? "" : "", !trig->arglist.empty() ? trig->arglist.c_str() : "None"); + buf, GET_TRIG_NARG(trig), trig->add_flag ? "ДА" : "НЕТ", !trig->arglist.empty() ? trig->arglist.c_str() : "None"); } else { snprintf(sb, sizeof(sb), "Trigger Type: %s, Numeric Arg: %d, Arg list: %s\r\n", buf, GET_TRIG_NARG(trig), !trig->arglist.empty() ? trig->arglist.c_str() : "None"); @@ -944,15 +944,15 @@ void find_uid_name(const char *uid, char *name, size_t name_size) { const auto FOREACH_LIST_GUID = "{18B3D8D1-240E-4D60-AEAB-6748580CA460}"; const auto FOREACH_LIST_POS_GUID = "{4CC4E031-7376-4EED-AD4F-2FD0DC8D4E2D}"; -// dg- ( foreach) -// , +// некоторые dg-функции (например foreach) создают внутренние переменные для работы +// выводим их в удобочитаемой форме, чтоб никого не смущали непонятные переменные в выводе static std::string print_variable_name(const std::string &name) { static std::map text_mapping_list = { {FOREACH_LIST_GUID, "[list] "}, {FOREACH_LIST_POS_GUID, "[position] "} }; - // / foreach + // включение/отключение вывода внутреннего состояния foreach const bool display_state_vars = true; std::string result = name; @@ -1015,7 +1015,7 @@ void script_stat(CharData *ch, Script *sc) { std::stringstream buffer; if (t->get_attach_type() == MOB_TRIGGER) { buffer << " Trigger Type: " << buf1 << ", Numeric Arg:" << GET_TRIG_NARG(t) - << " , Execute mob command: " << (t->add_flag ? "" : "") + << " , Execute mob command: " << (t->add_flag ? "ДА" : "НЕТ") << " , Arg list:" << (!t->arglist.empty() ? t->arglist.c_str() : "None"); } else { buffer << " Trigger Type: " << buf1 << ", Numeric Arg:" << GET_TRIG_NARG(t) @@ -1253,7 +1253,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { room = world[ch->in_room]; if (!SCRIPT(room)->has_triggers()) { SendMsgToChar("This room does not have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { room->cleanup_script(); SendMsgToChar("All triggers removed from room.\r\n", ch); @@ -1298,7 +1298,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar("Players don't have triggers.\r\n", ch); } else if (!SCRIPT(victim)->has_triggers()) { SendMsgToChar("That mob doesn't have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { victim->cleanup_script(); snprintf(buf, sizeof(buf), "All triggers removed from %s.\r\n", GET_SHORT(victim)); timechange_unregister_mob(victim); @@ -1314,7 +1314,7 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else if (object) { if (!object->get_script()->has_triggers()) { SendMsgToChar("That object doesn't have any triggers.\r\n", ch); - } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "")) { + } else if (!str_cmp(arg2, "all") || !str_cmp(arg2, "все")) { object->cleanup_script(); snprintf(buf, sizeof(buf), "All triggers removed from %s.\r\n", !object->get_short_description().empty() ? object->get_short_description().c_str() @@ -1334,13 +1334,13 @@ void do_detach(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { void add_var_cntx(std::list &var_list, std::string name, std::string value, long id) { /*++ - ( ). - 0. + Добавление переменной в список с учетом контекста (СТРОГИЙ поиск). + При добавлении в список локальных переменных контекст должен быть 0. - var_list - - name - - value - - id - + var_list - список переменных + name - имя переменной + value - значение переменной + id - контекст переменной --*/ TriggerVar vd; @@ -1358,17 +1358,17 @@ void add_var_cntx(std::list &var_list, std::string name, std::string } TriggerVar find_var_cntx(std::list var_list, std::string name, long id) { -/* ( ). +/* Поиск переменной с учетом контекста (НЕСТРОГИЙ поиск). - :. - 1. name - 2. id, - , - 0. + Поиск осуществляется по паре ИМЯ:КОНТЕКСТ. + 1. Имя переменной должно совпадать с параметром name + 2. Контекст переменной должен совпадать с параметром id, если + такой переменной нет, производится попытка найти переменную + с контекстом 0. - var_list - - name - - id - + var_list - указатель на первый элемент списка переменных + name - имя переменной + id - контекст переменной --*/ utils::ConvertToLow(name); auto it = std::find_if(var_list.begin(), var_list.end(), [&name, id](TriggerVar vd) { return (vd.name == name) && (vd.context == id); }); @@ -1380,17 +1380,17 @@ TriggerVar find_var_cntx(std::list var_list, std::string name, long int remove_var_cntx(std::list &var_list, std::string name, long id) { /*++ - ( ). + Удаление переменной из списка с учетом контекста (СТРОГИЙ поиск). - . + Поиск строгий. - var_list - - name - - id - + var_list - список переменных + name - имя переменной + id - контекст переменной - : - 1 - - 0 - + Возвращает: + 1 - переменная найдена и удалена + 0 - переменная не найдена --*/ utils::ConvertToLow(name); @@ -1414,7 +1414,7 @@ bool CheckSript(const RoomData *go, const long type) { return !SCRIPT(go)->is_purged() && SCRIPT(go)->has_triggers() && IS_SET(SCRIPT_TYPES(go->script), type); } -// * +// * Изменение указанной целочисленной константы long gm_char_field(CharData *ch, char *field, char *subfield, long val) { int tmpval; if (*subfield) { @@ -1452,7 +1452,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t snprintf(str, str_size, "%s", utils::TrimCopy(vd.value).c_str()); return true; } else if (!str_cmp(field, "fullword")) { -// +//не работает с русским /* std::string word = subfield; std::string sentence = vd->value; std::regex rgx("\\b" + word + "\\b"); @@ -1463,9 +1463,9 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t std::istringstream is(sentence); std::string temp; while(is >> temp) - tmpstr.push_back(temp); // + tmpstr.push_back(temp); // перенесем фразу в вектор if (std::find(tmpstr.begin(), tmpstr.end(), subfield) != tmpstr.end()) -// +// вариант номер два /* std::string sentence = vd->value; std::stringstream parser(sentence); std::istream_iterator start(parser); @@ -1503,7 +1503,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t return true; } else if (!str_cmp(field, "words")) { int n = 0; - // + // Подсчет количества слов или получение слова char buf1[kMaxTrglineLength]; char buf2[kMaxTrglineLength]; buf1[0] = 0; @@ -1546,7 +1546,7 @@ int text_processed(char *field, char *subfield, TriggerVar vd, char *str, size_t return false; } -// dg_*cmd.cpp - world.loadobj ( ) +// Команды загрузки из dg_*cmd.cpp - для world.loadobj (загрузка по типу триггера) void do_mload(CharData *ch, char *argument, int cmd, int subcmd, Trigger *trig); void do_dgoload(ObjData *obj, char *argument, int cmd, int subcmd, Trigger *trig); void do_wload(RoomData *room, char *argument, int cmd, int subcmd, Trigger *trig); @@ -1588,7 +1588,7 @@ void find_replacement(void *go, const char *spellturntemp[] = {"mspellturntemp", "ospellturntemp", "wspellturntemp"}; if (!subfield) { - subfield = nullptr; // + subfield = nullptr; // Чтобы проверок меньше было } // X.global() will have a NULL trig @@ -1607,7 +1607,7 @@ void find_replacement(void *go, } else { if (!str_cmp(var, "self")) { long uid; - // UID + // заменить на UID switch (type) { case MOB_TRIGGER: uid = ((CharData *) go)->get_uid(); uid_type = UID_CHAR; @@ -1724,7 +1724,7 @@ void find_replacement(void *go, } else if (!str_cmp(var, "exist")) { if (!str_cmp(field, "mob") && (num = atoi(subfield)) > 0) { if (GetMobRnum(num) <= 0) { - trig_log(trig, fmt::format(" vnum ({}) exist.mob", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в exist.mob", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1734,7 +1734,7 @@ void find_replacement(void *go, auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format(" vnum ({}) exist.obj", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в exist.obj", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1758,7 +1758,7 @@ void find_replacement(void *go, auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format(" vnum ({}) curobjs", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в curobjs", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1772,7 +1772,7 @@ void find_replacement(void *go, int count = 0; if (rnum <= 0) { - trig_log(trig, fmt::format(" vnum ({}) gameobjs", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в gameobjs", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1790,7 +1790,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format(" {} ", num)); + trig_log(trig, fmt::format("В зоне {} нет комнат", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1804,7 +1804,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format(" {} ", num)); + trig_log(trig, fmt::format("В зоне {} нет комнат", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1831,7 +1831,7 @@ void find_replacement(void *go, int from =0, to = 0; auto result = GetZoneRooms(GetZoneRnum(num), &from , &to); if (result == 0) { - trig_log(trig, fmt::format(" {} ", num)); + trig_log(trig, fmt::format("В зоне {} нет комнат", num)); } for (RoomRnum rrn = from; rrn <= to; rrn++) { for (const auto ch : world[rrn]->people) { @@ -1854,20 +1854,20 @@ void find_replacement(void *go, } else if (!str_cmp(field, "recalczone")) { auto param = utils::Split(subfield, ','); if (param.size() != 4) { - trig_log(trig, "recalc: "); + trig_log(trig, "recalc: несоответствие параметров"); return; } ZoneRnum zrn = GetZoneRnum(atoi(param[0].c_str())); if (zrn == 0) { - trig_log(trig, "recalc: "); + trig_log(trig, "recalc: нет такой зоны"); return; } - // , , log() syslog, - // trig_log(), ERRLOG. - - // (copy_from_zone): , . - // -- trig_log, , - // . - log(fmt::format(" {} ( {}) {} {} {} [ {}, VNum: {}]", + // Строка нужна для разбора по логу, а не богам онлайн, поэтому log() в syslog, а не + // trig_log(), который дублирует запись в канал ERRLOG. Заодно пишем зону-оригинал + // (copy_from_zone): по одному номеру инстанса данжа непонятно, что пересчитывается. + // Имя и внум триггера подставляем сами -- их давал trig_log, а без них не найти, + // кто затеял пересчет. + log(fmt::format("Пересчитываю зону {} (оригинал {}) {} {} {} [триггер {}, VNum: {}]", param[0], zone_table[zrn].copy_from_zone, param[1], param[2], param[3], GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig))); auto value = param[0] + " " + param[1]+ " " + param[2] + " " +param[3]; @@ -1876,7 +1876,7 @@ void find_replacement(void *go, for (auto rrn = zone_table[zrn].RnumRoomsLocation.first; rrn <= zone_table[zrn].RnumRoomsLocation.second; rrn++) { dungeons::ClearRoom(world[rrn]); } - dungeons::ClearRoom(world[zone_table[zrn].RnumRoomsLocation.second + 1]); // + dungeons::ClearRoom(world[zone_table[zrn].RnumRoomsLocation.second + 1]); //виртуалку zone_repop_list.push_back(zrn); DecayObjectsOnRepop(zone_repop_list); ResetZone(zrn); @@ -1913,7 +1913,7 @@ void find_replacement(void *go, UniqueList zone_repop_list; ZoneRnum zrn = GetZoneRnum(num); if (zrn < 0) { - trig_log(trig, " zreset"); + trig_log(trig, "Некорректный номер зоны в zreset"); return; } zone_repop_list.push_back(zrn); @@ -1943,7 +1943,7 @@ void find_replacement(void *go, const auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format(" vnum ({}) canbeloaded", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в canbeloaded", num).c_str()); snprintf(str, str_size, "0"); return; } @@ -1957,18 +1957,18 @@ void find_replacement(void *go, snprintf(str, str_size, "0"); } } else if (!str_cmp(field, "loadobj") && num > 0) { - // world.loadobj(vnum, chance): chance (1..1000 ), - // MIW. load . - // "1" , "0". - // %LoadedUid% ( load-). + // world.loadobj(vnum, chance): загрузить предмет с шансом chance (1..1000 промилле), + // учитывая MIW. Размещение делает штатная команда load по типу триггера. + // Возвращает "1" при загрузке, иначе "0". Загруженный предмет доступен через + // %LoadedUid% (выставляет load-команда). const auto rnum = GetObjRnum(num); if (rnum <= 0) { - trig_log(trig, fmt::format(" vnum ({}) loadobj", num).c_str()); + trig_log(trig, fmt::format("Указан неверный параметр vnum ({}) в loadobj", num).c_str()); snprintf(str, str_size, "0"); return; } - // () vnum subfield + // второй аргумент (шанс) после vnum в subfield const char *p = subfield; while (*p && *p != ',' && *p != ' ') { p++; @@ -1979,11 +1979,11 @@ void find_replacement(void *go, const int chance = atoi(p); if (chance < 1 || chance > 1000) { - trig_log(trig, fmt::format(" ({}) loadobj, 1..1000", chance).c_str()); + trig_log(trig, fmt::format("Неверный шанс ({}) в loadobj, нужно 1..1000", chance).c_str()); snprintf(str, str_size, "0"); return; } - // ( CanBeLoaded) + // проверка максимума в мире (логика как в CanBeLoaded) const bool can_load = stable_objs::IsTimerUnlimited(obj_proto[rnum].get()) || (GetObjMIW(rnum) < 0) || (obj_proto.actual_count(rnum) < GetObjMIW(rnum)); @@ -1992,12 +1992,12 @@ void find_replacement(void *go, snprintf(str, str_size, "0"); return; } - // + // бросок шанса if (number(1, 1000) > chance) { snprintf(str, str_size, "0"); return; } - // load + // размещение штатной командой load в зависимости от типа триггера char loadarg[kMaxInputLength]; snprintf(loadarg, sizeof(loadarg), "obj %d", num); switch (type) { @@ -2010,8 +2010,8 @@ void find_replacement(void *go, } else if ((!str_cmp(field, "maxobj") || !str_cmp(field, "maxobjs")) && num > 0) { num = GetObjRnum(num); if (num >= 0) { - // ., - // + // если у прототипа беск.таймер, + // то их оч много в мире if (stable_objs::IsTimerUnlimited(obj_proto[num].get()) || (GetObjMIW(num) < 0)) snprintf(str, str_size, "9999999"); else @@ -2047,24 +2047,24 @@ void find_replacement(void *go, } } else if (!str_cmp(field, "sunlight")) { switch (weather_info.sunlight) { - case kSunDark: snprintf(str, str_size, ""); + case kSunDark: snprintf(str, str_size, "ночь"); break; - case kSunSet: snprintf(str, str_size, ""); + case kSunSet: snprintf(str, str_size, "закат"); break; - case kSunLight: snprintf(str, str_size, ""); + case kSunLight: snprintf(str, str_size, "день"); break; - case kSunRise: snprintf(str, str_size, ""); + case kSunRise: snprintf(str, str_size, "рассвет"); break; } } else if (!str_cmp(field, "season")) { switch (weather_info.season) { - case ESeason::kWinter: snprintf(str, str_size, ""); + case ESeason::kWinter: snprintf(str, str_size, "зима"); break; - case ESeason::kSpring: snprintf(str, str_size, ""); + case ESeason::kSpring: snprintf(str, str_size, "весна"); break; - case ESeason::kSummer: snprintf(str, str_size, ""); + case ESeason::kSummer: snprintf(str, str_size, "лето"); break; - case ESeason::kAutumn: snprintf(str, str_size, ""); + case ESeason::kAutumn: snprintf(str, str_size, "осень"); break; } } @@ -2216,7 +2216,7 @@ void find_replacement(void *go, ++count; } } - snprintf(str, str_size, "%d", count + 1); // +1 + snprintf(str, str_size, "%d", count + 1); // +1 за первое слово return; } *(p++) = '\0'; @@ -2250,7 +2250,7 @@ void find_replacement(void *go, std::vector tokens = utils::Split(arg, ','); if (tokens.size() < 2 || tokens.size() > 3) { - snprintf(buf, sizeof(buf), "array.find: "); + snprintf(buf, sizeof(buf), "array.find: путанница в количестве аргументов"); trig_log(trig, buf); return; } @@ -2259,12 +2259,12 @@ void find_replacement(void *go, index = std::stoi(tokens.at(2)); } catch (const std::invalid_argument &) { - snprintf(buf, sizeof(buf), "array.find: index , '%s'", tokens.at(2).c_str()); + snprintf(buf, sizeof(buf), "array.find: index кривой, указано '%s'", tokens.at(2).c_str()); trig_log(trig, buf); return; } if (index < 0) { - snprintf(buf, sizeof(buf), "array.find: index , '%s'", tokens.at(2).c_str()); + snprintf(buf, sizeof(buf), "array.find: index меньше нуля, указано '%s'", tokens.at(2).c_str()); trig_log(trig, buf); return; } @@ -2272,7 +2272,7 @@ void find_replacement(void *go, std::vector arr = utils::Split(tokens.at(0)); std::string elem = tokens.at(1); if (index > static_cast(arr.size())) { - snprintf(buf, sizeof(buf), "%s", "array.find: index "); + snprintf(buf, sizeof(buf), "%s", "array.find: index больше размера массива"); trig_log(trig, buf); return; } @@ -2295,7 +2295,7 @@ void find_replacement(void *go, int index = 0; if (tokens.size() != 2) { - snprintf(buf, sizeof(buf), "array.remove: "); + snprintf(buf, sizeof(buf), "array.remove: путанница в количестве аргументов"); trig_log(trig, buf); return; } @@ -2303,22 +2303,22 @@ void find_replacement(void *go, index = std::stoi(tokens.at(1)); } catch (const std::invalid_argument &) { - snprintf(buf, sizeof(buf), "array.remove: index , '%s'", tokens.at(1).c_str()); + snprintf(buf, sizeof(buf), "array.remove: index кривой или отсуствует, указано '%s'", tokens.at(1).c_str()); trig_log(trig, buf); return; } if (index < 1) { - snprintf(buf, sizeof(buf), "array.remove: index , '%s'", tokens.at(1).c_str()); + snprintf(buf, sizeof(buf), "array.remove: index меньше единицы, указано '%s'", tokens.at(1).c_str()); trig_log(trig, buf); return; } std::vector arr = utils::Split(tokens.at(0)); if (index > static_cast(arr.size())) { - snprintf(buf, sizeof(buf), "%s", "array.remove: index "); + snprintf(buf, sizeof(buf), "%s", "array.remove: index больше размера массива"); trig_log(trig, buf); return; } - index--; // DG 1 + index--; // в DG массивы с 1 auto it = arr.begin(); std::advance(it, index); arr.erase(it); @@ -2332,7 +2332,7 @@ void find_replacement(void *go, snprintf(str, str_size, "%s", ss.c_str()); return; } - /* : + /*Вот тут можно наделать вот так еще: else if (!str_cmp(field, "pop")) {} else if (!str_cmp(field, "shift")) {} else if (!str_cmp(field, "push")) {} @@ -2403,7 +2403,7 @@ void find_replacement(void *go, mob->quested_remove(num); return; } else { - trig_log(trig, " unsetquest"); + trig_log(trig, "Ошибка в параметрах unsetquest"); return; } } @@ -2507,7 +2507,7 @@ void find_replacement(void *go, else snprintf(str, str_size, "%d", mob->get_hit_add()); } else if (!str_cmp(field, "maxhitp")) { - if (*subfield && mob->IsNpc()) // + if (*subfield && mob->IsNpc()) // доступно тока мобам mob->set_max_hit((int) gm_char_field(mob, field, subfield, (long) mob->get_max_hit())); else snprintf(str, str_size, "%d", mob->get_max_hit()); @@ -2590,9 +2590,9 @@ void find_replacement(void *go, snprintf(str, str_size, "%d", GET_AC_ADD(mob)); } else if (!str_cmp(field, "ac")) { snprintf(str, str_size, "%d", GetRealAc(mob)); - } else if (!str_cmp(field, "morale")) { // + } else if (!str_cmp(field, "morale")) { // общая сумма морали snprintf(str, str_size, "%d", mob->calc_morale()); - } else if (!str_cmp(field, "moraleadd")) {// + } else if (!str_cmp(field, "moraleadd")) {// добавочная мораль if (*subfield) GET_MORALE(mob) = (int) gm_char_field(mob, field, subfield, (long) GET_MORALE(mob)); else @@ -2610,7 +2610,7 @@ void find_replacement(void *go, } else if (!str_cmp(field, "linkdrop")) { if (!mob->IsNpc() && !mob->desc) { snprintf(str, str_size, "1"); - CharacterLinkDrop = false; // + CharacterLinkDrop = false; // чтоб триггер тут не прерывался для упавших в ЛД } else snprintf(str, str_size, "0"); @@ -2630,19 +2630,19 @@ void find_replacement(void *go, } else if ((!str_cmp(field, "restore")) || (!str_cmp(field, "fullrestore"))) { if (!str_cmp(field, "fullrestore")) { DoArenaRestore(mob, (char *) mob->get_name().c_str(), 0, kScmdRestoreTrigger); - trig_log(trig, " DoArenaRestore!"); + trig_log(trig, "был произведен вызов DoArenaRestore!"); } else { DoRestore(mob, (char *) mob->get_name().c_str(), 0, kScmdRestoreTrigger); - trig_log(trig, " DoRestore!"); + trig_log(trig, "был произведен вызов DoRestore!"); } } else if (!str_cmp(field, "dispel")) { if (!mob->affected.empty()) { - SendMsgToChar(" !\r\n", mob); + SendMsgToChar("Вы словно заново родились!\r\n", mob); mob->affected.clear(); affect_total(mob); } } else if (!str_cmp(field, "Currency")) { - // %actor.Currency()% - - ( id-). + // %actor.Currency()% - кол-во любой валюты на руках (валюта задаётся id-аргументом). const auto *money_cur = ¤cies::FindByTextIdNoCase(subfield); if (money_cur->GetId() < 0) { char errbuf[kMaxInputLength]; @@ -2651,7 +2651,7 @@ void find_replacement(void *go, } snprintf(str, str_size, "%ld", money_cur->GetId() >= 0 ? currencies::GetHand(*mob, money_cur->GetTextId()) : 0L); } else if (!str_cmp(field, "AddCurrency")) { - // %actor.AddCurrency(, )% - ( ). immortal- ( ). + // %actor.AddCurrency(, )% - начислить (минус снимает). Слава и immortal-валюты не начисляются (страж откажет). auto args = utils::Split(subfield, ','); if (args.size() >= 2) { utils::TrimLeft(args[0]); @@ -2679,7 +2679,7 @@ void find_replacement(void *go, currencies::SetHand(*mob, currencies::kGold, std::max(long(0), gm_char_field(mob, field, subfield, currencies::GetHand(*mob, currencies::kGold)))); value = currencies::GetHand(*mob, currencies::kGold) - before; snprintf(buf, sizeof(buf), - "<%s> {%d} %d %s. [Trigger: %s, Vnum: %d]", + "<%s> {%d} получил триггером %d %s. [Trigger: %s, Vnum: %d]", GET_PAD(mob, 0), GET_ROOM_VNUM(mob->in_room), value, @@ -2687,10 +2687,10 @@ void find_replacement(void *go, GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig)); mudlog(buf, NRM, kLvlGreatGod, MONEY_LOG, true); - // - + // клан-налог const long diff = currencies::GetHand(*mob, currencies::kGold) - before; split_or_clan_tax(mob, diff); - // show money + // стата для show money if (!mob->IsNpc() && mob->in_room > 0) { MoneyDropStat::add(zone_table[world[mob->in_room]->zone_rn].vnum, diff); } @@ -2701,10 +2701,10 @@ void find_replacement(void *go, if (*subfield) { const long before = currencies::GetBank(*mob, currencies::kGold); currencies::SetBank(*mob, currencies::kGold, std::max(long(0), gm_char_field(mob, field, subfield, currencies::GetBank(*mob, currencies::kGold)))); - // - + // клан-налог const long diff = currencies::GetBank(*mob, currencies::kGold) - before; split_or_clan_tax(mob, diff); - // show money + // стата для show money if (!mob->IsNpc() && mob->in_room > 0) { MoneyDropStat::add(zone_table[world[mob->in_room]->zone_rn].vnum, diff); } @@ -2714,7 +2714,7 @@ void find_replacement(void *go, if (!str_cmp(field, "questbodrich")) { if (*subfield) { if (IsCharmice(mob)) { -// SendMsgToChar(mob->get_master(), " , \r\n"); +// SendMsgToChar(mob->get_master(), "Квест чармисом, берем мастера\r\n"); mob->get_master()->dquest(atoi(subfield)); } else { @@ -2726,7 +2726,7 @@ void find_replacement(void *go, if (*subfield == '-') { experience::EndowExpToChar(mob, -std::max(1, atoi(subfield + 1))); snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) %s %d %d", + "SCRIPT_LOG (exp) у %s уменьшен опыт на %d в триггере %d", GET_NAME(mob), std::max(1, atoi(subfield + 1)), GET_TRIG_VNUM(trig)); @@ -2734,14 +2734,14 @@ void find_replacement(void *go, } else if (*subfield == '+') { experience::EndowExpToChar(mob, +std::max(1, atoi(subfield + 1))); snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) %s %d %d", + "SCRIPT_LOG (exp) у %s увеличен опыт на %d в триггере %d", GET_NAME(mob), std::max(1, atoi(subfield + 1)), GET_TRIG_VNUM(trig)); mudlog(buf, BRF, kLvlGreatGod, ERRLOG, 1); } else { snprintf(buf, sizeof(buf), - "SCRIPT_LOG (exp) ! %s %d %d", + "SCRIPT_LOG (exp) ОШИБКА! у %s напрямую указан опыт %d в триггере %d", GET_NAME(mob), atoi(subfield + 1), GET_TRIG_VNUM(trig)); @@ -2801,10 +2801,10 @@ void find_replacement(void *go, else if (!str_cmp(field, "cancarryweight")) snprintf(str, str_size, "%d", CAN_CARRY_W(mob)); else if (!str_cmp(field, "CanBeSeen")) { - // %char.canbeseen(<>)% - - // (sight::CanSee //). - // : MOB_TRIGGER - // - , . + // С аргументом %char.canbeseen(<смотрящий>)% - видит ли указанный + // персонаж этого (sight::CanSee учитывает слепоту/невидимость/темноту). + // Без аргумента сохраняем прежнее поведение: в MOB_TRIGGER проверяем + // видимость мобом-хозяином триггера, иначе считаем видимым. CharData *viewer = *subfield ? get_char(subfield) : nullptr; if (viewer) { snprintf(str, str_size, sight::CanSee(viewer, mob) ? "1" : "0"); @@ -2978,7 +2978,7 @@ void find_replacement(void *go, mob->save_char(); return; } else { - trig_log(trig, " loadroom"); + trig_log(trig, "ошибка в параметрах loadroom"); return; } } @@ -3130,7 +3130,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), " '%s' ApplyTypes", subfield); + snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке ApplyTypes", subfield); trig_log(trig, buf); return; } @@ -3154,13 +3154,13 @@ void find_replacement(void *go, else if (aff_add) { aff_flags.set(aff_id); *str = '\0'; } else { strcpy(str, aff_flags.get(aff_id) ? "1" : "0"); } } - // affect - // + //подозреваю что никто из билдеров даже не вкурсе насчет всего функционала этого affect + //к тому же аффекты в том списке не все кличи например никак там не отображаются } else if (!str_cmp(field, "affectedby")) { char *p = strchr(subfield, ','); // issue.affect-migration: an applied affect is disconnected from the cause that triggered // it, so "affectedby" tests for a specific AFFECT TYPE, not the casting spell. Builders - // pass the affect's Russian short name (e.g. " ", ".") -- dots and + // pass the affect's Russian short name (e.g. "мантия теней", "волшебный.щит") -- dots and // underscores read as spaces, mirroring the old FixName -- or its enum text-id (kPoisoned). auto resolve_affect = [](const char *name, EAffect &out) -> bool { std::string fixed(name ? name : ""); @@ -3172,7 +3172,7 @@ void find_replacement(void *go, if (!p) { EAffect aff_id; if (!resolve_affect(subfield, aff_id)) { - snprintf(buf, sizeof(buf), " %s AffectedBy", subfield); + snprintf(buf, sizeof(buf), "Не найден аффект %s в списке AffectedBy", subfield); trig_log(trig, buf); return; } @@ -3188,7 +3188,7 @@ void find_replacement(void *go, *(p++) = '\0'; EAffect aff_id; if (!resolve_affect(subfield, aff_id)) { - snprintf(buf, sizeof(buf), " %s AffectedBy", subfield); + snprintf(buf, sizeof(buf), "Не найден аффект %s в списке AffectedBy", subfield); trig_log(trig, buf); return; } @@ -3197,7 +3197,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), " '%s' AffectedBy", p); + snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке AffectedBy", p); trig_log(trig, buf); return; } @@ -3214,7 +3214,7 @@ void find_replacement(void *go, // mudlog(fmt::format("mob flag {}", subfield)); bool val = mob->char_specials.saved.mob_flags.gm_flag(subfield, action_bits, str); if (!val) { - trig_log(trig, fmt::format("mobflag: - ({})", subfield)); + trig_log(trig, fmt::format("mobflag: неправильный параметр в скобках - ({})", subfield)); return; } } @@ -3223,7 +3223,7 @@ void find_replacement(void *go, // mudlog(fmt::format("npc flag {}", subfield)); bool val = mob->mob_specials.npc_flags.gm_flag(subfield, function_bits, str); if (!val) { - trig_log(trig, fmt::format("npcflag: - ({})", subfield)); + trig_log(trig, fmt::format("npcflag: неправильный параметр в скобках - ({})", subfield)); return; } } @@ -3235,8 +3235,8 @@ void find_replacement(void *go, } } else if (!str_cmp(field, "leader")) { if (subfield && *subfield) { - // %actor.leader(UID)% -- - // ( UID ), follow (#3398). + // %actor.leader(UID)% -- установить следование за указанным + // персонажем (по UID или имени), как делает команда follow (#3398). CharData *new_leader = get_char(subfield); if (new_leader && new_leader != mob && !follow::CircleFollow(mob, new_leader)) { if (mob->has_master()) { @@ -3247,7 +3247,7 @@ void find_replacement(void *go, group::RemoveGroupFlags(f); } follow::AddFollower(new_leader, mob); - // UID ( ) + // возвращаем UID нового лидера (если следование удалось установить) if (mob->get_master() == new_leader) { snprintf(str, str_size, "%c%ld", uid_type, new_leader->get_uid()); } @@ -3264,7 +3264,7 @@ void find_replacement(void *go, if (!l) { l = mob; } - // l - + // l - лидер группы size_t s_len = strlen(str); snprintf(str + s_len, str_size - s_len, "%c%ld ", uid_type, l->get_uid()); for (auto *f : l->followers) { @@ -3323,10 +3323,10 @@ void find_replacement(void *go, || !str_cmp(field, "all")) { int inroom; - // ( mob) + // Составление списка (для mob) inroom = mob->in_room; if (inroom == kNowhere) { - trig_log(trig, "mob- kNowhere"); + trig_log(trig, "mob-построитель списка в kNowhere"); return; } @@ -3443,13 +3443,13 @@ void find_replacement(void *go, } else if (!str_cmp(field, "type")) { snprintf(str, str_size, "%d", (int) obj->get_type()); } else if (!str_cmp(field, "CanBeSeen")) { - // CAN_SEE_OBJ, - // , /: - // "" . // - // ( MORT_CAN_SEE_OBJ). ң - // : , , , - // . kNowhere - - // . + // В отличие от CAN_SEE_OBJ, не считаем предмет автоматически видимым + // только потому, что персонаж его держит/носит: иначе слепой всегда + // "видит" предмет в инвентаре. Проверяем слепоту/невидимость/темноту + // напрямую (как MORT_CAN_SEE_OBJ). Освещение берём по комнате + // смотрящего: предмет, который он может видеть, либо у него в руках, + // либо в его же комнате. Если смотрящий в kNowhere - он вне игрового + // мира и видеть не может. CharData *viewer = *subfield ? get_char(subfield) : nullptr; bool seen = true; if (viewer && !(!viewer->IsNpc() && viewer->IsFlagged(EPrf::kHolylight))) { @@ -3524,13 +3524,13 @@ void find_replacement(void *go, if (!obj->get_dgscript_field().empty()) { saved_info = utils::Split(obj->get_dgscript_field(), '#'); } else { - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "Нет сохраненных переменных"); trig_log(trig, buf); } for (auto &it : saved_info) { name = utils::ExtractFirstArgument(it, value); if (name.empty() || value.empty()) { - snprintf(buf, sizeof(buf), " ( 'value text') '%s'", it.c_str()); + snprintf(buf, sizeof(buf), "Кривая переменная (нужно 'value text') сейчас '%s'", it.c_str()); trig_log(trig, buf); continue; } @@ -3540,7 +3540,7 @@ void find_replacement(void *go, } } } else { - snprintf(buf, sizeof(buf), " LoadVar"); + snprintf(buf, sizeof(buf), "Нет аргумента в команде LoadVar"); trig_log(trig, buf); } } else if (!str_cmp(field, "savevar")) { @@ -3559,7 +3559,7 @@ void find_replacement(void *go, vd = find_var_cntx(worlds_vars, subfield, trig->context); } if (vd_tmp.name.empty()) { - snprintf(buf, sizeof(buf), " %s", subfield); + snprintf(buf, sizeof(buf), "Не найдена переменная %s", subfield); trig_log(trig, buf); return; } @@ -3570,7 +3570,7 @@ void find_replacement(void *go, for (auto &it : saved_info) { name = utils::ExtractFirstArgument(it, value); if (name.empty() || value.empty()) { - snprintf(buf, sizeof(buf), " ( 'value text') '%s'", it.c_str()); + snprintf(buf, sizeof(buf), "Кривая переменная (нужно 'value text') сейчас '%s'", it.c_str()); trig_log(trig, buf); continue; } @@ -3586,12 +3586,12 @@ void find_replacement(void *go, out << it << "#"; } if (out.str().size() > kMaxInputLength) { - snprintf(buf, sizeof(buf), " , %zu ", out.str().size() - kMaxInputLength); + snprintf(buf, sizeof(buf), "Список переменных переполнен, сократите на %zu символов", out.str().size() - kMaxInputLength); trig_log(trig, buf); } else obj->set_dgscript_field(out.str()); } else { - snprintf(buf, sizeof(buf), " SaveVar"); + snprintf(buf, sizeof(buf), "Нет аргумента в команде SaveVar"); trig_log(trig, buf); } } else if (!str_cmp(field, "maker")) { @@ -3611,7 +3611,7 @@ void find_replacement(void *go, break; } if (num == EApply::kNumberApplies) { - snprintf(buf, sizeof(buf), " '%s' apply_types", subfield); + snprintf(buf, sizeof(buf), "Не найден апплай '%s' в списке apply_types", subfield); trig_log(trig, buf); return; } @@ -3646,7 +3646,7 @@ void find_replacement(void *go, break; } if (skill_id == ESkill::kLast) { - snprintf(buf, sizeof(buf), " '%s'", subfield); + snprintf(buf, sizeof(buf), "Не найдено умение '%s'", subfield); trig_log(trig, buf); return; } @@ -3702,28 +3702,28 @@ void find_replacement(void *go, CharData *char_to = nullptr; RoomData *room_to = nullptr; if (!((*subfield == UID_CHAR) || (*subfield == UID_CHAR_ALL) || (*subfield == UID_OBJ) || (*subfield == UID_ROOM))) { - trig_log(trig, "object.put: , UID"); + trig_log(trig, "object.put: недопустимый аргумент, необходимо указать UID"); return; } if (*subfield == UID_OBJ) { obj_to = world_objects.find_by_id(atoi(subfield + 1)).get(); if (!(obj_to && obj_to->get_type() == EObjType::kContainer)) { - trig_log(trig, "object.put: - "); + trig_log(trig, "object.put: объект-приемник не найден или не является контейнером"); return; } } if ((*subfield == UID_CHAR) || (*subfield == UID_CHAR_ALL)) { char_to = find_char(atoi(subfield + 1)); if (!char_to) { - trig_log(trig, "object.put: - "); + trig_log(trig, "object.put: субъект-приемник не найден"); return; } } if (*subfield == UID_ROOM) { room_to = find_room(atoi(subfield + 1)); if (!(room_to && (room_to->vnum != kNowhere))) { - trig_log(trig, "object.put: "); + trig_log(trig, "object.put: недопустимая комната для размещения объекта"); return; } } @@ -3738,7 +3738,7 @@ void find_replacement(void *go, } else if (obj->get_in_room() > kNowhere) { RemoveObjFromRoom(obj); } else { - trig_log(trig, "object.put: "); + trig_log(trig, "object.put: не удалось извлечь объект"); return; } //finally, put it to destination @@ -3746,8 +3746,8 @@ void find_replacement(void *go, if (CanTakeObj(char_to, obj)) { PlaceObjToInventory(obj, char_to); } else { - act(" $o3 .", false, char_to, obj, nullptr, kToChar); - act("$n $g $o3 $g .", false, char_to, obj, nullptr, kToRoom); + act("Вы не смогли удержать и выбросили $o3 на землю.", false, char_to, obj, nullptr, kToChar); + act("$n не удержал$g $o3 и уронил$g на землю.", false, char_to, obj, nullptr, kToRoom); PlaceObjToRoom(obj, char_to->in_room); } } else if (obj_to) @@ -3756,7 +3756,7 @@ void find_replacement(void *go, PlaceObjToRoom(obj, GetRoomRnum(room_to->vnum)); else { snprintf(buf2, sizeof(buf2), - "object.put: ATTENTION! >%s< . kNowhere", + "object.put: ATTENTION! за время подготовки объекта >%s< к передаче перестал существовать адресат. Объект сейчас в kNowhere", obj->get_short_description().c_str()); trig_log(trig, buf2); return; @@ -3766,10 +3766,10 @@ void find_replacement(void *go, !str_cmp(field, "pc") || !str_cmp(field, "npc") || !str_cmp(field, "all")) { int inroom; - // ( obj) + // Составление списка (для obj) inroom = obj_room(obj); if (inroom == kNowhere) { - trig_log(trig, "obj- kNowhere"); + trig_log(trig, "obj-построитель списка в kNowhere"); return; } @@ -3803,8 +3803,8 @@ void find_replacement(void *go, if (*subfield) { skip_spaces(&subfield); int num = atoi(subfield); - // . 0 -- . - // . + // Убрал пока проверку. По идее 0 -- отсутствие владельца. + // Понадобилась возможность обнулить владельца из трига. obj->set_owner(num); } else { snprintf(str, str_size, "%d", obj->get_owner()); @@ -3845,13 +3845,13 @@ void find_replacement(void *go, strncpy(str + str_length, tmp, str_size - str_length - 1); str_length += n; } else { - snprintf(buf2, sizeof(buf2), " VNUM %d , ", GET_OBJ_VNUM(obj)); + snprintf(buf2, sizeof(buf2), "Предмет VNUM %d данные переполнены, далее содержимое не учитывается", GET_OBJ_VNUM(obj)); trig_log(trig, buf2); break; // too many carying objects } } } else { - snprintf(buf2, sizeof(buf2), " VNUM %d , 'objs' .", GET_OBJ_VNUM(obj)); + snprintf(buf2, sizeof(buf2), "Предмет VNUM %d не контейнер, поля 'objs' нет.", GET_OBJ_VNUM(obj)); trig_log(trig, buf2); } } else //get global var. obj.varname @@ -3970,10 +3970,10 @@ void find_replacement(void *go, || !str_cmp(field, "all")) { int inroom; - // ( room) + // Составление списка (для room) inroom = GetRoomRnum(room->vnum); if (inroom == kNowhere) { - trig_log(trig, "room- kNowhere"); + trig_log(trig, "room-построитель списка в kNowhere"); return; } @@ -4004,12 +4004,12 @@ void find_replacement(void *go, return; } else if (!str_cmp(field, "objects")) { - //mixaz + //mixaz Выдаем список объектов в комнате int inroom; - // ( room) + // Составление списка (для room) inroom = GetRoomRnum(room->vnum); if (inroom == kNowhere) { - trig_log(trig, "room- kNowhere"); + trig_log(trig, "room-построитель списка в kNowhere"); return; } @@ -4082,10 +4082,10 @@ void var_subst(void *go, Script *sc, Trigger *trig, int type, const char *line, field = p; subfield_p = subfield; //new - // : - // , . - // - %world.curobjs(66809)% -- - // 6680. + // Буфер обязательно гасим: ниже его длина участвует в расчете места под + // вложенную подстановку, а до первой записи здесь лежит мусор со стека. + // Из-за этого у %world.curobjs(66809)% отрезало последний символ -- движок + // ругался на несуществующий внум 6680. *subfield = '\0'; if (*p == '.') { *(p++) = '\0'; @@ -4105,10 +4105,10 @@ void var_subst(void *go, Script *sc, Trigger *trig, int type, const char *line, paren_count--; if (!paren_count) { *local_p = '\0'; - // , strlen: - // , . strlen - // size_t - // -- snprintf . + // Остаток буфера считаем по указателю, а не по strlen: подполя пишутся + // друг за другом, и длина содержимого тут ни при чем. Со strlen на + // неинициализированной памяти разность могла уйти в минус и как size_t + // стать огромной -- snprintf писал бы за пределы буфера. var_subst(go, sc, trig, type, local, subfield_p, sizeof(subfield) - static_cast(subfield_p - subfield)); local_p = nullptr; @@ -4225,9 +4225,9 @@ void eval_op(const char *op, else snprintf(result, result_size, "%d", !*rhs); } else if (!strcmp("#", op)) { - // . 0 -- , "/" : DG - // , - - // . + // Остаток от деления. Деление на ноль дает 0 -- так же, как у "/" выше: у DG нет + // способа сообщить билдеру об ошибке в середине выражения, а падать из-за опечатки + // в триггере сервер не должен. snprintf(result, result_size, "%d", (n = atoi(rhs)) ? (atoi(lhs) % n) : 0); } } @@ -4315,14 +4315,14 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go "/", "*", "!", - // . , : - // "%a% + %b% # 2" "(a + b) # 2". - // (. 607bdaa84 not_sale), . + // Остаток от деления. Стоит последним, то есть связывает слабее всех остальных: + // "%a% + %b% # 2" читается как "(a + b) # 2". Так оператор и был задуман изначально + // (см. 607bdaa84 в ветке not_sale), и так он описан в руководстве билдеров. "#", "\n" }; if (strlen(expr) > kMaxTrglineLength - 1) { - trig_log(trig, fmt::format("! : {}", expr).c_str()); + trig_log(trig, fmt::format("Ошибка! Слишком длинное выражение: {}", expr).c_str()); return 0; } snprintf(line, sizeof(line), "%s", expr); @@ -4367,19 +4367,19 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go } /*++ -cond - . "foreach i .." cond = "i .." -go - MOB/OBJ/ROOM (. type) +cond - строка параметров цикла. Для комнады "foreach i .." cond = "i .." +go - уразатель на MOB/OBJ/ROOM (см. type) sc - SCRIPT(go) -trig - -type - (MOB_TRIGGER,OBJ_TRIGGER,WLD_TRIGGER) +trig - исполняемый триггер +type - тип (MOB_TRIGGER,OBJ_TRIGGER,WLD_TRIGGER) - -foreach i <> - : +Запись +foreach i <список> +работает так: -1. - -2. i - . - - i = . +1. Если список пустой - выйти +2. Переменная i равна к-ому элементу списка. Если это последний элемент - выйти +Иначе i = след. элемент и выполнить тело --*/ // returns 1 if next iteration, else 0 int process_foreach_begin(const char *cond, void *go, Script *sc, Trigger *trig, int type) { @@ -4448,7 +4448,7 @@ int process_foreach_done(const char *cond, void *, Script *, Trigger *trig, int) if (!var_list_value || !var_list_pos_value) { trig_log(trig, "foreach utility vars not found"); - free(var_list_value); // issue #3574: str_dup -- + free(var_list_value); // issue #3574: str_dup выше -- освобождаем на всех выходах free(var_list_pos_value); return 0; } @@ -4463,7 +4463,7 @@ int process_foreach_done(const char *cond, void *, Script *, Trigger *trig, int) snprintf(value, kMaxTrglineLength, "%s%s", name, FOREACH_LIST_POS_GUID); remove_var_cntx(trig->var_list, value, 0); - free(var_list_value); // issue #3574: str_dup ޣ foreach + free(var_list_value); // issue #3574: str_dup выше течёт на завершении foreach free(var_list_pos_value); return 0; } @@ -4677,8 +4677,8 @@ cmdlist_element::shared_ptr find_case(Trigger *trig, } // processes any 'wait' commands in a trigger -// issue #3523: wait -// trig_wait_event. from_current=true -> . +// issue #3523: единая точка постановки wait на триггер через штатный +// trig_wait_event. from_current=true -> возобновление с текущей строки. void hang_trig_wait(void *go, Trigger *trig, int type, long time, bool from_current) { if (time <= 0) { return; @@ -4689,9 +4689,9 @@ void hang_trig_wait(void *go, Trigger *trig, int type, long time, bool from_curr wait_event_obj->go = go; wait_event_obj->type = type; wait_event_obj->from_current = from_current; - // issue #3655: from_current- (/) (curr_line). - // wait_line , show trig - // wait', . + // issue #3655: from_current-пауза (стан/лаг) вешается на ТЕКУЩУЮ строку (curr_line). + // Синхронизируем wait_line с ней, иначе show trig показывает протухшую + // строку прошлого wait'а, а не место реальной паузы. if (from_current && trig->curr_line) { trig->wait_line = trig->curr_line; } @@ -4708,10 +4708,10 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el if ((trig->get_attach_type() == MOB_TRIGGER && IS_SET(GET_TRIG_TYPE(trig), MTRIG_DEATH)) ||(trig->get_attach_type() == OBJ_TRIGGER && IS_SET(GET_TRIG_TYPE(trig), OTRIG_PURGE))) { - snprintf(buf, sizeof(buf), "&Y&G wait '%s' (VNUM=%d).", + snprintf(buf, sizeof(buf), "&YВНИМАНИЕ&G Используется wait в триггере '%s' (VNUM=%d).", GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig)); mudlog(buf, BRF, kLvlBuilder, ERRLOG, true); - snprintf(buf, sizeof(buf), "&G wait !"); + snprintf(buf, sizeof(buf), "&GКод триггера после wait выполнен НЕ БУДЕТ!"); mudlog(buf, BRF, kLvlBuilder, ERRLOG, true); } @@ -4748,7 +4748,7 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el } } if (time == 0) { - trig_log(trig, " Wait 0"); + trig_log(trig, "попытка запустить Wait 0"); return; } hang_trig_wait(go, trig, type, time, from_current); @@ -4761,12 +4761,12 @@ void process_set(Script * /*sc*/, Trigger *trig, char *cmd) { value = two_arguments(cmd, arg, name); if (!*name) { - snprintf(buf2, sizeof(buf2), "set w/o an argument, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "set w/o an argument, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (strlen(name) > kMaxTrglineLength) { - snprintf(buf2, sizeof(buf2), "eval result (%zu), : '%s'", strlen(name), cmd); + snprintf(buf2, sizeof(buf2), "eval result превышает максимальную длину триггерной строки (%zu), команда: '%s'", strlen(name), cmd); trig_log(trig, buf2); } add_var_cntx(trig->var_list, name, value, 0); @@ -4779,14 +4779,14 @@ void process_eval(void *go, Script *sc, Trigger *trig, int type, char *cmd) { expr = two_arguments(cmd, arg, name); if (!*name) { - snprintf(buf2, sizeof(buf2), "eval w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "eval w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } size_t len_expr = strlen(expr); if (len_expr > kMaxTrglineLength) { - snprintf(buf2, sizeof(buf2), "eval: expr (%zu), : '%s'", len_expr, cmd); + snprintf(buf2, sizeof(buf2), "eval: expr превышает максимальную длину триггерной строки (%zu), команда: '%s'", len_expr, cmd); trig_log(trig, buf2); } eval_expr(expr, result, sizeof(result), go, sc, trig, type); @@ -4807,13 +4807,13 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { skip_spaces(&id_p); if (!*trignum_s || atoi(trignum_s) == 0) { - snprintf(buf2, sizeof(buf2), "attach: 1: '%s', : '%s'", trignum_s, cmd); + snprintf(buf2, sizeof(buf2), "attach: нет или ошибка в аргументе 1: аргумент '%s', команда: '%s'", trignum_s, cmd); trig_log(trig, buf2); return; } if (!id_p || !*id_p || atoi(id_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "attach: 2, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "attach: нет или ошибка в аргументе 2, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -4823,7 +4823,7 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "attach: 2- '%s' -- vnum, UID, . : '%s'", + "attach: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", id_p, cmd); trig_log(trig, buf2); return; @@ -4835,14 +4835,14 @@ void process_attach(void *go, Script *sc, Trigger *trig, int type, char *cmd) { if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "attach: 2 (), : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "attach: не найден аргумент 2 (кому), команда: '%s'", cmd); trig_log(trig, buf2); return; } } } else { if (!c->IsNpc()) { - snprintf(buf2, sizeof(buf2), "attach: "); + snprintf(buf2, sizeof(buf2), "attach: триггер нельзя прикрепить к игроку"); trig_log(trig, buf2); return; } @@ -4910,13 +4910,13 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd skip_spaces(&id_p); if (!*trignum_s) { - snprintf(buf2, sizeof(buf2), "detach w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return retval; } if (!id_p || !*id_p || atoi(id_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "detach invalid id arg(1), : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach invalid id arg(1), команда: '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -4926,7 +4926,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "detach: 2- '%s' -- vnum, UID, . : '%s'", + "detach: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", id_p, cmd); trig_log(trig, buf2); return retval; @@ -4938,7 +4938,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "detach invalid id arg(2), : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach invalid id arg(2), команда: '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -4947,7 +4947,7 @@ Trigger *process_detach(void *go, Script *sc, Trigger *trig, int type, char *cmd int tvnum = atoi(trignum_s); int trn = GetTriggerRnum(tvnum); if (trn == -1) { - snprintf(buf2, sizeof(buf2), "detach , : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "detach попытка удалить несуществующий триггер, команда: '%s'", cmd); trig_log(trig, buf2); return retval; } @@ -5008,12 +5008,12 @@ bool process_halt(Trigger *trig, char *cmd) { TrgRnum trn = GetTriggerRnum(tvn); if (tvn == 0) { - snprintf(buf2, sizeof(buf2), "halt: , : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "halt: кривой аргумент, команда: '%s'", cmd); trig_log(trig, buf2); return false; } if (trn == -1) { - snprintf(buf2, sizeof(buf2), "halt: , : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "halt: такой триггер не существует, команда: '%s'", cmd); trig_log(trig, buf2); return false; } @@ -5046,13 +5046,13 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int skip_spaces(&id_p); if (!*trignum_s) { - snprintf(buf2, sizeof(buf2), "run w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run w/o an arg, команда: '%s'", cmd); trig_log(*trig, buf2); return (false); } if (!id_p || !*id_p) { - snprintf(buf2, sizeof(buf2), "run invalid id arg(2), : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run invalid id arg(2), команда: '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5062,7 +5062,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int if (is_plain_vnum_string(id_p)) { snprintf(buf2, sizeof(buf2), - "run: 2- '%s' -- vnum, UID, . : '%s'", + "run: 2-й аргумент '%s' -- голый vnum, используйте UID, строка отменена. Команда: '%s'", id_p, cmd); trig_log(*trig, buf2); return false; @@ -5074,7 +5074,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int if (!o) { r = get_room(id_p); if (!r) { - snprintf(buf2, sizeof(buf2), "id not found - arg(2), : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "id not found - arg(2), команда: '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5082,7 +5082,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int } num = atoi(trignum_s); if (num == 0) { - snprintf(buf2, sizeof(buf2), "run invalid trignum, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "run invalid trignum, команда: '%s'", cmd); trig_log(*trig, buf2); return (false); } @@ -5115,7 +5115,7 @@ int process_run(void *go, Script **sc, Trigger **trig, int type, char *cmd, int } } if (!runtrig) { - snprintf(buf2, sizeof(buf2), " , : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "Не найден триггер, команда: '%s'", cmd); trig_log(*trig, buf2); return false; } @@ -5168,7 +5168,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { ObjData::shared_ptr object; object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5182,7 +5182,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { vnumum = number(15021, 15084); object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5196,7 +5196,7 @@ void add_stuf_zone(Trigger *trig, char *cmd) { vnumum = number(15021, 15084); object = world_objects.create_from_prototype_by_vnum(obj_vnum); if (!object) { - snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, : '%s'", obj_vnum, cmd); + snprintf(buf2, sizeof(buf2), "Add stuf: wrong ObjVnum %d, команда: '%s'", obj_vnum, cmd); trig_log(trig, buf2); return; } @@ -5217,13 +5217,13 @@ void makeuid_var(void *go, Script *sc, Trigger *trig, int type, char *cmd) { skip_spaces(&uid_p); if (!*varname) { - snprintf(buf2, sizeof(buf2), "makeuid w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "makeuid w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!uid_p || !*uid_p || atoi(uid_p + 1) == 0) { - snprintf(buf2, sizeof(buf2), "makeuid invalid id arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "makeuid invalid id arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5236,8 +5236,8 @@ void makeuid_var(void *go, Script *sc, Trigger *trig, int type, char *cmd) { /** * Added 17/04/2000 * calculate a UID variable from the VNUM -* calcuid < id> <> < 1 > -* - . +* calcuid <переменная куда пишется id> <внум> <порядковый номер от 1 до х> +* если порядковый не указан - возвращается первое вхождение. */ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; @@ -5250,28 +5250,28 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { three_arguments(t, vnum, what, count); if (!*varname) { - snprintf(buf2, sizeof(buf2), "calcuid w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*vnum || (result = atoi(vnum)) == 0) { - snprintf(buf2, sizeof(buf2), "calcuid invalid VNUM arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid invalid VNUM arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*what) { - snprintf(buf2, sizeof(buf2), "calcuid exceed TYPE arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid exceed TYPE arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } int count_num = 0; if (*count) { - count_num = atoi(count) - 1; // dg 1 + count_num = atoi(count) - 1; //В dg индексация с 1 if (count_num < 0) { - //, dg 0 () + //Произойдет, если в dg пришел индекс 0 (ошибка) snprintf(buf2, sizeof(buf2), "calcuid invalid count: '%s'", count); trig_log(trig, buf2); return; @@ -5287,7 +5287,7 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { uid_type = UID_OBJ; result = find_obj_by_id_vnum__calcuid(result, count_num, type, go); } else { - snprintf(buf2, sizeof(buf2), "calcuid unknown TYPE arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuid unknown TYPE arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5305,9 +5305,9 @@ void calcuid_var(void *go, Trigger *trig, int type, char *cmd) { } /* - * UID- - * UID PC, - * + * Поиск чаров с записью в переменную UID-а в случае онлайна человека + * Возвращает в указанную переменную UID первого PC, с именем которого + * совпадает аргумент */ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; @@ -5319,13 +5319,13 @@ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { three_arguments(cmd, arg, varname, who); if (!*varname) { - snprintf(buf2, sizeof(buf2), "charuid w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuid w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*who) { - snprintf(buf2, sizeof(buf2), "charuid name is missing, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuid name is missing, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5350,7 +5350,7 @@ void charuid_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { add_var_cntx(trig->var_list, varname, uid, 0); } -// +// поиск всех чаров с лд void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { char arg[kMaxInputLength], varname[kMaxInputLength]; char who[kMaxInputLength], uid[kMaxInputLength]; @@ -5361,13 +5361,13 @@ void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { three_arguments(cmd, arg, varname, who); if (!*varname) { - snprintf(buf2, sizeof(buf2), "charuidall w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuidall w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*who) { - snprintf(buf2, sizeof(buf2), "charuidall name is missing, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "charuidall name is missing, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5394,7 +5394,7 @@ void charuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, char *cmd) { } -// * calcuidall_var. +// * Поиск мобов для calcuidall_var. std::string ListAllMobsByVnum(MobVnum vnum) { Characters::list_t mobs; std::string str; @@ -5413,7 +5413,7 @@ std::string ListAllMobsByVnum(MobVnum vnum) { return str; } -// * calcuidall_var. +// * Поиск предметов для calcuidall_var. std::string ListAllObjsByVnum(MobVnum mvn) { std::string str; std::stringstream ss; @@ -5428,7 +5428,7 @@ std::string ListAllObjsByVnum(MobVnum mvn) { return str; } -// * - calcuid_var / ( 25 ). +// * Копи-паст с calcuid_var для возврата строки со всеми найденными уидами мобов/предметов (до 25ти вхождений). void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, char *cmd) { char arg[kMaxTrglineLength], varname[kMaxTrglineLength]; char *t, str_vnum[kMaxInputLength], what[kMaxInputLength]; @@ -5441,19 +5441,19 @@ void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, two_arguments(t, str_vnum, what); if (!*varname) { - snprintf(buf2, sizeof(buf2), "calcuidall w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*str_vnum || (vnum = atoi(str_vnum)) == 0) { - snprintf(buf2, sizeof(buf2), "calcuidall invalid VNUM arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall invalid VNUM arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } if (!*what) { - snprintf(buf2, sizeof(buf2), "calcuidall exceed TYPE arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall exceed TYPE arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5463,7 +5463,7 @@ void calcuidall_var(void * /*go*/, Script * /*sc*/, Trigger *trig, int/* type*/, } else if (!str_cmp(what, "obj")) { result = ListAllObjsByVnum(vnum); } else { - snprintf(buf2, sizeof(buf2), "calcuidall unknown TYPE arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "calcuidall unknown TYPE arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5486,7 +5486,7 @@ int process_return(Trigger *trig, char *cmd) { two_arguments(cmd, arg1, arg2); if (!*arg2) { - snprintf(buf2, sizeof(buf2), "return w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "return w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return 1; } @@ -5501,14 +5501,14 @@ void ClearContextVar(Trigger *trig,char *cmd) { var = any_one_arg(cmd, arg); if (!*var) { - snprintf(buf2, sizeof(buf2), "clearcontext w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "clearcontext w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } id = atoi(var); if (id == 0) { - snprintf(buf2, sizeof(buf2), "clearcontext 0 , : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "clearcontext попытка удалить в 0 контексте, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5527,7 +5527,7 @@ void process_unset(Script *sc, Trigger *trig, char *cmd) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "unset w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "unset w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5558,7 +5558,7 @@ void process_remote(Script *sc, Trigger *trig, char *cmd) { skip_spaces(&uid_p); if (!*buf || !*buf2) { - snprintf(buf2, sizeof(buf2), "remote: invalid arguments, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "remote: invalid arguments, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5588,13 +5588,13 @@ void process_remote(Script *sc, Trigger *trig, char *cmd) { // for all but PC's, context comes from the existing context. // for PC's, context is 0 (global) // context = vd->context; -// vd->context sc->context -// vd->context, -// sc->context, - , : -// , .. -// -// 0. 0 -// "" +// Контекст можно брать как vd->context или sc->context +// Если брать vd->context, то теряем контекст при переносе локальной переменной +// Если брать sc->context, то по-моему получится правильней, а именно: +// Для локальной переменной контекст значения не играет, т.о. +// если есть желание перенести локальную переменную заранее установите +// контекст в 0. Для глобальной переменной переменная с контекстом 0 +// "покрывает" отсутствующие контексты context = trig->context; if ((room = get_room(buf2))) { @@ -5697,7 +5697,7 @@ void process_rdelete(Script * /*sc*/, Trigger *trig, char *cmd) { skip_spaces(&uid_p); if (!*buf || !*buf2) { - snprintf(buf2, sizeof(buf2), "rdelete: invalid arguments, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "rdelete: invalid arguments, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5743,7 +5743,7 @@ void process_global(Script *sc, Trigger *trig, char *cmd, long id) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "global w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "global w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5769,7 +5769,7 @@ void process_worlds(Script * /*sc*/, Trigger *trig, char *cmd, long id) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "worlds w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "worlds w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5795,7 +5795,7 @@ void process_context(Script * /*sc*/, Trigger *trig, char *cmd) { skip_spaces(&var); if (!*var) { - snprintf(buf2, sizeof(buf2), "context w/o an arg, : '%s'", cmd); + snprintf(buf2, sizeof(buf2), "context w/o an arg, команда: '%s'", cmd); trig_log(trig, buf2); return; } @@ -5833,7 +5833,7 @@ void extract_value(Script * /*sc*/, Trigger *trig, char *cmd) { int timed_script_driver(void *go, Trigger *trig, int type, int mode); int script_driver(void *go, Trigger *trig, int type, int mode) { - int timewarning = 50; // + int timewarning = 50; // в текущий момент миллисекунды int return_code; CharacterLinkDrop = false; @@ -5916,20 +5916,20 @@ int timed_script_driver(void *go, Trigger *trig, int type, int mode) { break; } if (trig->get_attach_type() != type) { - log("SCRIPTDRIVER: , %d ", trig_index[trig->get_rnum()]->vnum); + log("SCRIPTDRIVER: типы триггеров отличаются, триггер %d ", trig_index[trig->get_rnum()]->vnum); debug::backtrace(runtime_config.logs(SYSLOG).handle()); } if (!sc) { - log("SCRIPTDRIVER: SC %d ", trig_index[trig->get_rnum()]->vnum); + log("SCRIPTDRIVER: SC отсутсвует триггер %d ", trig_index[trig->get_rnum()]->vnum); return ret_val; } if (sc->is_purged()) { return ret_val; } - // issue #3523: (extract_char - // in_room == kNowhere) -- Σ . ģ - // purged ( ). + // issue #3523: моб выведен из игрового мира (extract_char выставил + // in_room == kNowhere) -- триггер на нём не выполняем. Признак надёжнее + // устаревшего флага purged (оставлен лишь для совместимости). if (type == MOB_TRIGGER && static_cast(go)->in_room == kNowhere) { return ret_val; } @@ -6186,9 +6186,9 @@ int timed_script_driver(void *go, Trigger *trig, int type, int mode) { break; } } - // issue #3523: wait ( ), - // script_driver -- - // (TRIG_FROM_LINE). . + // issue #3523: если команда повесила на триггер wait (моб в стане), + // выходим из script_driver -- событие позже повторит ту же строку + // (TRIG_FROM_LINE). Без этого остаток триггера отработал бы вхолостую. if (trig && GET_TRIG_WAIT(trig).time_remaining > 0) { depth--; cur_trig = prev_trig; @@ -6236,12 +6236,12 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { two_arguments(argument, buf, buf2); if (!*buf) { - SendMsgToChar("Usage: tlist < > [< >]\r\n", ch); + SendMsgToChar("Usage: tlist <начальный номер или зона> [<конечный номер>]\r\n", ch); return; } - // -- : ţ (vnum / 100 == ). - // -- vnum [first, last]. + // Один аргумент -- номер зоны: листаем все её триггеры (vnum / 100 == зона). + // Два аргумента -- диапазон vnum [first, last]. first = atoi(buf); if (*buf2) { last = atoi(buf2); @@ -6251,30 +6251,30 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if ((first < 0) || (first > kMaxProtoNumber) || (last < 0) || (last > kMaxProtoNumber)) { - snprintf(buf, sizeof(buf), " 0 %d.\n\r", kMaxProtoNumber); + snprintf(buf, sizeof(buf), "Значения должны быть между 0 и %d.\n\r", kMaxProtoNumber); SendMsgToChar(buf, ch); return; } if (first > last) { - SendMsgToChar(" .\n\r", ch); + SendMsgToChar("Второе значение должно быть больше первого.\n\r", ch); return; } if (first + 200 < last) { - SendMsgToChar(" - 200.\n\r", ch); + SendMsgToChar("Максимальный показываемый промежуток - 200.\n\r", ch); return; } - // ( vnum, ). + // Билдеру доступна только его зона (сравниваем по зоне vnum, а не по сырому аргументу). if (!privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false) && (GET_OLC_ZONE(ch) != first / 100 || GET_OLC_ZONE(ch) != last / 100)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } - // trig_index vnum -- - // vnum >= first, >16k . + // trig_index отсортирован по vnum -- бинарным поиском находим первый триггер + // с vnum >= first, а не сканируем все >16k триггеров последовательно. const auto start = std::lower_bound(trig_index, trig_index + top_of_trigt, first, [](const IndexData *trig, int vnum) { return trig->vnum < vnum; }); nr = start - trig_index; @@ -6300,7 +6300,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { out += "[WLD] "; out += trgtypes; } - out += "\r\n : "; + out += "\r\nПрикреплен к: "; if (!owner_trig[trig_index[nr]->vnum].empty()) { for (auto it = owner_trig[trig_index[nr]->vnum].begin(); it != owner_trig[trig_index[nr]->vnum].end(); ++it) { @@ -6311,7 +6311,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { out_tmp += buf; } if (it->first != -1) { - out += "attach " + std::to_string(it->first) + " : "; + out += "attach из " + std::to_string(it->first) + " к: "; } out += out_tmp;// + "]"; } @@ -6323,7 +6323,7 @@ void do_tlist(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if (!found) { - SendMsgToChar(" .\n\r", ch); + SendMsgToChar("В этом промежутке триггеров нет.\n\r", ch); } else { page_string(ch->desc, pagebuf, true); } @@ -6342,7 +6342,7 @@ void do_tstat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } auto first = atoi(str); if (!(privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false)) && (GET_OLC_ZONE(ch) != first / 100)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } if (*str) { @@ -6517,8 +6517,8 @@ TriggersList::~TriggersList() { bool TriggersList::add(Trigger *trigger, const bool to_front /*= false*/) { for (const auto &i : m_list) { if (trigger->get_rnum() == i->get_rnum() && !trigger->is_runned) { - // - //.. (wait command) + //Мы не можем здесь заменить имеющийся триггер на новый + //т.к. имеющийся триггер может уже использоваться или быть в ожидание (wait command) return false; } } diff --git a/src/engine/ui/cmd/do_statistic.cpp b/src/engine/ui/cmd/do_statistic.cpp index a29ecb03e..4f7e817f2 100644 --- a/src/engine/ui/cmd/do_statistic.cpp +++ b/src/engine/ui/cmd/do_statistic.cpp @@ -53,11 +53,11 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ ++total; } /* - * + * Тут нужно использовать форматирование таблицы * \todo table format */ std::ostringstream out; - out << kColorBoldCyn << " ( / 25 . / 25 .):" + out << kColorBoldCyn << " Статистика по персонажам в игре (всего / 25 ур. и выше / ниже 25 ур.):" << kColorNrm << "\r\n" << "\r\n" << " "; int count{1}; const int columns{2}; @@ -83,29 +83,29 @@ void do_statistic(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ const int headline_width{33}; - out << fmt::format("{:<{}}", " :", headline_width); + out << fmt::format("{:<{}}", " Всего игроков:", headline_width); PrintValue(out, number_col_width, total); - out << fmt::format("{:<{}}", " | 25 :", headline_width); + out << fmt::format("{:<{}}", " Игроков выше|ниже 25 уровня:", headline_width); PrintPair(out, number_col_width, hilvl, lowlvl); - out << fmt::format("{:<{}}", " |:", headline_width); + out << fmt::format("{:<{}}", " Игроков с перевоплощениями|без:", headline_width); PrintPair(out, number_col_width, rem, norem); - out << fmt::format("{:<{}}", " | :", headline_width); + out << fmt::format("{:<{}}", " Клановых|внеклановых игроков:", headline_width); PrintPair(out, number_col_width, clan, noclan); - out << fmt::format("{:<{}}", " | :", headline_width); + out << fmt::format("{:<{}}", " Игроков с флагами ПК|без ПК:", headline_width); PrintPair(out, number_col_width, pk, nopk); - out << fmt::format("{:<{}}", " ( ) | :", headline_width); + out << fmt::format("{:<{}}", " Героев (без ПК) | Тварей убито:", headline_width); const int kills_col_width{5}; PrintPair(out, kills_col_width, char_stat::players_killed, char_stat::mobs_killed); out << "\r\n"; char_stat::PrintClassesExpStat(out); - out << " : "; + out << " Времени с перезагрузки: "; PrintUptime(out); SendMsgToChar(out.str(), ch); diff --git a/src/engine/ui/cmd/do_toggle.cpp b/src/engine/ui/cmd/do_toggle.cpp index f326c98a6..2bfaed51d 100644 --- a/src/engine/ui/cmd/do_toggle.cpp +++ b/src/engine/ui/cmd/do_toggle.cpp @@ -13,19 +13,19 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; if (GET_WIMP_LEV(ch) == 0) - strcpy(buf2, ""); + strcpy(buf2, "нет"); else sprintf(buf2, "%-3d", GET_WIMP_LEV(ch)); if (GetRealLevel(ch) >= kLvlImmortal || ch->IsFlagged(EPrf::kCoderinfo)) { snprintf(buf, kMaxStringLength, - " : %-3s " - " : %-3s " - " : %-3s \r\n" - " : %-3s " - " : %-3s " - " : %-3s \r\n" - " : %-3s \r\n", + " Нет агров : %-3s " + " Супервидение : %-3s " + " Флаги комнат : %-3s \r\n" + " Частный режим : %-3s " + " Замедление : %-3s " + " Кодер : %-3s \r\n" + " Опечатки : %-3s \r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kNohassle)), BoolToOnOffStr(ch->IsFlagged(EPrf::kHolylight)), BoolToOnOffStr(ch->IsFlagged(EPrf::kRoomFlags)), @@ -37,47 +37,47 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { } snprintf(buf, kMaxStringLength, - " : %-3s " - " : %-3s " - " : %-3s \r\n" - " : %-3s " - " : %-3s " - " - : %s \r\n" - " : %-3s " - " : %-3s \r\n" - " : %-3s " - " : %-3s " - " : %-3s \r\n" - " : %-3s " - " : %-3s " - " () : %s \r\n" - " : %-3s " - " : %-3s " - " : %-3s \r\n" - " : %-7s " - " : %-3s " - " : %-3s \r\n" - " : %-3s " - " : %-3d " - " : %-3d \r\n" - " : %s " - " () : %-5s " - " : %-3s \r\n" - " : %-8s" - " : %-3s " - " : %-3s \r\n" - " : %s " - " : %-8s" - " : %-3s \r\n" - " : %-3s " - " : %-3s " - " : %-3u \r\n", + " Автовыходы : %-3s " + " Краткий режим : %-3s " + " Сжатый режим : %-3s \r\n" + " Повтор команд : %-3s " + " Обращения : %-3s " + " Кто-то : %s \r\n" + " Болтать : %-3s " + " Орать : %-3s \r\n" + " Аукцион : %-3s " + " Базар : %-3s " + " Автозаучивание: %-3s \r\n" + " Призыв : %-3s " + " Автозавершение: %-3s " + " Группа (вид) : %s \r\n" + " Без двойников : %-3s " + " Автопомощь : %-3s " + " Автодележ : %-3s \r\n" + " Автограбеж : %-7s " + " Брать куны : %-3s " + " Арена : %-3s \r\n" + " Трусость : %-3s " + " Ширина экрана : %-3d " + " Высота экрана : %-3d \r\n" + " Сжатие : %s " + " Новости (вид) : %-5s " + " Доски : %-3s \r\n" + " Хранилище : %-8s" + " Пклист : %-3s " + " Политика : %-3s \r\n" + " Пкформат : %s " + " Соклановцы : %-8s" + " Оффтоп : %-3s \r\n" + " Потеря связи : %-3s " + " Ингредиенты : %-3s " + " Вспомнить : %-3u \r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kAutoexit)), BoolToOnOffStr(ch->IsFlagged(EPrf::kBrief)), BoolToOnOffStr(ch->IsFlagged(EPrf::kCompact)), (ch->IsFlagged(EPrf::kNoRepeat) ? "NO" : "YES"), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoTell)), - native_text::pad_right(ch->IsFlagged(EPrf::kNoInvistell) ? "" : "", 6).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kNoInvistell) ? "нельзя" : "можно", 6).c_str(), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoGossip)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoHoller)), BoolToOnOffStr(!ch->IsFlagged(EPrf::kNoAuction)), @@ -85,7 +85,7 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { BoolToOnOffStr(ch->IsFlagged(EPrf::kAutomem)), BoolToOnOffStr(ch->IsFlagged(EPrf::KSummonable)), BoolToOnOffStr(ch->IsFlagged(EPrf::kGoAhead)), - native_text::pad_right(ch->IsFlagged(EPrf::kShowGroup) ? "" : "", 7).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kShowGroup) ? "полный" : "краткий", 7).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kNoClones)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutoassist)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutosplit)), @@ -96,16 +96,16 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { (ch)->player_specials->saved.stringLength, (ch)->player_specials->saved.stringWidth, #if defined(HAVE_ZLIB) - native_text::pad_right(ch->desc->deflate == nullptr ? "" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), 6).c_str(), + native_text::pad_right(ch->desc->deflate == nullptr ? "нет" : (ch->desc->mccp_version == 2 ? "MCCPv2" : "MCCPv1"), 6).c_str(), #else "N/A", #endif - ch->IsFlagged(EPrf::kNewsMode) ? "" : "", + ch->IsFlagged(EPrf::kNewsMode) ? "доска" : "лента", BoolToOnOffStr(ch->IsFlagged(EPrf::kBoardMode)), GetChestMode(ch).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kPklMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kPolitMode)), - native_text::pad_right(ch->IsFlagged(EPrf::kPkFormatMode) ? "" : "", 6).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kPkFormatMode) ? "краткий" : "полный", 6).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kClanmembersMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kOfftopMode)), BoolToOnOffStr(ch->IsFlagged(EPrf::kAntiDcMode)), @@ -113,27 +113,27 @@ void do_toggle(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { ch->remember_get_num()); SendMsgToChar(buf, ch); if ((ch)->player_specials->saved.ntfyExchangePrice > 0) { - sprintf(buf, " : %-7ld ", (ch)->player_specials->saved.ntfyExchangePrice); + sprintf(buf, " Уведомления : %-7ld ", (ch)->player_specials->saved.ntfyExchangePrice); } else { - sprintf(buf, " : %s ", native_text::pad_right("", 7).c_str()); + sprintf(buf, " Уведомления : %s ", native_text::pad_right("Нет", 7).c_str()); } SendMsgToChar(buf, ch); snprintf(buf, kMaxStringLength, - " : %-3s " - " : %-3s \r\n" - " () : %s" - " : %-5s " - " : %-3s \r\n" - " IP : %-6s ", + " Карта : %-3s " + " Вход в зону : %-3s \r\n" + " Магщиты (вид) : %s" + " Автопризыв : %-5s " + " Маппер : %-3s \r\n" + " Контроль IP : %-6s ", BoolToOnOffStr(ch->IsFlagged(EPrf::kDrawMap)), BoolToOnOffStr(ch->IsFlagged(EPrf::kShowZoneNameOnEnter)), - native_text::pad_right(ch->IsFlagged(EPrf::kBriefShields) ? "" : "", 8).c_str(), + native_text::pad_right(ch->IsFlagged(EPrf::kBriefShields) ? "краткий" : "полный", 8).c_str(), BoolToOnOffStr(ch->IsFlagged(EPrf::kAutonosummon)), BoolToOnOffStr(ch->IsFlagged(EPrf::kMapper)), BoolToOnOffStr(ch->IsFlagged(EPrf::kIpControl))); SendMsgToChar(buf, ch); if (GET_GOD_FLAG(ch, EGf::kAllowTesterMode)) - sprintf(buf, " : %-3s\r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kTester))); + sprintf(buf, " Тестер : %-3s\r\n", BoolToOnOffStr(ch->IsFlagged(EPrf::kTester))); else sprintf(buf, "\r\n"); SendMsgToChar(buf, ch); diff --git a/src/engine/ui/cmd_god/do_print_armor.cpp b/src/engine/ui/cmd_god/do_print_armor.cpp index 5dd57ab39..9fa366274 100644 --- a/src/engine/ui/cmd_god/do_print_armor.cpp +++ b/src/engine/ui/cmd_god/do_print_armor.cpp @@ -22,14 +22,14 @@ struct FilterType { EWearFlag wear; int wear_message; int material; - std::vector affect; // weap - std::vector affect2; // apply - std::vector affect3; // + std::vector affect; // аффекты weap + std::vector affect2; // аффекты apply + std::vector affect3; // экстрафлаг }; void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || (!privilege::IsGrGod(ch) && !ch->IsFlagged(EPrf::kCoderinfo))) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } @@ -39,84 +39,84 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { while (*argument) { switch (native_text::first_char_code(argument)) { case rus::kEmUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "")) { + if (utils::IsAbbr(tmpbuf, "булат")) { filter.material = EObjMaterial::kBulat; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "бронза")) { filter.material = EObjMaterial::kBronze; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "железо")) { filter.material = EObjMaterial::kIron; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "сталь")) { filter.material = EObjMaterial::kSteel; - } else if (utils::IsAbbr(tmpbuf, ".")) { + } else if (utils::IsAbbr(tmpbuf, "кованая.сталь")) { filter.material = EObjMaterial::kForgedSteel; - } else if (utils::IsAbbr(tmpbuf, ".")) { + } else if (utils::IsAbbr(tmpbuf, "драг.металл")) { filter.material = EObjMaterial::kPreciousMetel; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "кристалл")) { filter.material = EObjMaterial::kCrystal; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "дерево")) { filter.material = EObjMaterial::kWood; - } else if (utils::IsAbbr(tmpbuf, ".")) { + } else if (utils::IsAbbr(tmpbuf, "прочное.дерево")) { filter.material = EObjMaterial::kHardWood; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "керамика")) { filter.material = EObjMaterial::kCeramic; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "стекло")) { filter.material = EObjMaterial::kGlass; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "камень")) { filter.material = EObjMaterial::kStone; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "кость")) { filter.material = EObjMaterial::kBone; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "ткань")) { filter.material = EObjMaterial::kCloth; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "кожа")) { filter.material = EObjMaterial::kSkin; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "органика")) { filter.material = EObjMaterial::kOrganic; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "береста")) { filter.material = EObjMaterial::kPaper; - } else if (utils::IsAbbr(tmpbuf, ".")) { + } else if (utils::IsAbbr(tmpbuf, "драг.камень")) { filter.material = EObjMaterial::kDiamond; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неверный материал предмета.\r\n", ch); return; } find_param = true; break; case rus::kTeUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "armor")) { + if (utils::IsAbbr(tmpbuf, "броня") || utils::IsAbbr(tmpbuf, "armor")) { filter.type = EObjType::kArmor; - } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "легкие") || utils::IsAbbr(tmpbuf, "легкая")) { filter.type = EObjType::kLightArmor; - } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "средние") || utils::IsAbbr(tmpbuf, "средняя")) { filter.type = EObjType::kMediumArmor; - } else if (utils::IsAbbr(tmpbuf, "") || utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "тяжелые") || utils::IsAbbr(tmpbuf, "тяжелая")) { filter.type = EObjType::kHeavyArmor; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неверный тип предмета.\r\n", ch); return; } find_param = true; break; case rus::kOUpper: argument = one_argument(++argument, tmpbuf); - if (utils::IsAbbr(tmpbuf, "")) { + if (utils::IsAbbr(tmpbuf, "тело")) { filter.wear = EWearFlag::kBody; filter.wear_message = 3; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "голова")) { filter.wear = EWearFlag::kHead; filter.wear_message = 4; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "ноги")) { filter.wear = EWearFlag::kLegs; filter.wear_message = 5; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "ступни")) { filter.wear = EWearFlag::kFeet; filter.wear_message = 6; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "кисти")) { filter.wear = EWearFlag::kHands; filter.wear_message = 7; - } else if (utils::IsAbbr(tmpbuf, "")) { + } else if (utils::IsAbbr(tmpbuf, "руки")) { filter.wear = EWearFlag::kArms; filter.wear_message = 8; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неверное место одевания предмета.\r\n", ch); return; } find_param = true; @@ -125,18 +125,18 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { bool tmp_find = false; argument = one_argument(++argument, tmpbuf); if (!strlen(tmpbuf)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неверный аффект предмета.\r\n", ch); return; } if (filter.affect.size() + filter.affect2.size() + filter.affect3.size() >= 3) { break; } switch (*tmpbuf) { - case '1': sprintf(tmpbuf, " 1 "); + case '1': sprintf(tmpbuf, "можно вплавить 1 камень"); break; - case '2': sprintf(tmpbuf, " 2 "); + case '2': sprintf(tmpbuf, "можно вплавить 2 камня"); break; - case '3': sprintf(tmpbuf, " 3 "); + case '3': sprintf(tmpbuf, "можно вплавить 3 камня"); break; default: break; } @@ -145,7 +145,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int num = 0; for (int flag = 0; flag < 4; ++flag) { - for (/* */; *equipment_affects[num] != '\n'; ++num) { + for (/* тут ничего не надо */; *equipment_affects[num] != '\n'; ++num) { if (strlen(equipment_affects[num]) < len) continue; if (!strncmp(equipment_affects[num], tmpbuf, len)) { @@ -170,11 +170,11 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - // + // поиск по экстрафлагу if (!tmp_find) { num = 0; for (int flag = 0; flag < 4; ++flag) { - for (/* */; *extra_bits[num] != '\n'; ++num) { + for (/* тут ничего не надо */; *extra_bits[num] != '\n'; ++num) { if (strlen(extra_bits[num]) < len) continue; if (!strncmp(extra_bits[num], tmpbuf, len)) { @@ -190,7 +190,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } if (!tmp_find) { - sprintf(buf, " : '%s'.\r\n", tmpbuf); + sprintf(buf, "Неверный аффект предмета: '%s'.\r\n", tmpbuf); SendMsgToChar(buf, ch); return; } @@ -201,12 +201,12 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } if (!find_param) { - SendMsgToChar(" :\r\n" - " armor [|||] [||||] [] []\r\n", + SendMsgToChar("Формат команды:\r\n" + " armor Т[броня|легкие|средние|тяжелые] О[тело|голова|ногиступни|кисти|руки] А[аффект] М[материал]\r\n", ch); return; } - std::string buffer = " : "; + std::string buffer = "Выборка по следующим параметрам: "; if (filter.material >= 0) { buffer += material_name[filter.material]; buffer += " "; @@ -237,25 +237,25 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { buffer += " "; } } - buffer += "\r\n | | | + \r\n"; + buffer += "\r\nСредний уровень мобов в зоне | внум предмета | материал | имя предмета + аффекты если есть\r\n"; SendMsgToChar(buffer, ch); std::multimap tmp_list; for (const auto &i : obj_proto) { - // + // материал if (filter.material >= 0 && filter.material != i->get_material()) { continue; } - // + // тип if (filter.type >= 0 && filter.type != i->get_type()) { continue; } - // + // куда можно одеть if (filter.wear != EWearFlag::kUndefined && !i->has_wear_flag(filter.wear)) { continue; } - // + // аффекты bool find = true; if (!filter.affect.empty()) { for (int it : filter.affect) { @@ -264,7 +264,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - // , + // аффект не найден, продолжать смысла нет if (!find) { continue; } @@ -280,7 +280,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - // . , + // доп.свойство не найдено, продолжать смысла нет if (!find) { continue; } @@ -293,7 +293,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - // , + // экстрафлаг не найден, продолжать смысла нет if (!find) { continue; } @@ -334,15 +334,15 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { snprintf(buf, kMaxStringLength, " %s%s%s%s%s%d%s\r\n", kColorCyn, buf2, kColorNrm, kColorCyn, - negative ? " " : " ", abs(drsdice), kColorNrm); + negative ? " ухудшает на " : " улучшает на ", abs(drsdice), kColorNrm); out << " | | | " << buf; } } if (!out.str().empty()) { - SendMsgToChar(ch, " : %zu\r\n\r\n", tmp_list.size()); + SendMsgToChar(ch, "Всего найдено предметов: %zu\r\n\r\n", tmp_list.size()); page_string(ch->desc, out.str()); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Ничего не найдено.\r\n", ch); } } diff --git a/src/engine/ui/cmd_god/do_stat.cpp b/src/engine/ui/cmd_god/do_stat.cpp index 03ec538a6..da239904f 100644 --- a/src/engine/ui/cmd_god/do_stat.cpp +++ b/src/engine/ui/cmd_god/do_stat.cpp @@ -54,18 +54,18 @@ std::string print_special(CharData *mob) { // issue.specials: identity via the registry; a mob may carry several specials (comma-joined). std::string out; for (const auto s : specials::MobSpecials(GET_MOB_VNUM(mob))) { - const char *name = ""; + const char *name = "глюк"; switch (s) { - case specials::ESpecial::kShop: name = ""; break; - case specials::ESpecial::kRent: name = ""; break; - case specials::ESpecial::kMail: name = ""; break; - case specials::ESpecial::kBank: name = ""; break; - case specials::ESpecial::kExchange: name = ""; break; - case specials::ESpecial::kHorse: name = ""; break; - case specials::ESpecial::kGuild: name = ""; break; - case specials::ESpecial::kTorc: name = ""; break; - case specials::ESpecial::kOutfit: name = ""; break; - case specials::ESpecial::kMercenary: name = ""; break; + case specials::ESpecial::kShop: name = "торговец"; break; + case specials::ESpecial::kRent: name = "рентер"; break; + case specials::ESpecial::kMail: name = "почтальон"; break; + case specials::ESpecial::kBank: name = "банкир"; break; + case specials::ESpecial::kExchange: name = "зазывала"; break; + case specials::ESpecial::kHorse: name = "конюх"; break; + case specials::ESpecial::kGuild: name = "учитель"; break; + case specials::ESpecial::kTorc: name = "меняла"; break; + case specials::ESpecial::kOutfit: name = "нубхелпер"; break; + case specials::ESpecial::kMercenary: name = "ватажник"; break; default: break; } if (!out.empty()) { @@ -73,25 +73,25 @@ std::string print_special(CharData *mob) { } out += name; } - return out.empty() ? "" : out; + return out.empty() ? "нет" : out; } void do_statip(CharData *ch, CharData *k) { log("Start logon list stat"); - // ip- + // Отображаем список ip-адресов с которых персонаж входил if (!LOGON_LIST(k).empty()) { - // update: - , ., buf2 - // , **OVERFLOW** - std::ostringstream out(" IP-:\r\n"); + // update: логон-лист может быть капитально большим, поэтому пишем это в свой дин.буфер, а не в buf2 + // заодно будет постраничный вывод ип, чтобы имма не посылало на йух с **OVERFLOW** + std::ostringstream out("Персонаж заходил с IP-адресов:\r\n"); for (const auto &logon : LOGON_LIST(k)) { snprintf(buf1, sizeof(buf1), "%16s %5ld %20s%s\r\n", logon.ip.c_str(), logon.count, rustime(localtime(&logon.lasttime)), - logon.is_first ? " ()" : ""); + logon.is_first ? " (создание)" : ""); out << buf1; } @@ -103,10 +103,10 @@ void do_statip(CharData *ch, CharData *k) { void DoStatKarma(CharData *ch, CharData *victim) { std::stringstream ss; if (!KARMA(victim)) { - SendMsgToChar(ch, " %s .\r\n", GET_NAME(victim)); + SendMsgToChar(ch, "Карма у игрока %s отсутствует.\r\n", GET_NAME(victim)); return; } - ss << " " << GET_NAME(victim) << ":\r\n" << KARMA(victim); + ss << "Карма игрока " << GET_NAME(victim) << ":\r\n" << KARMA(victim); SendMsgToChar(ss.str(), ch); } @@ -126,7 +126,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sline = fmt::sprintf("%s %s ", utils::sprintGender(to_underlying(k->get_sex())).c_str(), smallBuf); } snprintf(buf2, sizeof(buf2), - "%s '%s' [%d] UID:[%ld]", + "%s '%s' В комнате [%d] Текущий UID:[%ld]", (!k->IsNpc() ? "PC" : "MOB"), GET_NAME(k), k_room, @@ -134,10 +134,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sline += buf2; SendMsgToChar(sline, ch); } - SendMsgToChar(ch, " : [%d]\r\n", k->get_wait()); + SendMsgToChar(ch, " ЛАГ: [%d]\r\n", k->get_wait()); if (k->IsNpc()) { snprintf(buf, sizeof(buf), - ": &S%s&s, VNum: [%5d], RNum: [%5d]\r\n", + "Синонимы: &S%s&s, VNum: [%5d], RNum: [%5d]\r\n", k->GetCharAliases().c_str(), GET_MOB_VNUM(k), k->get_rnum()); @@ -145,7 +145,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } snprintf(buf, sizeof(buf), - ": %s/%s/%s/%s/%s/%s ", + "Падежи: %s/%s/%s/%s/%s/%s ", GET_PAD(k, 0), GET_PAD(k, 1), GET_PAD(k, 2), @@ -157,17 +157,17 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!k->IsNpc()) { if (!(k)->player_specials->saved.NameGod) { - snprintf(buf, sizeof(buf), " !\r\n"); + snprintf(buf, sizeof(buf), "Имя никем не одобрено!\r\n"); SendMsgToChar(buf, ch); } else if ((k)->player_specials->saved.NameGod < 1000) { - snprintf(buf, sizeof(buf), " ! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); + snprintf(buf, sizeof(buf), "Имя запрещено! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); SendMsgToChar(buf, ch); } else { - snprintf(buf, sizeof(buf), " ! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); + snprintf(buf, sizeof(buf), "Имя одобрено! - %s\r\n", GetNameById((k)->player_specials->saved.NameIDGod).c_str()); SendMsgToChar(buf, ch); } - snprintf(buf, sizeof(buf), ": %s\r\n", religion_name[(int) GET_RELIGION(k)][(int) k->get_sex()]); + snprintf(buf, sizeof(buf), "Вероисповедание: %s\r\n", religion_name[(int) GET_RELIGION(k)][(int) k->get_sex()]); SendMsgToChar(buf, ch); std::string file_name = GET_NAME(k); @@ -180,70 +180,70 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { SendMsgToChar(ch, "Registered by email from %s\r\n", text.c_str()); if (k->player_specials->saved.telegram_id != 0) - SendMsgToChar(ch, " , chat_id: %lu\r\n", k->player_specials->saved.telegram_id); + SendMsgToChar(ch, "Подключен Телеграм, chat_id: %lu\r\n", k->player_specials->saved.telegram_id); if (k->IsFlagged(EPlrFlag::kFrozen) && punishments::Get(k, punishments::EType::kFreeze).duration) { - snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", + snprintf(buf, sizeof(buf), "Заморожен : %ld час [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kFreeze).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kFreeze).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kFreeze).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kHelled) && punishments::Get(k, punishments::EType::kHell).duration) { - snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", + snprintf(buf, sizeof(buf), "Находится в темнице : %ld час [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kHell).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kHell).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kHell).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kNameDenied) && punishments::Get(k, punishments::EType::kName).duration) { - snprintf(buf, sizeof(buf), " : %ld .\r\n", + snprintf(buf, sizeof(buf), "Находится в комнате имени : %ld час.\r\n", static_cast((punishments::Get(k, punishments::EType::kName).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kMuted) && punishments::Get(k, punishments::EType::kMute).duration) { - snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", + snprintf(buf, sizeof(buf), "Будет молчать : %ld час [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kMute).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kMute).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kMute).reason.c_str()); SendMsgToChar(buf, ch); } if (k->IsFlagged(EPlrFlag::kDumbed) && punishments::Get(k, punishments::EType::kDumb).duration) { - snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", + snprintf(buf, sizeof(buf), "Будет нем : %ld мин [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kDumb).duration - time(nullptr)) / 60), punishments::Get(k, punishments::EType::kDumb).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kDumb).reason.c_str()); SendMsgToChar(buf, ch); } if (!k->IsFlagged(EPlrFlag::kRegistred) && punishments::Get(k, punishments::EType::kUnreg).duration) { - snprintf(buf, sizeof(buf), " : %ld [%s].\r\n", + snprintf(buf, sizeof(buf), "Не будет зарегистрирован : %ld час [%s].\r\n", static_cast((punishments::Get(k, punishments::EType::kUnreg).duration - time(nullptr)) / 3600), punishments::Get(k, punishments::EType::kUnreg).reason.empty() ? "-" : punishments::Get(k, punishments::EType::kUnreg).reason.c_str()); SendMsgToChar(buf, ch); } if (GET_GOD_FLAG(k, EGf::kGodsLike) && punishments::Get(k, punishments::EType::kGcurse).duration) { - snprintf(buf, sizeof(buf), " : %ld .\r\n", + snprintf(buf, sizeof(buf), "Под защитой Богов : %ld час.\r\n", static_cast((punishments::Get(k, punishments::EType::kGcurse).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } if (GET_GOD_FLAG(k, EGf::kGodscurse) && punishments::Get(k, punishments::EType::kGcurse).duration) { - snprintf(buf, sizeof(buf), " : %ld .\r\n", + snprintf(buf, sizeof(buf), "Проклят Богами : %ld час.\r\n", static_cast((punishments::Get(k, punishments::EType::kGcurse).duration - time(nullptr)) / 3600)); SendMsgToChar(buf, ch); } } const auto &title = k->GetTitleStr(); - snprintf(buf, sizeof(buf), ": %s\r\n", (title.empty() ? "<>" : title.c_str())); + snprintf(buf, sizeof(buf), "Титул: %s\r\n", (title.empty() ? "<Нет>" : title.c_str())); SendMsgToChar(buf, ch); if (k->IsNpc()) - snprintf(buf, sizeof(buf), "L-Des: %s", (!k->player_data.long_descr.empty() ? k->player_data.long_descr.c_str() : "<>\r\n")); + snprintf(buf, sizeof(buf), "L-Des: %s", (!k->player_data.long_descr.empty() ? k->player_data.long_descr.c_str() : "<Нет>\r\n")); else snprintf(buf, sizeof(buf), "L-Des: %s", - (!k->player_data.description.empty() ? k->player_data.description.c_str() : "<>\r\n")); + (!k->player_data.description.empty() ? k->player_data.description.c_str() : "<Нет>\r\n")); SendMsgToChar(buf, ch); if (!k->IsNpc()) { snprintf(smallBuf, sizeof(smallBuf), "%s", MUD::Class(k->GetClass()).GetCName()); - snprintf(buf, sizeof(buf), ": %s, : %s", + snprintf(buf, sizeof(buf), "Род: %s, Профессия: %s", MUD::RaceMessages().GetMessage(GET_RACE(k), k->get_sex()).c_str(), smallBuf); SendMsgToChar(buf, ch); @@ -252,20 +252,20 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->get_role_bits().any()) { print_bitset(k->get_role_bits(), npc_role_types, ",", str); } else { - str += ""; + str += "нет"; } - SendMsgToChar(ch, " NPC: %s%s%s", kColorCyn, str.c_str(), kColorNrm); + SendMsgToChar(ch, "Роли NPC: %s%s%s", kColorCyn, str.c_str(), kColorNrm); } char tmp_buf[256]; if (experience::GetZoneGroup(k) > 1) { - snprintf(tmp_buf, sizeof(tmp_buf), " : %ldx%d", + snprintf(tmp_buf, sizeof(tmp_buf), " : групповой %ldx%d", k->get_exp() / experience::GetZoneGroup(k), experience::GetZoneGroup(k)); } else { tmp_buf[0] = '\0'; } - snprintf(buf, sizeof(buf), ", : [%s%2d%s], : [%s%10ld%s]%s, : [%4d]\r\n", + snprintf(buf, sizeof(buf), ", Уровень: [%s%2d%s], Опыт: [%s%10ld%s]%s, Наклонности: [%4d]\r\n", kColorYel, GetRealLevel(k), kColorNrm, kColorYel, k->get_exp(), kColorNrm, tmp_buf, alignment::GetAlignment(k)); @@ -273,10 +273,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!k->IsNpc()) { if (CLAN(k)) { - SendMsgToChar(ch, " : %s\r\n", GET_CLAN_STATUS(k)); + SendMsgToChar(ch, "Статус дружины: %s\r\n", GET_CLAN_STATUS(k)); } - //added by WorM + //added by WorM когда статишь файл собсно показывалось текущее время а не время последнего входа time_t ltime = GetLastlogonByUnique(k->get_uid()); char t1[11]; char t2[11]; @@ -285,14 +285,14 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { t1[10] = t2[10] = '\0'; snprintf(buf, sizeof(buf), - ": [%s] : [%s] : [%dh %dm] : [%d]\r\n", + "Создан: [%s] Последний вход: [%s] Играл: [%dh %dm] Возраст: [%d]\r\n", t1, t2, k->player_data.time.played / 3600, ((k->player_data.time.played % 3600) / 60), CalcCharAge(k)->year); SendMsgToChar(buf, ch); { - // -- ", " OutWordsList + // сегменты без запятых -- разделитель ", " и перенос по ширине добавит OutWordsList std::vector parts; - parts.push_back(fmt::sprintf(": [%d], : [%9ld], : [%9ld] (: %ld)", + parts.push_back(fmt::sprintf("Рента: [%d], Денег: [%9ld], В банке: [%9ld] (Всего: %ld)", GET_LOADROOM(k), currencies::GetHand(*k, currencies::kGold), currencies::GetBank(*k, currencies::kGold), currencies::GetTotal(*k, currencies::kGold))); for (const auto &cur : MUD::Currencies()) { if (cur.GetId() < 0 || cur.GetTextId() == currencies::kGold) { continue; } @@ -310,16 +310,16 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } } else { int mob_online = mob_index[k->get_rnum()].total_online - (virt ? 1 : 0); - snprintf(buf, sizeof(buf), " : %d, %d. ", mob_online, mob_index[k->get_rnum()].stored); + snprintf(buf, sizeof(buf), "Сейчас в мире : %d, макс %d. ", mob_online, mob_index[k->get_rnum()].stored); SendMsgToChar(buf, ch); std::string stats; mob_stat::GetLastMobKill(k, stats); - snprintf(buf, sizeof(buf), " : &r%s&n", stats.c_str()); + snprintf(buf, sizeof(buf), "Последний раз убит: &r%s&n", stats.c_str()); SendMsgToChar(buf, ch); } snprintf(buf, sizeof(buf), - ": [%s%d/%d%s] : [%s%d/%d%s] : [%s%d/%d%s] \r\n" - ": [%s%d/%d%s] :[%s%d/%d%s] :[%s%d/%d%s] : [%s%d/%d%s]\r\n", + "Сила: [%s%d/%d%s] Инт : [%s%d/%d%s] Мудр : [%s%d/%d%s] \r\n" + "Ловк: [%s%d/%d%s] Тело:[%s%d/%d%s] Обаян:[%s%d/%d%s] Размер: [%s%d/%d%s]\r\n", kColorCyn, k->GetInbornStr(), GetRealStr(k), kColorNrm, kColorCyn, k->GetInbornInt(), GetRealInt(k), kColorNrm, kColorCyn, k->GetInbornWis(), GetRealWis(k), kColorNrm, @@ -329,12 +329,12 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { kColorCyn, GET_SIZE(k), GET_REAL_SIZE(k), kColorNrm); SendMsgToChar(buf, ch); - snprintf(buf, sizeof(buf), " :[%s%d/%d+%d%s] :[%s%d/%d+%d%s]", + snprintf(buf, sizeof(buf), "Жизни :[%s%d/%d+%d%s] Энергии :[%s%d/%d+%d%s]", kColorGrn, k->get_hit(), k->get_real_max_hit(), hit_gain(k), kColorNrm, kColorGrn, k->get_move(), k->get_real_max_move(), move_gain(k), kColorNrm); SendMsgToChar(buf, ch); if (IS_MANA_CASTER(k)) { - snprintf(buf, sizeof(buf), " :[%s%d/%d+%d%s]\r\n", + snprintf(buf, sizeof(buf), " Мана :[%s%d/%d+%d%s]\r\n", kColorGrn, k->mem_queue.stored, Mana(GetRealWis(k)), CalcManaGain(k), kColorNrm); } else { snprintf(buf, sizeof(buf), "\r\n"); @@ -342,7 +342,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - "Glory: [%d], ConstGlory: [%ld], AC: [%d/%d(%d)], : [%d], : [%2d/%2d/%d], : [%2d/%2d/%d]\r\n", + "Glory: [%d], ConstGlory: [%ld], AC: [%d/%d(%d)], Броня: [%d], Попадания: [%2d/%2d/%d], Повреждения: [%2d/%2d/%d]\r\n", Glory::get_glory(k->get_uid()), currencies::GetHand(*k, currencies::kGlory), GET_AC(k), @@ -357,7 +357,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { GetRealDamroll(k) + str_bonus(GetRealStr(k), STR_TO_DAM)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - ".: () [Will:%d/Crit.:%d/Stab.:%d/Reflex:%d], () : [%d], : [%d], .: [%d], .: [%d], .: [%d]\r\n", + "Защитн.аффекты: (базовые) [Will:%d/Crit.:%d/Stab.:%d/Reflex:%d], (полные) Поглощ: [%d], Воля: [%d], Здор.: [%d], Стойк.: [%d], Реакц.: [%d]\r\n", GetBasicSave(k, ESaving::kWill), GetBasicSave(k, ESaving::kCritical), GetBasicSave(k, ESaving::kStability), @@ -369,12 +369,12 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { CalcSaving(k, k, ESaving::kReflex, 0)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - ": [:%d/:%d/:%d/:%d/:%d/:%d/:%d/:%d]\r\n", + "Резисты: [Огонь:%d/Воздух:%d/Вода:%d/Земля:%d/Жизнь:%d/Разум:%d/Иммунитет:%d/Тьма:%d]\r\n", GET_RESIST(k, 0), GET_RESIST(k, 1), GET_RESIST(k, 2), GET_RESIST(k, 3), GET_RESIST(k, 4), GET_RESIST(k, 5), GET_RESIST(k, 6), GET_RESIST(k, 7)); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - " . : [%d], . : [%d], . : [%d], .: [%d], .: [%d]\r\n", + "Защита от маг. аффектов: [%d], Защита от маг. урона: [%d], Защита от физ. урона: [%d], Маг.урон: [%d], Физ.урон: [%d]\r\n", GET_AR(k), GET_MR(k), GET_PR(k), @@ -382,7 +382,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { k->add_abils.percent_physdam_add); SendMsgToChar(buf, ch); snprintf(buf, sizeof(buf), - ": [%d], : [%d], : [%d], : [%d], : [%d], : [%d], : [%d]\r\n", + "Запом: [%d], УспехКолд: [%d], ВоссЖиз: [%d], ВоссСил: [%d], Поглощ: [%d], Удача: [%d], Иниц: [%d]\r\n", GET_MANAREG(k), GET_CAST_SUCCESS(k), k->get_hitreg(), @@ -394,14 +394,14 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { sprinttype(static_cast(k->GetPosition()), position_types, smallBuf); { - std::string sline = fmt::sprintf(": %s, : %s, : %s", - smallBuf, (k->GetEnemy() ? GET_NAME(k->GetEnemy()) : ""), (IsEquipInMetall(k) ? "" : "")); + std::string sline = fmt::sprintf("Положение: %s, Сражается: %s, Экипирован в металл: %s", + smallBuf, (k->GetEnemy() ? GET_NAME(k->GetEnemy()) : "Нет"), (IsEquipInMetall(k) ? "Да" : "Нет")); if (k->IsNpc()) { - sline += ", : "; + sline += ", Тип атаки: "; sline += fight::GetAttackTypeDescription(k->mob_specials.attack_type); } if (k->desc) { - sline += ", : "; + sline += ", Соединение: "; sline += GetConDescription(k->desc->state); } sline += "\r\n"; @@ -409,9 +409,9 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } { sprinttype(static_cast(k->mob_specials.default_pos), position_types, buf2); - std::string sline = std::string(" : ") + buf2; + std::string sline = std::string("Позиция по умолчанию: ") + buf2; if (k->char_specials.timer > 0) { - sline += fmt::sprintf(", () [%d]\r\n", k->char_specials.timer); + sline += fmt::sprintf(", Таймер отсоединения (тиков) [%d]\r\n", k->char_specials.timer); } else if (k->extract_timer > 0) { sline += fmt::sprintf(", Extract timer [%d]\r\n", k->extract_timer); } else { @@ -422,29 +422,29 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->IsNpc()) { k->char_specials.saved.mob_flags.sprintbits(action_bits, smallBuf, sizeof(smallBuf), ",", 4); - snprintf(buf, sizeof(buf), "MOB : %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); + snprintf(buf, sizeof(buf), "MOB флаги: %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); SendMsgToChar(buf, ch); k->mob_specials.npc_flags.sprintbits(function_bits, smallBuf, sizeof(smallBuf), ",", 4); - snprintf(buf, sizeof(buf), "NPC : %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); + snprintf(buf, sizeof(buf), "NPC флаги: %s%s%s\r\n", kColorCyn, smallBuf, kColorNrm); SendMsgToChar(buf, ch); SendMsgToChar(ch, - " : %s%d%s. ", + "Количество атак: %s%d%s. ", kColorCyn, k->mob_specials.extra_attack + 1, kColorNrm); SendMsgToChar(ch, - " : %s%d%%%s. ", + "Вероятность использования умений: %s%d%%%s. ", kColorCyn, k->mob_specials.like_work, kColorNrm); SendMsgToChar(ch, - " : %s%d%s\r\n", + "Убить до начала замакса: %s%d%s\r\n", kColorCyn, k->mob_specials.MaxFactor, kColorNrm); - // issue #3429: // - // utils::OutWordsList ( / - // , ). + // issue #3429: длинные списки умений/заклинаний/способностей переносим + // по ширине экрана игрока через utils::OutWordsList (цвет метки/элементов + // зашит в префикс, ширину считаем без цветокодов). const size_t list_width = (!ch->IsNpc() && ch->player_specials->saved.stringLength > 0) ? ch->player_specials->saved.stringLength : 120; { @@ -454,10 +454,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(fmt::sprintf("%s:[%d]", skill.GetName(), GetSkill(k, skill.GetId()))); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GУмения:&c ") + "&n\r\n", ch); } if (!k->mob_specials.have_spell) { - SendMsgToChar(ch, "&G: &R&n\r\n"); + SendMsgToChar(ch, "&GЗаклинания: &Rнет&n\r\n"); } else { std::vector parts; for (auto spell_id = ESpell::kFirst; spell_id <= ESpell::kLast; ++spell_id) { @@ -468,7 +468,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(fmt::sprintf("%s:[%d]", MUD::Spell(spell_id).GetCName(), GET_SPELL_MEM(k, spell_id))); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GЗаклинания:&c ") + "&n\r\n", ch); } { std::vector parts; @@ -480,11 +480,11 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { parts.push_back(feat.GetCName()); } } - SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&G:&c ") + "&n\r\n", ch); + SendMsgToChar(utils::OutWordsList(parts, list_width, ", ", "&GСпособности:&c ") + "&n\r\n", ch); } - // + // информация о маршруте моба if (k->mob_specials.dest_count > 0 && k->in_room != kNowhere) { - // + // подготавливаем путевые точки std::stringstream str_dest_list; for (auto i = 0; i < k->mob_specials.dest_count; i++) { if (i) { @@ -494,18 +494,18 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } SendMsgToChar(ch, - " : %s%s%s\r\n", + "Заданные путевые точки: %s%s%s\r\n", kColorCyn, str_dest_list.str().c_str(), kColorNrm); - // - // ( vstat): vstat rnum 1 - // , kStayZone- . - // -- find_first_step , - // (issue #3384). + // Предполагаемый маршрут считаем и показываем только для живого моба + // (не vstat): vstat ставит временный прототип в комнату rnum 1 чужой + // зоны, а kStayZone-моб не строит путь между зонами. В лог это теперь + // не сыплется в любом случае -- find_first_step молчит, пока его не + // попросят жаловаться (issue #3384). if (!virt) { - // + // пытаемся просчитать маршрут на несколько клеток вперед std::vector predictive_path_vnum_list; static const int max_path_size = 25; RoomVnum current_room = world[k->in_room]->vnum; @@ -519,7 +519,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { break; } } - // + // конвертируем путь из внумов в строку std::stringstream str_predictive_path; for (const auto &room : predictive_path_vnum_list) { if (!str_predictive_path.str().empty()) { @@ -529,7 +529,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { } SendMsgToChar(ch, - " : %s%s%s\r\n", + "Предполагаемый маршрут: %s%s%s\r\n", kColorCyn, str_predictive_path.str().c_str(), kColorNrm); @@ -559,26 +559,26 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (k->IsNpc()) { - snprintf(buf, sizeof(buf), "Mob : &R%s&n, NPC : %dd%d\r\n", + snprintf(buf, sizeof(buf), "Mob СпецПроц: &R%s&n, NPC сила удара: %dd%d\r\n", print_special(k).c_str(), k->mob_specials.damnodice, k->mob_specials.damsizedice); SendMsgToChar(buf, ch); } { - std::string sline = fmt::sprintf(" - %d, %d; ", k->GetCarryingWeight(), k->GetCarryingQuantity()); + std::string sline = fmt::sprintf("Несет - вес %d, предметов %d; ", k->GetCarryingWeight(), k->GetCarryingQuantity()); for (i = 0, j = k->carrying; j; j = j->get_next_content(), i++); - sline += fmt::sprintf("( ) : %d, ", i); + sline += fmt::sprintf("(в инвентаре) : %d, ", i); for (i = 0, i2 = 0; i < EEquipPos::kNumEquipPos; i++) { if (GET_EQ(k, i)) { i2++; } } - sline += fmt::sprintf("(): %d\r\n", i2); + sline += fmt::sprintf("(надето): %d\r\n", i2); SendMsgToChar(sline, ch); } if (!k->IsNpc()) { - snprintf(buf, sizeof(buf), ": %d, : %d, : %d\r\n", + snprintf(buf, sizeof(buf), "Голод: %d, Жажда: %d, Опьянение: %d\r\n", GET_COND(k, condition::kFull), GET_COND(k, condition::kThirst), GET_COND(k, condition::kDrunk)); SendMsgToChar(buf, ch); } @@ -586,10 +586,10 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (god_level >= kLvlGreatGod) { std::string fl_str; - // ": X, : " -- ( - // ) , -30. + // "Ведущий: X, Ведомые: " передаём префиксом -- его длина (с именем + // ведущего) учитывается в ширине строки автоматически, без магического -30. const std::string lead_prefix = - fmt::format(": {}, : ", (k->has_master() ? GET_NAME(k->get_master()) : "<>")); + fmt::format("Ведущий: {}, Ведомые: ", (k->has_master() ? GET_NAME(k->get_master()) : "<нет>")); for (auto &it : k->followers) { if (!it->IsNpc()) { fl_str += " " + it->get_name(); @@ -603,26 +603,26 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { for (auto &helper : k->summon_helpers) { fl_str += " " + std::to_string(helper); } - SendMsgToChar(ch, "%s\r\n", utils::OutWordsList(fl_str, ch->player_specials->saved.stringLength, ", ", ": ").c_str()); + SendMsgToChar(ch, "%s\r\n", utils::OutWordsList(fl_str, ch->player_specials->saved.stringLength, ", ", "Помогают: ").c_str()); } } // Showing the bitvector snprintf(smallBuf, sizeof(smallBuf), "%s", affects::DescribeActive(k->char_specials.saved.affected_by, ", ").c_str()); std::vector out_str = utils::Split(smallBuf, ','); - sprintf(buf, ": %s%s%s\r\n", kColorYel, utils::OutWordsList(out_str, ch->player_specials->saved.stringLength - 10).c_str(), kColorNrm); + sprintf(buf, "Аффекты: %s%s%s\r\n", kColorYel, utils::OutWordsList(out_str, ch->player_specials->saved.stringLength - 10).c_str(), kColorNrm); SendMsgToChar(buf, ch); - snprintf(buf, sizeof(buf), "&G: %d\r\n&n", remort::GetRealRemort(k)); + snprintf(buf, sizeof(buf), "&GПеревоплощений: %d\r\n&n", remort::GetRealRemort(k)); SendMsgToChar(buf, ch); // Routine to show what spells a char is affected by if (!k->affected.empty()) { for (const auto &aff : k->affected) { - std::string sline = fmt::sprintf(": (%3d%s|%s) %s%s%s ", + std::string sline = fmt::sprintf("Заклинания: (%3d%s|%s) %s%s%s ", aff->duration + 1, - (aff->battleflag.get(kAfPulsedec)) || (aff->battleflag.get(kAfSameTime)) ? "" : "", - (aff->battleflag.get(kAfBattledec)) || (aff->battleflag.get(kAfSameTime)) ? "" : "", + (aff->battleflag.get(kAfPulsedec)) || (aff->battleflag.get(kAfSameTime)) ? "плс" : "мин", + (aff->battleflag.get(kAfBattledec)) || (aff->battleflag.get(kAfSameTime)) ? "рнд" : "мин", kColorCyn, // issue.affect-migration: affect name by its own identity (affect_type), spell fallback. - // -- : printf %-21s (issue #3681). + // Ширина поля -- в символах: printf меряет %-21s в байтах (issue #3681). native_text::pad_right( affects::AffectMsg(aff->affect_type, affects::EAffectMsgType::kShortDesc), 21).c_str(), kColorNrm); @@ -651,7 +651,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { do_sstat_character(ch, k); if (MEMORY(k)) { struct mob_ai::MemoryRecord *memchar; - SendMsgToChar(":\r\n", ch); + SendMsgToChar("Помнит:\r\n", ch); for (memchar = MEMORY(k); memchar; memchar = memchar->next) { snprintf(buf, sizeof(buf), "%10ld - %10ld\r\n", static_cast(memchar->id), @@ -664,7 +664,7 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { if (!SCRIPT(k)->global_vars.empty()) { char name[kMaxInputLength]; void find_uid_name(const char *uid, char *name, size_t name_size); - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Глобальные переменные:\r\n", ch); // currently, variable context for players is always 0, so it is // not displayed here. in the future, this might change for (auto tv : k->script->global_vars) { @@ -685,20 +685,20 @@ void do_stat_character(CharData *ch, CharData *k, const int virt) { std::string quested(k->quested_print()); if (!quested.empty()) { - // issue #3429: -- - // () WrapText. + // issue #3429: список выполненных квестов бывает очень длинным -- + // переносим каждую строку (зону) по ширине экрана через WrapText. const size_t width = (!ch->IsNpc() && ch->player_specials->saved.stringLength > 0) ? ch->player_specials->saved.stringLength : 120; - SendMsgToChar(ch, " :\r\n%s\r\n", utils::WrapText(quested, width).c_str()); + SendMsgToChar(ch, "Выполнил квесты:\r\n%s\r\n", utils::WrapText(quested, width).c_str()); } if (NORENTABLE(k)) { - snprintf(buf, sizeof(buf), " %ld\r\n", + snprintf(buf, sizeof(buf), "Не может уйти на постой %ld\r\n", static_cast(NORENTABLE(k) - time(nullptr))); SendMsgToChar(buf, ch); } if (AGRO(k)) { - snprintf(buf, sizeof(buf), " %ld\r\n", static_cast(AGRO(k) - time(nullptr))); + snprintf(buf, sizeof(buf), "Агрессор %ld\r\n", static_cast(AGRO(k) - time(nullptr))); SendMsgToChar(buf, ch); } pk_list_sprintf(k, buf); @@ -716,21 +716,21 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { vnum = GET_OBJ_VNUM(j); rnum = j->get_rnum(); - snprintf(buf, sizeof(buf), ": '%s%s%s',\r\n: '&c%s&n',", + snprintf(buf, sizeof(buf), "Название: '%s%s%s',\r\nСинонимы: '&c%s&n',", kColorYel, (!j->get_short_description().empty() ? j->get_short_description().c_str() : ""), kColorNrm, j->get_aliases().c_str()); SendMsgToChar(buf, ch); if (j->get_custom_label() && !j->get_custom_label()->text_label.empty()) { - snprintf(buf, sizeof(buf), " : '&c%s&n',", j->get_custom_label()->text_label.c_str()); + snprintf(buf, sizeof(buf), " нацарапано: '&c%s&n',", j->get_custom_label()->text_label.c_str()); SendMsgToChar(buf, ch); } snprintf(buf, sizeof(buf), "\r\n"); SendMsgToChar(buf, ch); sprinttype(j->get_type(), item_types, buf1); if (rnum >= 0) { - snprintf(buf2, sizeof(buf2), "%s", (obj_proto.func(j->get_rnum()) ? "" : "")); + snprintf(buf2, sizeof(buf2), "%s", (obj_proto.func(j->get_rnum()) ? "Есть" : "Нет")); } else { snprintf(buf2, sizeof(buf2), "None"); } @@ -738,41 +738,41 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(ch, "VNum: [%s%5d%s], RNum: [%5d], UniqueID: [%ld], Id: [%ld]\r\n", kColorGrn, vnum, kColorNrm, j->get_rnum(), j->get_unique_id(), j->get_id()); - SendMsgToChar(ch, " : %f, : (%f) \r\n", j->show_koef_obj(), j->show_mort_req()); - SendMsgToChar(ch, ": %s, : %s", buf1, buf2); + SendMsgToChar(ch, "Расчет критерия: %f, мортов: (%f) \r\n", j->show_koef_obj(), j->show_mort_req()); + SendMsgToChar(ch, "Тип: %s, СпецПроцедура: %s", buf1, buf2); if (j->get_owner()) { auto tmpstr = GetPlayerNameByUnique(j->get_owner()); - SendMsgToChar(ch, ", : %s", tmpstr.empty() ? "": tmpstr.c_str()); + SendMsgToChar(ch, ", Владелец : %s", tmpstr.empty() ? "УДАЛЕН": tmpstr.c_str()); } // if (GET_OBJ_ZONE(j)) - SendMsgToChar(ch, ", VNUM : %d", j->get_vnum_zone_from()); + SendMsgToChar(ch, ", Принадлежит зоне VNUM : %d", j->get_vnum_zone_from()); if (j->get_crafter_uid()) { auto to_name = GetPlayerNameByUnique(j->get_crafter_uid()); if (to_name.empty()) { - SendMsgToChar(ch, ", : "); + SendMsgToChar(ch, ", Создатель : не найден"); } else { - SendMsgToChar(ch, ", : %s", to_name.c_str()); + SendMsgToChar(ch, ", Создатель : %s", to_name.c_str()); } } if (obj_proto[j->get_rnum()]->get_parent_rnum() > -1) { - SendMsgToChar(ch, ", (VNum) : [%d]", obj_proto[j->get_rnum()]->get_parent_vnum()); + SendMsgToChar(ch, ", Родитель(VNum) : [%d]", obj_proto[j->get_rnum()]->get_parent_vnum()); } if (j->get_craft_timer() > 0) { - SendMsgToChar(ch, ", &Y : [%d]&n", j->get_craft_timer()); + SendMsgToChar(ch, ", &Yскрафчена с таймером : [%d]&n", j->get_craft_timer()); } - if (j->get_is_rename()) // + if (j->get_is_rename()) // изменены падежи { - SendMsgToChar(ch, ", &G &n"); + SendMsgToChar(ch, ", &Gпадежи отличны от прототипа&n"); } SendMsgToChar(ch, "\r\n%s", sight::diag_weapon_to_char(j, 2)); snprintf(buf, sizeof(buf), "L-Des: %s\r\n%s", - !j->get_description().empty() ? j->get_description().c_str() : "", + !j->get_description().empty() ? j->get_description().c_str() : "Нет", kColorNrm); SendMsgToChar(buf, ch); if (!j->get_ex_description().empty()) { - std::string sline = fmt::sprintf(" :%s", kColorCyn); + std::string sline = fmt::sprintf("Экстра описание:%s", kColorCyn); for (const auto &desc : j->get_ex_description()) { sline += " "; sline += desc.keyword; @@ -781,27 +781,27 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { sline += "\r\n"; SendMsgToChar(sline, ch); } - SendMsgToChar(" : ", ch); + SendMsgToChar("Может быть надет : ", ch); sprintbit(j->get_wear_flags(), wear_bits, buf, sizeof(buf)); SendMsgToChar(ch, "%s\r\n", buf); sprinttype(j->get_material(), material_name, buf2); - snprintf(buf, sizeof(buf), " : %s, . : %d, . : %d\r\n", + snprintf(buf, sizeof(buf), "Материал : %s, макс.прочность : %d, тек.прочность : %d\r\n", buf2, j->get_maximum_durability(), j->get_current_durability()); SendMsgToChar(buf, ch); - SendMsgToChar(" : ", ch); + SendMsgToChar("Неудобства : ", ch); j->get_no_flags().sprintbits(no_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - SendMsgToChar(" : ", ch); + SendMsgToChar("Запреты : ", ch); j->get_anti_flags().sprintbits(anti_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - SendMsgToChar(" : ", ch); + SendMsgToChar("Устанавливает аффекты : ", ch); j->get_affect_flags().sprintbits(equipment_affects, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); if (j->has_suppressed_affects()) { - SendMsgToChar(" : ", ch); + SendMsgToChar("Подавленные аффекты : ", ch); std::string sup; for (const auto &pr : j->suppressed_equip_affects()) { char one[128]; @@ -813,23 +813,23 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(ch, "%s\r\n", sup.c_str()); } - SendMsgToChar(" : ", ch); + SendMsgToChar("Дополнительные флаги : ", ch); j->get_extra_flags().sprintbits(extra_bits, buf, sizeof(buf), ",", 4); SendMsgToChar(ch, "%s\r\n", buf); - snprintf(buf, sizeof(buf), ": %d, : %d, (eq): %d, (inv): %d, ", + snprintf(buf, sizeof(buf), "Вес: %d, Цена: %d, Рента(eq): %d, Рента(inv): %d, ", j->get_weight(), j->get_cost(), j->get_rent_on(), j->get_rent_off()); SendMsgToChar(buf, ch); if (stable_objs::IsTimerUnlimited(j)) - snprintf(buf, sizeof(buf), ": , "); + snprintf(buf, sizeof(buf), "Таймер: нерушимо, "); else - snprintf(buf, sizeof(buf), ": %d, ", j->get_timer()); + snprintf(buf, sizeof(buf), "Таймер: %d, ", j->get_timer()); SendMsgToChar(buf, ch); - // , - // (PlaceObjToRoom). (60), - // , . + // Таймер на земле тикает только пока вещь лежит в комнате, и выставляется при попадании + // туда (PlaceObjToRoom). У вещи в инвентаре в этом поле лежит умолчание конструктора (60), + // которое выглядело настоящим таймером, хотя таким никогда не станет. if (j->get_in_room() != kNowhere) { - snprintf(buf, sizeof(buf), " : %d\r\n", j->get_destroyer()); + snprintf(buf, sizeof(buf), "Таймер на земле: %d\r\n", j->get_destroyer()); } else { snprintf(buf, sizeof(buf), "\r\n"); } @@ -846,7 +846,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (str.empty()) { for (ExchangeItem *tmp_obj = exchange_item_list; tmp_obj; tmp_obj = tmp_obj->next) { if (GET_EXCHANGE_ITEM(tmp_obj)->get_unique_id() == j->get_unique_id()) { - str = fmt::format(" , #{}\r\n", GET_EXCHANGE_ITEM_LOT(tmp_obj)); + str = fmt::format("продается на базаре, лот #{}\r\n", GET_EXCHANGE_ITEM_LOT(tmp_obj)); break; } } @@ -857,7 +857,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (!tmp_obj) { continue; } - str = fmt::format(" : {}\r\n", shop->GetDictionaryName()); + str = fmt::format("можно купить в магазине: {}\r\n", shop->GetDictionaryName()); } } if (!str.empty()) { @@ -866,33 +866,33 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { } else { auto room = get_room_where_obj(j); - std::string sline = "&C : "; + std::string sline = "&CНаходится в комнате : "; if (room == kNowhere || !is_grgod) { - sline += ""; + sline += "нигде"; } else { sline += std::to_string(room); } - sline += ", : "; + sline += ", В контейнере: "; if (j->get_in_obj() && is_grgod) { sline += fmt::sprintf("[%d] %s", GET_OBJ_VNUM(j->get_in_obj()), j->get_in_obj()->get_short_description().c_str()); } else { - sline += ""; + sline += "Нет"; } - sline += ", : "; + sline += ", В инвентаре: "; if (j->get_carried_by() && is_grgod) { sline += GET_NAME(j->get_carried_by()); } else if (j->get_in_obj() && j->get_in_obj()->get_carried_by() && is_grgod) { sline += GET_NAME(j->get_in_obj()->get_carried_by()); } else { - sline += ""; + sline += "Нет"; } - sline += ", : "; + sline += ", Надет: "; if (j->get_worn_by() && is_grgod) { sline += GET_NAME(j->get_worn_by()); } else if (j->get_in_obj() && j->get_in_obj()->get_worn_by() && is_grgod) { sline += GET_NAME(j->get_in_obj()->get_worn_by()); } else { - sline += ""; + sline += "Нет"; } sline += "\r\n&n"; SendMsgToChar(sline, ch); @@ -905,112 +905,112 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EBook::kSpell: { auto spell_id = static_cast(GET_OBJ_VAL(j, 1)); if (spell_id >= ESpell::kFirst && spell_id <= ESpell::kLast) { - snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Spell(spell_id).GetCName()); + snprintf(buf, sizeof(buf), "содержит заклинание : \"%s\"", MUD::Spell(spell_id).GetCName()); } else - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "неверный номер заклинания"); break; } case EBook::kSkill: { auto skill_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Skill(skill_id).IsValid()) { - snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Skill(skill_id).GetName()); + snprintf(buf, sizeof(buf), "содержит секрет умения : \"%s\"", MUD::Skill(skill_id).GetName()); } else - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "неверный номер умения"); break; } case EBook::kSkillUpgrade: { auto skill_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Skills().IsValid(skill_id)) { if (GET_OBJ_VAL(j, 3) > 0) { - snprintf(buf, sizeof(buf), " \"%s\" ( %d)", + snprintf(buf, sizeof(buf), "повышает умение \"%s\" (максимум %d)", MUD::Skill(skill_id).GetName(), GET_OBJ_VAL(j, 3)); } else { - snprintf(buf, sizeof(buf), " \"%s\" ( )", + snprintf(buf, sizeof(buf), "повышает умение \"%s\" (не больше максимума текущего перевоплощения)", MUD::Skill(skill_id).GetName()); } } else { - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "неверный номер повышаемого умения"); } } break; case EBook::kFeat: { const auto feat_id = static_cast(GET_OBJ_VAL(j, 1)); if (MUD::Feat(feat_id).IsValid()) { - snprintf(buf, sizeof(buf), " : \"%s\"", MUD::Feat(feat_id).GetCName()); + snprintf(buf, sizeof(buf), "содержит секрет способности : \"%s\"", MUD::Feat(feat_id).GetCName()); } else { - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "неверный номер способности"); } } break; case EBook::kReceipt: { const auto recipe = im_get_recipe(GET_OBJ_VAL(j, 1)); if (recipe >= 0) { - // issue.class-recipes: per-class; , - // / . + // issue.class-recipes: требования теперь per-class; класса тут нет, поэтому + // показываем минимальные уровень/реморт среди владеющих классов. const auto [minlevel, minremort] = classes::GetRecipeMinRequirements(imrecipes[recipe].str_id); if ((minlevel >= 0) && (minremort >= 0)) { const auto recipelevel = std::max(GET_OBJ_VAL(j, 2), minlevel); snprintf(buf, sizeof(buf), - " : \"%s\", . : %d, . : %d", + "содержит рецепт отвара : \"%s\", мин. уровень изучения: %d, мин. количество ремортов: %d", imrecipes[recipe].name, recipelevel, minremort); } else { snprintf(buf, sizeof(buf), - " : \"%s\" ( )", + "содержит рецепт отвара : \"%s\" (не доступен ни одному классу)", imrecipes[recipe].name); } } else - snprintf(buf, sizeof(buf), " "); + snprintf(buf, sizeof(buf), "Некорректная запись рецепта"); } break; - default:snprintf(buf, sizeof(buf), " "); + default:snprintf(buf, sizeof(buf), "НЕВЕРНО УКАЗАН ТИП КНИГИ"); break; } break; case EObjType::kLightSource: if (GET_OBJ_VAL(j, 2) < 0) { - snprintf(buf, sizeof(buf), " !"); + snprintf(buf, sizeof(buf), "Вечный свет!"); } else { - snprintf(buf, sizeof(buf), " : [%d]", GET_OBJ_VAL(j, 2)); + snprintf(buf, sizeof(buf), "Осталось светить: [%d]", GET_OBJ_VAL(j, 2)); } break; case EObjType::kScroll: { - // issue.magic-items: extra_values, -- + // issue.magic-items: заклинания свитка лежат в extra_values, сила -- умение мастера snprintf(buf, sizeof(buf), "%s", utils::OutWordsList(SpellItemSpellsWithPotency(j), ch->player_specials->saved.stringLength, ", ", - std::string(kColorGrn) + ":" + kColorNrm + " ").c_str()); + std::string(kColorGrn) + "Заклинания:" + kColorNrm + " ").c_str()); break; } case EObjType::kPotion: { snprintf(buf, sizeof(buf), "%s", utils::OutWordsList(SpellItemSpellsWithPotency(j), ch->player_specials->saved.stringLength, ", ", - std::string(kColorGrn) + ":" + kColorNrm + " ").c_str()); + std::string(kColorGrn) + "Заклинания:" + kColorNrm + " ").c_str()); break; } case EObjType::kWand: case EObjType::kStaff: - // issue.magic-items: extra_values, val[] + // issue.magic-items: заклинание и заряды берем из extra_values, val[] у посохов нулевые { const auto staff_spell = static_cast(j->GetSpellItemSpellNum(1)); const int potency = static_cast(MagicItemPotency(j, staff_spell) + 0.5f); - // issue #3611: , - // -- , . - snprintf(buf, sizeof(buf), "%s:%s %s%s%s ( %d%s), %d ( %d) ", + // issue #3611: у вещи из прототипа сила посчитана по зашитым умолчаниям, а не по + // умению мастера -- помечаем так же, как в перечне заклинаний свитков и зелий. + snprintf(buf, sizeof(buf), "%sЗаклинание:%s %s%s%s (сила %d%s), %d (из %d) зарядов осталось", kColorGrn, kColorNrm, kColorCyn, MUD::Spell(staff_spell).GetCName(), kColorNrm, potency, - IsPotencyFromProto(j) ? ", " : "", + IsPotencyFromProto(j) ? ", базовая" : "", j->GetPotionValueKey(ObjVal::EValueKey::kCurCharges), j->GetPotionValueKey(ObjVal::EValueKey::kMaxCharges)); } break; case EObjType::kWeapon: - snprintf(buf, sizeof(buf), ": %dd%d, : %d", + snprintf(buf, sizeof(buf), "Повреждения: %dd%d, Тип повреждения: %d", GET_OBJ_VAL(j, 1), GET_OBJ_VAL(j, 2), GET_OBJ_VAL(j, 3)); @@ -1019,7 +1019,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kArmor: case EObjType::kLightArmor: case EObjType::kMediumArmor: - case EObjType::kHeavyArmor:snprintf(buf, sizeof(buf), "AC: [%d] : [%d]", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); + case EObjType::kHeavyArmor:snprintf(buf, sizeof(buf), "AC: [%d] Броня: [%d]", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); break; case EObjType::kTrap:snprintf(buf, sizeof(buf), "Spell: %d, - Hitpoints: %d", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1)); @@ -1027,10 +1027,10 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kContainer:sprintbit(GET_OBJ_VAL(j, 1), container_bits, smallBuf, sizeof(smallBuf)); if (IS_CORPSE(j)) { - snprintf(buf, sizeof(buf), ": %d, : %s, VNUM : %d, : ", + snprintf(buf, sizeof(buf), "Объем: %d, Тип ключа: %s, VNUM моба: %d, Труп: да", GET_OBJ_VAL(j, 0), smallBuf, GET_OBJ_VAL(j, 2)); } else { - snprintf(buf, sizeof(buf), ": %d, : %s, : %d, : %d", + snprintf(buf, sizeof(buf), "Объем: %d, Тип ключа: %s, Номер ключа: %d, Сложность замка: %d", GET_OBJ_VAL(j, 0), smallBuf, GET_OBJ_VAL(j, 2), GET_OBJ_VAL(j, 3)); } break; @@ -1040,7 +1040,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { { std::string spells = drinkcon::print_spells(j); utils::Trim(spells); - snprintf(buf, sizeof(buf), ": %d, : %d, : %d, : %d, : %s\r\n%s", + snprintf(buf, sizeof(buf), "Объем: %d, Содержит: %d, Свежесть: %d, Отрава: %d, Жидкость: %s\r\n%s", GET_OBJ_VAL(j, 0), GET_OBJ_VAL(j, 1), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidTimer), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidPoison), smallBuf, spells.c_str()); @@ -1055,14 +1055,14 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { case EObjType::kFood: snprintf(buf, sizeof(buf), - "(): %d, : %d, : %d", + "Насыщает(час): %d, Свежесть: %d, Отрава: %d", GET_OBJ_VAL(j, 0), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidTimer), j->GetPotionValueKey(ObjVal::EValueKey::kLiquidPoison)); break; case EObjType::kMoney: - snprintf(buf, sizeof(buf), ": %d\r\n: %s", GET_OBJ_VAL(j, 0), + snprintf(buf, sizeof(buf), "Сумма: %d\r\nВалюта: %s", GET_OBJ_VAL(j, 0), MUD::Currency(GET_OBJ_VAL(j, 1)).GetCName(grammar::ECase::kNom)); break; @@ -1071,35 +1071,35 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { if (IS_SET(j->get_spec_param(), kItemCheckUses)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %d ", GET_OBJ_VAL(j, 2)); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить %d раз", GET_OBJ_VAL(j, 2)); } if (IS_SET(j->get_spec_param(), kItemCheckLag)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n 1 %d ", (i = GET_OBJ_VAL(j, 0) & 0xFF)); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить 1 раз в %d сек", (i = GET_OBJ_VAL(j, 0) & 0xFF)); buf_len = strlen(buf); if (GET_OBJ_VAL(j, 3) == 0 || GET_OBJ_VAL(j, 3) + i < time(nullptr)) { - snprintf(buf + buf_len, sizeof(buf) - buf_len, "( )."); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "(можно применять)."); } else { li = GET_OBJ_VAL(j, 3) + i - time(nullptr); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "( %ld ).", li); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "(осталось %ld сек).", li); } } if (IS_SET(j->get_spec_param(), kItemCheckLevel)) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %d .", (GET_OBJ_VAL(j, 0) >> 8) & 0x1F); + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nможно применить с %d уровня.", (GET_OBJ_VAL(j, 0) >> 8) & 0x1F); } if ((i = GetObjRnum(GET_OBJ_VAL(j, 1))) >= 0) { size_t buf_len = strlen(buf); - snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\n %s%s%s.", + snprintf(buf + buf_len, sizeof(buf) - buf_len, "\r\nпрототип %s%s%s.", kColorBoldCyn, obj_proto[i]->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); } break; case EObjType::kMagicContaner: case EObjType::kMagicArrow: - snprintf(buf, sizeof(buf), ": [%s]. [%d]. [%d].", + snprintf(buf, sizeof(buf), "Заклинание: [%s]. Объем [%d]. Осталось стрел[%d].", MUD::Spell(static_cast(GET_OBJ_VAL(j, 0))).GetCName(), GET_OBJ_VAL(j, 1), GET_OBJ_VAL(j, 2)); break; @@ -1115,7 +1115,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { // * more or less useless and just takes up valuable screen space. if (j->get_contains()) { - std::string sline = fmt::sprintf("\r\n:%s", kColorGrn); + std::string sline = fmt::sprintf("\r\nСодержит:%s", kColorGrn); for (found = 0, j2 = j->get_contains(); j2; j2 = j2->get_next_content()) { sline += fmt::sprintf("%s %s", found++ ? "," : "", j2->get_short_description().c_str()); if (sline.size() >= 62) { @@ -1132,7 +1132,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar(kColorNrm, ch); } found = 0; - SendMsgToChar(":", ch); + SendMsgToChar("Аффекты:", ch); for (i = 0; i < kMaxObjAffect; i++) { if (j->get_affected(i).modifier) { sprinttype(j->get_affected(i).location, apply_types, smallBuf); @@ -1141,14 +1141,14 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { } } if (!found) { - SendMsgToChar(" ", ch); + SendMsgToChar(" Нет", ch); } // issue.obj-affects: obj affects on the item (gods see everything -> nullptr viewer). { const std::string oaff = obj_affects::Diag(j, nullptr); if (!oaff.empty()) { - SendMsgToChar("\r\n :\r\n", ch); + SendMsgToChar("\r\nОбъектные аффекты:\r\n", ch); SendMsgToChar(oaff, ch); } } @@ -1157,7 +1157,7 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { CObjectPrototype::skills_t skills; j->get_skills(skills); - SendMsgToChar("\r\n :", ch); + SendMsgToChar("\r\nУмения :", ch); for (const auto &it : skills) { if (it.second == 0) { continue; @@ -1169,18 +1169,18 @@ void do_stat_object(CharData *ch, ObjData *j, const int virt = 0) { SendMsgToChar("\r\n", ch); if (j->get_ilevel() > 0) { - SendMsgToChar(ch, " (ilvl): %f\r\n", j->get_ilevel()); + SendMsgToChar(ch, "Уровень (ilvl): %f\r\n", j->get_ilevel()); } if (j->get_minimum_remorts() != 0) { - SendMsgToChar(ch, " : %d\r\n", j->get_minimum_remorts()); + SendMsgToChar(ch, "Проставлено поле перевоплощений: %d\r\n", j->get_minimum_remorts()); } else if (j->get_auto_mort_req() > 0) { - SendMsgToChar(ch, " : %d\r\n", j->get_auto_mort_req()); + SendMsgToChar(ch, "Вычислено поле минимальных перевоплощений: %d\r\n", j->get_auto_mort_req()); } - SendMsgToChar(ch, " DGScript: %s\r\n", j->get_dgscript_field().empty() ? "" : j->get_dgscript_field().c_str()); + SendMsgToChar(ch, "Сохраненные переменные из DGScript: %s\r\n", j->get_dgscript_field().empty() ? "ничего" : j->get_dgscript_field().c_str()); if (is_grgod) { snprintf(buf, sizeof(buf), - " : %d. : %d. : %d\r\n", + "Сейчас в мире : %d. На постое : %d. Макс в мире: %d\r\n", rnum >= 0 ? obj_proto.total_online(rnum) - (virt ? 1 : 0) : -1, rnum >= 0 ? obj_proto.stored(rnum) : -1, GetObjMIW(j->get_rnum())); @@ -1200,24 +1200,24 @@ void do_stat_room(CharData *ch, const int rnum = 0) { rm = world[rnum]; } - snprintf(buf, sizeof(buf), " : %s%s%s\r\n", kColorCyn, rm->name, kColorNrm); + snprintf(buf, sizeof(buf), "Комната : %s%s%s\r\n", kColorCyn, rm->name, kColorNrm); SendMsgToChar(buf, ch); sprinttype(rm->sector_type, sector_types, smallBuf); snprintf(buf, sizeof(buf), - ": [%3d], VNum: [%s%5d%s], RNum: [%5d], : %s\r\n", + "Зона: [%3d], VNum: [%s%5d%s], RNum: [%5d], Тип сектора: %s\r\n", zone_table[rm->zone_rn].vnum, kColorGrn, rm->vnum, kColorNrm, rnum, smallBuf); SendMsgToChar(buf, ch); rm->flags_sprint(smallBuf, sizeof(smallBuf), ","); - snprintf(buf, sizeof(buf), ": %s, : %s\r\n", (rm->func == nullptr) ? "None" : "Exists", smallBuf); + snprintf(buf, sizeof(buf), "СпецПроцедура: %s, Флаги: %s\r\n", (rm->func == nullptr) ? "None" : "Exists", smallBuf); SendMsgToChar(buf, ch); - SendMsgToChar(":\r\n", ch); + SendMsgToChar("Описание:\r\n", ch); SendMsgToChar(GlobalObjects::descriptions().get(rm->description_num), ch); if (!rm->ex_description.empty()) { - std::string sline = fmt::sprintf(". :%s", kColorCyn); + std::string sline = fmt::sprintf("Доп. описание:%s", kColorCyn); for (const auto &desc : rm->ex_description) { sline += " "; sline += desc.keyword; @@ -1227,7 +1227,7 @@ void do_stat_room(CharData *ch, const int rnum = 0) { SendMsgToChar(sline, ch); } { - std::string sline = fmt::sprintf(" :%s", kColorYel); + std::string sline = fmt::sprintf("Живые существа:%s", kColorYel); found = 0; size_t counter = 0; for (auto k_i = rm->people.begin(); k_i != rm->people.end(); ++k_i) { @@ -1252,7 +1252,7 @@ void do_stat_room(CharData *ch, const int rnum = 0) { SendMsgToChar(kColorNrm, ch); } if (!rm->contents.empty()) { - std::string sline = fmt::sprintf(":%s", kColorGrn); + std::string sline = fmt::sprintf("Предметы:%s", kColorGrn); found = 0; for (auto it = rm->contents.begin(); it != rm->contents.end(); ++it) { auto j = *it; @@ -1281,31 +1281,31 @@ void do_stat_room(CharData *ch, const int rnum = 0) { GET_ROOM_VNUM(rm->dir_option[i]->to_room()), kColorNrm); sprintbit(rm->dir_option[i]->exit_info.get_plane(0), exit_bits, tmpBuf, sizeof(tmpBuf)); snprintf(buf, sizeof(buf), - " %s%s%s: : [%s], : [%5d], : %s (%s), : %s\r\n", + "Выход %s%s%s: Ведет в : [%s], Ключ: [%5d], Название: %s (%s), Тип: %s\r\n", kColorCyn, native_text::pad_right(dirs[i], 5).c_str(), kColorNrm, smallBuf, rm->dir_option[i]->key, - rm->dir_option[i]->keyword ? rm->dir_option[i]->keyword : "()", - rm->dir_option[i]->vkeyword ? rm->dir_option[i]->vkeyword : "()", tmpBuf); + rm->dir_option[i]->keyword ? rm->dir_option[i]->keyword : "Нет(дверь)", + rm->dir_option[i]->vkeyword ? rm->dir_option[i]->vkeyword : "Нет(дверь)", tmpBuf); SendMsgToChar(buf, ch); if (!rm->dir_option[i]->general_description.empty()) { snprintf(buf, sizeof(buf), " %s\r\n", rm->dir_option[i]->general_description.c_str()); } else { - snprintf(buf, sizeof(buf), " .\r\n"); + snprintf(buf, sizeof(buf), " Нет описания выхода.\r\n"); } SendMsgToChar(buf, ch); } } if (!rm->affected.empty()) { - snprintf(buf1, sizeof(buf1), "&G :\r\n&n"); + snprintf(buf1, sizeof(buf1), "&GАффекты на комнате:\r\n&n"); for (const auto &aff : rm->affected) { size_t buf1_len = strlen(buf1); - snprintf(buf1 + buf1_len, sizeof(buf1) - buf1_len, " \"%s\" (: %d, : %d, : %.1f) - %s.\r\n", + snprintf(buf1 + buf1_len, sizeof(buf1) - buf1_len, " Заклинание \"%s\" (длит: %d, модиф: %d, сила: %.1f) - %s.\r\n", NAME_BY_ITEM(aff->affect_type).c_str(), aff->duration, room_spells::IsPortalAffect(aff->affect_type) ? world[aff->modifier]->vnum : aff->modifier, aff->potency, - (k = find_char(aff->caster_id)) ? GET_NAME(k) : ""); + (k = find_char(aff->caster_id)) ? GET_NAME(k) : "неизвестно"); } SendMsgToChar(buf1, ch); } @@ -1316,15 +1316,15 @@ void do_stat_room(CharData *ch, const int rnum = 0) { if (!ex || ex->affected.empty()) { continue; } - snprintf(buf1, sizeof(buf1), "&G (%s):\r\n&n", dirs_rus[d]); + snprintf(buf1, sizeof(buf1), "&GАффекты на выходе (%s):\r\n&n", dirs_rus[d]); for (const auto &aff : ex->affected) { const size_t len = strlen(buf1); snprintf(buf1 + len, sizeof(buf1) - len, - " \"%s\" (: %d, : %d, : %.1f, : %s) - %s.\r\n", + " Заклинание \"%s\" (длит: %d, модиф: %d, сила: %.1f, заряды: %s) - %s.\r\n", NAME_BY_ITEM(aff->affect_type).c_str(), aff->duration, aff->modifier, aff->potency, - (aff->charges == -1 ? "" : std::to_string(aff->charges).c_str()), - (k = find_char(aff->caster_id)) ? GET_NAME(k) : ""); + (aff->charges == -1 ? "беск" : std::to_string(aff->charges).c_str()), + (k = find_char(aff->caster_id)) ? GET_NAME(k) : "неизвестно"); } SendMsgToChar(buf1, ch); } @@ -1342,11 +1342,11 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { half_chop(argument, buf1, buf2); if (!(privilege::HasPrivilege(ch, std::string(cmd_info[cmd].command), 0, 0, false)) && (GET_OLC_ZONE(ch) <=0)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } if (!*buf1) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Состояние КОГО или ЧЕГО?\r\n", ch); return; } if (*buf1 && privilege::IsImmortal(ch)) { @@ -1356,7 +1356,7 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { if ((rnum = GetRoomRnum(vnum)) != kNowhere) do_stat_room(ch, rnum); else - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Состояние какой комнаты?\r\n", ch); } if (!*buf2) do_stat_room(ch); @@ -1364,91 +1364,91 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { } if (utils::IsAbbr(buf1, "mob") && level >= kLvlBuilder) { if (!*buf2) - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Состояние какого создания?\r\n", ch); else { victim = target_resolver::FindCharInWorld(ch, buf2); if ((victim != nullptr)) do_stat_character(ch, victim, 0); else - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такого создания в этом МАДе.\r\n", ch); } return; } if (utils::IsAbbr(buf1, "player")) { if (!*buf2) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Состояние какого игрока?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) do_stat_character(ch, victim); else - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Этого персонажа сейчас нет в игре.\r\n", ch); } return; } if (utils::IsAbbr(buf1, "ip")) { if (!*buf2) { - SendMsgToChar(" ip ?\r\n", ch); + SendMsgToChar("Состояние ip какого игрока?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) { do_statip(ch, victim); return; } else { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Этого персонажа сейчас нет в игре, смотрим пфайл.\r\n", ch); } Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { do_statip(ch, &t_vict); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); } } return; } - if (utils::IsAbbr(buf1, "karma") || utils::IsAbbr(buf1, "")) { + if (utils::IsAbbr(buf1, "karma") || utils::IsAbbr(buf1, "карма")) { if (!*buf2) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Карму какого игрока?\r\n", ch); } else { if ((victim = target_resolver::FindPlayerVis(ch, buf2)) != nullptr) { DoStatKarma(ch, victim); return; } else { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Этого персонажа сейчас нет в игре, смотрим пфайл.\r\n", ch); } Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { DoStatKarma(ch, &t_vict); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); } } return; } if (utils::IsAbbr(buf1, "file")) { if (!*buf2) { - SendMsgToChar(" ( )?\r\n", ch); + SendMsgToChar("Состояние какого игрока(из файла)?\r\n", ch); } else { Player t_vict; if (LoadPlayerCharacter(buf2, &t_vict, ELoadCharFlags::kFindId) > -1) { if (GetRealLevel(&t_vict) > level) { - SendMsgToChar(", .\r\n", ch); + SendMsgToChar("Извините, вам это еще рано.\r\n", ch); } else { Clan::SetClanData(&t_vict); do_stat_character(ch, &t_vict); } } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Такого игрока нет ВООБЩЕ.\r\n", ch); } } return; } if (utils::IsAbbr(buf1, "object") && level >= kLvlBuilder) { if (!*buf2) - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Состояние какого предмета?\r\n", ch); else { if ((object = target_resolver::FindObjInWorld(ch, buf2)) != nullptr) do_stat_object(ch, object); else - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такого предмета в игре.\r\n", ch); } return; } @@ -1499,5 +1499,5 @@ void do_stat(CharData *ch, char *argument, int cmd, int/* subcmd*/) { return; } } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Ничего похожего с этим именем нет.\r\n", ch); } diff --git a/src/engine/ui/commands.cpp b/src/engine/ui/commands.cpp index 2eafa4cc9..80210898c 100644 --- a/src/engine/ui/commands.cpp +++ b/src/engine/ui/commands.cpp @@ -210,7 +210,7 @@ void CommandEmbranchmentImplementation::make_suggestion(const CommandContext::sh std::string suggestions_string; joinList(suggestions, suggestions_string); std::stringstream ss; - ss << " [" << wrong_subcommand << "] . , : " + ss << "Подкоманда [" << wrong_subcommand << "] не существует. Возможно, вы имели ввиду одну из следующих: " << suggestions_string << "?"; reply(context, ss.str()); @@ -246,7 +246,7 @@ CommandEmbranchment::shared_ptr CommandEmbranchment::create(const std::string &h return result; } -ParentalHelp::ParentalHelp(const shared_ptr &parent) : CommandWithHelp(" ."), m_parent(parent) { +ParentalHelp::ParentalHelp(const shared_ptr &parent) : CommandWithHelp("Показать справку."), m_parent(parent) { } void ParentalHelp::execute(const CommandContext::shared_ptr &context, diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 3fe31195e..f97b2ff2c 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -72,12 +72,12 @@ using namespace ClanSystem; extern int mortal_start_room; extern void sight::list_obj_to_char(ObjData *list, CharData *ch, int mode, int show); extern int AllocateBufferForFile(const char *name, char **destination_buf); -// TODO: , , , +// TODO: думать надо с этим, или глобально следить за спамом, или игноров напихать на все случаи жизни, или так и оставить extern void SetWait(CharData *ch, int waittime, int victim_in_room); extern const char *sight::show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_state, int how); extern bool char_to_pk_clan(CharData *ch); -void fix_ingr_chest_rnum(const int room_rnum)// +void fix_ingr_chest_rnum(const int room_rnum)//Нужно чтоб позиция короба не съехала { for (const auto &i : Clan::ClanList) { if (i->get_ingr_chest_room_rnum() >= room_rnum) @@ -96,17 +96,17 @@ long long clan_level_exp[kMaxClanlevel + 1] = 1000000000000LL // BIG NUMBER. // }; -// vnum +// vnum кланового сундука const int CLAN_CHEST_VNUM = 330; int CLAN_CHEST_RNUM = -1; -// vnum +// vnum кланового сундука const int INGR_CHEST_VNUM = 333; int INGR_CHEST_RNUM = -1; -// ( ) +// налог на хранилище ингров (в день) const int INGR_CHEST_TAX = 1000; -// . +// макс. длина сообщения дружины const int MAX_MOD_LENGTH = 3 * 80; -// . +// макс. длина названия ранга в дружине const unsigned MAX_RANK_LENGHT = 10; enum { @@ -116,21 +116,21 @@ enum { void prepare_write_mod(CharData *ch, std::string ¶m) { utils::Trim(param); - if (!param.empty() && (CompareParam(param, "") || CompareParam(param, ""))) { + if (!param.empty() && (CompareParam(param, "очистить") || CompareParam(param, "удалить"))) { std::string zero_str; CLAN(ch)->write_mod(zero_str); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Сообщение удалено.\r\n", ch); return; } - SendMsgToChar(" . (/s /h )\r\n", ch); + SendMsgToChar("Можете писать сообщение. (/s записать /h помощь)\r\n", ch); ch->desc->state = EConState::kWriteMod; utils::AbstractStringWriter::shared_ptr writer(new utils::StdStringWriter()); string_write(ch->desc, writer, MAX_MOD_LENGTH, 0, nullptr); } /** -* MAX_RANK_LENGHT -* . +* Обрезание названий рангов дружины до MAX_RANK_LENGHT символов +* и перевод всего слова в нижний регистр. */ void check_rank(std::string &rank) { if (rank.size() > MAX_RANK_LENGHT) { @@ -141,7 +141,7 @@ void check_rank(std::string &rank) { } // namespace -// , +// для сортировки вывода членов клана по рангам, когда оно через поля чара дергается class SortRank { public: bool operator()(const CharData::shared_ptr ch1, const CharData::shared_ptr ch2); @@ -153,7 +153,7 @@ inline bool SortRank::operator()(const CharData::shared_ptr ch1, const CharData: Clan::ClanListType Clan::ClanList; -// to_room -, , +// поиск to_room в зонах клан-замков, выставляет за замок, если найдено RoomRnum Clan::CloseRent(RoomRnum to_room) { for (auto &clan : Clan::ClanList) if (world[to_room]->zone_rn == world[GetRoomRnum(clan->rent)]->zone_rn) @@ -165,7 +165,7 @@ int Clan::get_chest_room() { return this->chest_room; } -// +// проверяет находится ли чар в зоне чужого клана bool Clan::InEnemyZone(CharData *ch) { int zone = world[ch->in_room]->zone_rn; for (auto &clan : Clan::ClanList) @@ -191,7 +191,7 @@ Clan::Clan() : Clan::~Clan() { } -// , . ! +// релоад одного отдельного клана, абр. указывать на латинице! void Clan::ClanReload(const std::string &index) { std::ifstream file(LIB_CLANS "index"); if (!file.is_open()) { @@ -203,10 +203,10 @@ void Clan::ClanReload(const std::string &index) { while (file >> buffer) clanIndex.push_back(buffer); file.close(); - // + // ищем наш клан for (const auto &it : clanIndex) { if (it == index) { - // + // надо удалить наш клан из общего списка for (auto clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { std::string name_buffer = (*clan)->abbrev; CreateFileName(name_buffer); @@ -221,7 +221,7 @@ void Clan::ClanReload(const std::string &index) { } -// +// лоад отдельного клана из файла void Clan::ClanLoadSingle(const std::string &index) { std::string buffer; const auto tempClan = std::make_shared(); @@ -257,7 +257,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'Rent:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // + // зоны может и не быть if (!GetRoomRnum(rent)) { log("Room %d is no longer exist (%s).", rent, filename.c_str()); break; @@ -269,7 +269,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'OutRent:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // + // зоны может и не быть if (!GetRoomRnum(out_rent)) { log("Room %d is no longer exist (%s).", out_rent, filename.c_str()); break; @@ -281,7 +281,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'ChestRoom:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // + // зоны может и не быть if (!GetRoomRnum(chest_room)) { log("Room %d is no longer exist (%s).", chest_room, filename.c_str()); break; @@ -295,7 +295,7 @@ void Clan::ClanLoadSingle(const std::string &index) { break; } if (tmp_vnum != 0) { - // + // зоны может и не быть int ingr_chest_room_rnum = GetRoomRnum(tmp_vnum); if (ingr_chest_room_rnum > 0) { tempClan->ingr_chest_room_rnum_ = ingr_chest_room_rnum; @@ -317,7 +317,7 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error open 'Guard:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // + // как и охранника if (GetMobRnum(guard) < 0) { log("Guard %d is no longer exist (%s).", guard, filename.c_str()); break; @@ -334,7 +334,7 @@ void Clan::ClanLoadSingle(const std::string &index) { unsigned long priv = 0; std::string buffer2; - // , + // для верности воеводе проставим все привилегии, заодно сверим с файлом if (!(stream >> buffer >> buffer2 >> priv)) { log("Error open 'Ranks' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; @@ -358,7 +358,7 @@ void Clan::ClanLoadSingle(const std::string &index) { check_rank(buffer2); tempClan->ranks.push_back(buffer); tempClan->ranks_female.push_back(buffer2); - // + // на случай уменьшения привилегий if (priv > tempClan->privileges[0].to_ulong()) priv = tempClan->privileges[0].to_ulong(); tempClan->privileges.push_back(std::bitset(priv)); @@ -403,9 +403,9 @@ void Clan::ClanLoadSingle(const std::string &index) { } } else if (buffer == "Bank:") { file >> tempClan->bank; - log("Clans in bank, file (%s) %ld.", filename.c_str(), tempClan->bank); + log("Clans in bank, file (%s) банк %ld.", filename.c_str(), tempClan->bank); if (tempClan->bank <= 0) { - log("Clan has 0 in bank, file (%s) .", filename.c_str()); + log("Clan has 0 in bank, file (%s) возможно будет удален.", filename.c_str()); } } else if (buffer == "Pk:") { file >> tempClan->pk; @@ -428,14 +428,14 @@ void Clan::ClanLoadSingle(const std::string &index) { long unique = 0; long long money = 0; long long exp = 0; - int exp_persent = 0; // + int exp_persent = 0; // заглушка long long clan_exp = 0; if (!(file >> unique >> money >> exp >> exp_persent >> clan_exp)) { log("Error open 'Owner:' in %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // + // воеводы тоже уже может не быть const auto tempMember = std::make_shared(); tempMember->name = GetNameByUnique(unique); if (tempMember->name.empty()) { @@ -451,12 +451,12 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->owner = tempMember->name; } else if (buffer == "Members:") { - // , , ( ) + // параметры, критичные для мемберов, нужно загрузить до того как (ранги например) long unique = 0; unsigned rank = 0; long long money = 0; long long exp = 0; - int exp_persent = 0; // + int exp_persent = 0; // заглушка long long clan_exp = 0; std::getline(file, buffer, '~'); @@ -466,12 +466,12 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Error read %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); break; } - // , + // на случай, если рангов стало меньше if (!tempClan->ranks.empty() && rank > tempClan->ranks.size() - 1) { rank = static_cast(tempClan->ranks.size()) - 1; } - // + // удаленные персонажи просто игнорируются const auto tempMember = std::make_shared(); tempMember->name = GetNameByUnique(unique); if (tempMember->name.empty()) { @@ -490,8 +490,8 @@ void Clan::ClanLoadSingle(const std::string &index) { } file.close(); - // - // .. - - + // тут нужно проверить наличие критичных для клана полей + // т.к. загрузка без привязки к положению в файле - что-то может не проинициализироваться if (tempClan->abbrev.empty() || tempClan->name.empty() || tempClan->title.empty() || tempClan->rent == 0 || tempClan->guard == 0 || tempClan->out_rent == 0 @@ -499,12 +499,12 @@ void Clan::ClanLoadSingle(const std::string &index) { log("Clan read fail: %s", filename.c_str()); return; } - // + // удаление неактивных кланов tempClan->exp_history.load(tempClan->get_file_abbrev()); - // , + // иним на случай полной неактивности по итогам месяца, чтобы не было пропусков в списке tempClan->exp_history.add_exp(0); if (tempClan->exp_history.need_destroy() && !tempClan->test_clan) { - // - + // клан-банк на воеводу if (tempClan->bank > 0) { Player t_victim; Player *victim = &t_victim; @@ -518,20 +518,20 @@ void Clan::ClanLoadSingle(const std::string &index) { } Boards::Static::clan_delete_message(tempClan->abbrev, tempClan->rent / 100); char smallbuf[128]; - sprintf(smallbuf, ""); + sprintf(smallbuf, "неактивность"); DestroyClan(tempClan, smallbuf); log("Clan deleted: %s", filename.c_str()); } - // + // по дефолту жен род для титула берем из основного if (tempClan->title_female.empty()) { tempClan->title_female = tempClan->title; } - // + // сундук по дефолту на ренте if (!tempClan->chest_room) tempClan->chest_room = tempClan->rent; - // / + // чтобы не получилось потерь/прибавок экспы if (tempClan->exp_buf) { tempClan->exp += tempClan->exp_buf; if (tempClan->exp < 0) @@ -539,7 +539,7 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->exp_buf = 0; } - // / + // подгружаем пкл/дрл std::ifstream pkFile((filename + ".pkl").c_str()); if (pkFile.is_open()) { int author = 0; @@ -558,14 +558,14 @@ void Clan::ClanLoadSingle(const std::string &index) { std::string victimName = GetNameByUnique(victim, true); name_convert(authorName); name_convert(victimName); - // - + // если автора уже нет - сбросим УИД для верности if (authorName.empty()) author = 0; - // , - + // если жертвы уже нет, или жертва выбилась в БОГИ - не грузим if (!victimName.empty()) { ClanPkPtr tempRecord(new ClanPk); tempRecord->author = author; - tempRecord->authorName = authorName.empty() ? " " : authorName; + tempRecord->authorName = authorName.empty() ? "Того уж с нами нет" : authorName; tempRecord->victimName = victimName; tempRecord->time = tempTime; tempRecord->text = buffer; @@ -577,7 +577,7 @@ void Clan::ClanLoadSingle(const std::string &index) { } pkFile.close(); } - // + //подгружаем кланстафф std::ifstream stuffFile((filename + ".stuff").c_str()); if (stuffFile.is_open()) { int i; @@ -602,7 +602,7 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->clanstuff.push_back(temp); } } - // . + // лоад доп. параметров клана tempClan->load_mod(); tempClan->pk_log.load(tempClan->get_file_abbrev()); tempClan->last_exp.load(tempClan->get_file_abbrev()); @@ -611,22 +611,22 @@ void Clan::ClanLoadSingle(const std::string &index) { if ((tempClan->bank <= 0) && (tempClan->m_members.size() > 0) && !tempClan->test_clan) { Boards::Static::clan_delete_message(tempClan->abbrev, tempClan->rent / 100); char smallbuf[128]; - sprintf(smallbuf, "_"); + sprintf(smallbuf, "банк_пуст"); DestroyClan(tempClan, smallbuf); log("Clan deleted bank 0: %s", filename.c_str()); } Clan::ClanList.push_back(tempClan); } -// / +// лоад/релоад индекса и файлов кланов void Clan::ClanLoad() { const bool reload = Clan::ClanList.empty() ? false : true; init_chest_rnum(); - // + // на случай релоада Clan::ClanList.clear(); - // + // файл со списком кланов std::ifstream file(LIB_CLANS "index"); if (!file.is_open()) { log("Error open file: %s! (%s %s %d)", LIB_CLANS "index", __FILE__, __func__, __LINE__); @@ -637,7 +637,7 @@ void Clan::ClanLoad() { while (file >> buffer) clanIndex.push_back(buffer); file.close(); - // + // собственно грузим кланы for (const auto &it : clanIndex) { Clan::ClanLoadSingle(it); } @@ -653,8 +653,8 @@ void Clan::ClanLoad() { HelpSystem::reload(HelpSystem::DYNAMIC); } - // - // , - , + // на случай релоада кланов для выставления изменений игрокам онлайн + // лдшникам воткнется в другом месте, можно и тут чар-лист прогнать, варианты одинаково корявые for (auto d = descriptor_list; d; d = d->next) { if (d->character) { Clan::SetClanData(d->character.get()); @@ -662,11 +662,11 @@ void Clan::ClanLoad() { } } -// +// вывод имму информации о кланах void Clan::HconShow(CharData *ch) { std::ostringstream buffer; buffer - << "Abbrev| Rent|OutRent| Chest|iChest| Guard|CreateDate| StoredExp| Bank|Items| Ing |DayTax|Lvl|Test|\r\n"; + << "Abbrev| Rent|OutRent| Chest|iChest| Guard|CreateDate| StoredExp| Bank|Items| Ing |DayTax|Lvl|Test|Распущена\r\n"; std::string_view show("{:<4}|{:<6}|{:<7}|{:<6}|{:<6}|{:<7}|{:<10}|{:<15}|{:<10}|{:<5}|{:<5}|{:<6}|{:<3}|{:<4}|{:<9}\r\n"); int total_day_tax = 0; @@ -683,14 +683,14 @@ void Clan::HconShow(CharData *ch) { GET_ROOM_VNUM(clan->get_ingr_chest_room_rnum()), clan->guard, timeBuf, clan->clan_exp, clan->bank, clan->chest_objcount, clan->ingr_chest_objcount_, cost, clan->clan_level, - (clan->test_clan ? "y" : "n"), ((clan->m_members.size() > 0) ? "" : "")); + (clan->test_clan ? "y" : "n"), ((clan->m_members.size() > 0) ? "Нет" : "Да")); } buffer << "Total day tax: " << total_day_tax << "\r\n"; SendMsgToChar(buffer.str().c_str(), ch); } -// +// формирование содержимого основного файла клана std::string Clan::build_clan_file() const { std::ostringstream file; @@ -756,7 +756,7 @@ std::string Clan::build_clan_file() const { return file.str(); } -// / +// формирование содержимого пкл/дрл файла клана std::string Clan::build_pk_file() const { std::ostringstream file; @@ -772,7 +772,7 @@ std::string Clan::build_pk_file() const { namespace { -// , +// запись файла, если его содержимое изменилось с прошлого сохранения bool write_if_changed(const std::string &filename, const std::string &contents, std::optional &cache) { if (cache.has_value() && *cache == contents) { @@ -797,7 +797,7 @@ void Clan::save_clan_file(const std::string &filename) const { write_if_changed(filename, build_clan_file(), saved_clan_file_); } -// +// сохранение кланов в файлы void Clan::ClanSave() { std::ofstream index(LIB_CLANS "index"); if (!index.is_open()) { @@ -806,13 +806,13 @@ void Clan::ClanSave() { } for (auto &clan : Clan::ClanList) { - // ( ) + // именем файла для клана служит его аббревиатура (английский и нижний регистр) std::string buffer = clan->abbrev; CreateFileName(buffer); - // , + // в индексе дружина остается всегда, иначе она пропадет из игры после перезагрузки index << buffer << "\n"; - // : + // дружина без игроков считается распущенной: ее файлы больше не трогаем if (clan->m_members.empty()) { continue; } @@ -826,30 +826,30 @@ void Clan::ClanSave() { return; } } - // + // основной файл клана clan->save_clan_file(filepath); - // / + // пкл/дрл write_if_changed(filepath + ".pkl", clan->build_pk_file(), clan->saved_pk_file_); } index.close(); } /** -* -* CLAN() -* (stat file) +* проставляем персонажу нужные поля если он клановый +* отсюда и далее для проверки на клановость пользуем CLAN() +* может быть вызвана с пустым дескриптором персонажа (stat file) */ void Clan::SetClanData(CharData *ch) { CLAN(ch).reset(); CLAN_MEMBER(ch).reset(); - // , + // если правит свою дружину в олц, то радостно его выкидываем if (ch->desc && ch->desc->state == EConState::kClanedit) { ch->desc->clan_olc.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar(" - .\r\n", ch); + SendMsgToChar("Редактирование отменено из-за обновления ваших данных в дружине.\r\n", ch); } - // - , + // если куда-то приписан, то дергаем сразу итераторы на клан и список членов for (auto &clan : Clan::ClanList) { const auto member = clan->m_members.find(ch->get_uid()); if (member != clan->m_members.end()) { @@ -858,13 +858,13 @@ void Clan::SetClanData(CharData *ch) { break; } } - // + // никуда не приписан if (!CLAN(ch)) { free(GET_CLAN_STATUS(ch)); GET_CLAN_STATUS(ch) = nullptr; return; } - // - + // куда-то таки приписан std::string buffer; if (IsMale(ch)) buffer = CLAN(ch)->ranks[CLAN_MEMBER(ch)->rank_num] + " " + CLAN(ch)->title; @@ -872,13 +872,13 @@ void Clan::SetClanData(CharData *ch) { buffer = CLAN(ch)->ranks_female[CLAN_MEMBER(ch)->rank_num] + " " + CLAN(ch)->title_female; GET_CLAN_STATUS(ch) = str_dup(buffer.c_str()); - // + // чтобы при выходе не смог приписаться опять за один ребут мада if (ch->desc) { ch->desc->clan_invite.reset(); } } -// - +// проверка комнаты на принадлежность какому-либо замку Clan::shared_ptr Clan::GetClanByRoom(RoomRnum room) { for (const auto &clan : ClanList) { if (world[room]->zone_rn == world[GetRoomRnum(clan->rent)]->zone_rn) { @@ -889,7 +889,7 @@ Clan::shared_ptr Clan::GetClanByRoom(RoomRnum room) { return nullptr; } -// +// может ли персонаж зайти в замок bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { const auto clan = GetClanByRoom(room); if (!clan @@ -912,7 +912,7 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { int _mode = mode ? kHousePortal : kHouseAtrium; switch (_mode) { - // - + // вход через дверь - контролирует охранник case kHouseAtrium: for (const auto mobs : world[ch->in_room]->people) { if (clan->guard == GET_MOB_VNUM(mobs) @@ -921,20 +921,20 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { } } - // - + // охранника нет - свободный доступ return true; - // + // телепортация case kHousePortal: if (!isMember) { - SendMsgToChar(" - !\r\n", ch); + SendMsgToChar("Частная собственность - посторонним в ней делать нечего!\r\n", ch); return false; } - // + // с временным флагом тоже курят if (NORENTABLE(ch)) { if (mode == kHouseAtrium) { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Пускай сначала кровь с тебя стечет, а потом входи сколько угодно.\r\n", ch); } return false; } @@ -944,27 +944,27 @@ bool Clan::MayEnter(CharData *ch, RoomRnum room, bool mode) { return false; } -// +// в зависимости от доступности команды будут видны только нужные строки const char *HOUSE_FORMAT[] = { - " \r\n", - " \r\n", - " \r\n", - " \r\n", - " ()\r\n", - " < > <||>\r\n", - " <|>\r\n", - " | <|>\r\n", - " \r\n", - " \r\n", - " ()\r\n", - " ( )\r\n", - " ( )\r\n", - " < >\r\n", - " \r\n" + " клан информация\r\n", + " клан принять имя звание\r\n", + " клан изгнать имя\r\n", + " клан привилегии\r\n", + " гдругам (текст)\r\n", + " политика <имя дружины> <нейтралитет|война|альянс>\r\n", + " дрновости <писать|очистить>\r\n", + " пклист|дрлист <добавить|удалить>\r\n", + " класть вещи в хранилище\r\n", + " брать вещи из хранилища\r\n", + " казна (снимать)\r\n", + " клан покинуть (выход из дружины)\r\n", + " клан сообщение (написание сообщения дружины)\r\n", + " клан налог <процент отдаваемых кун>\r\n", + " дрвече писать\r\n" }; -// +// репутация int Clan::get_rep() { return this->reputation; } @@ -973,9 +973,9 @@ void Clan::set_rep(int rep) { this->reputation = rep; } -// house +// house информация void Clan::HouseInfo(CharData *ch) { - // , , + // думаю, вываливать список сортированный по уиду некрасиво, поэтому перебираем по рангам std::vector temp_list; for (const auto &it : m_members) { temp_list.push_back(it.second); @@ -987,7 +987,7 @@ void Clan::HouseInfo(CharData *ch) { }); std::ostringstream buffer; - buffer << " :\r\n"; + buffer << "К замку приписаны:\r\n"; size_t char_num = 0; std::string temp; @@ -1015,7 +1015,7 @@ void Clan::HouseInfo(CharData *ch) { char_num += it->name.size() + 1; } - buffer << "\r\n:\r\n"; + buffer << "\r\nПривилегии:\r\n"; int num = 0; for (auto it = ranks.begin(); it != ranks.end(); ++it, ++num) { @@ -1023,114 +1023,114 @@ void Clan::HouseInfo(CharData *ch) { for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (this->privileges[num][i]) { switch (i) { - case MAY_CLAN_INFO: buffer << " "; + case MAY_CLAN_INFO: buffer << " инфо"; break; - case MAY_CLAN_ADD: buffer << " "; + case MAY_CLAN_ADD: buffer << " принять"; break; - case MAY_CLAN_REMOVE: buffer << " "; + case MAY_CLAN_REMOVE: buffer << " изгнать"; break; - case MAY_CLAN_PRIVILEGES: buffer << " "; + case MAY_CLAN_PRIVILEGES: buffer << " привилегии"; break; - case MAY_CLAN_CHANNEL: buffer << " "; + case MAY_CLAN_CHANNEL: buffer << " гд"; break; - case MAY_CLAN_POLITICS: buffer << " "; + case MAY_CLAN_POLITICS: buffer << " политика"; break; - case MAY_CLAN_NEWS: buffer << " "; + case MAY_CLAN_NEWS: buffer << " дрн"; break; - case MAY_CLAN_PKLIST: buffer << " "; + case MAY_CLAN_PKLIST: buffer << " пкл"; break; - case MAY_CLAN_CHEST_PUT: buffer << " ."; + case MAY_CLAN_CHEST_PUT: buffer << " хран.полож"; break; - case MAY_CLAN_CHEST_TAKE: buffer << " ."; + case MAY_CLAN_CHEST_TAKE: buffer << " хран.взять"; break; - case MAY_CLAN_BANK: buffer << " "; + case MAY_CLAN_BANK: buffer << " казна"; break; - case MAY_CLAN_EXIT: buffer << " "; + case MAY_CLAN_EXIT: buffer << " выход"; break; - case MAY_CLAN_MOD: buffer << " "; + case MAY_CLAN_MOD: buffer << " сообщение дружины"; break; - case MAY_CLAN_TAX: buffer << " "; + case MAY_CLAN_TAX: buffer << " налог"; break; - case MAY_CLAN_BOARD: buffer << " "; + case MAY_CLAN_BOARD: buffer << " дрвече"; break; } } } buffer << "\r\n"; } - // , - buffer << " " << this->clan_exp - << " " << this->clan_level << "\r\n" - << " : " << this->exp - << " :), .\r\n" - << " " << this->get_rep() << " .\r\n" - << " " << this->ChestMaxObjects() + //инфа о экспе замка левеле замка, рейтинге замка и плюшках + buffer << "Ваш замок набрал " << this->clan_exp + << " очков опыта и имеет уровень " << this->clan_level << "\r\n" + << "Рейтинг вашего замка: " << this->exp + << " Это очень круто :), но ничего вам не дает.\r\n" + << "Ваша дружина имеет " << this->get_rep() << " очков репутации.\r\n" + << "В хранилище замка может храниться до " << this->ChestMaxObjects() << " " << grammar::GetDeclensionInNumber(this->ChestMaxObjects(), grammar::EWhat::kObjU) - << " " << this->ChestMaxWeight() << "\r\n" - << " " << this->ingr_chest_max_objects() + << " с общим весом не более чем " << this->ChestMaxWeight() << "\r\n" + << "В хранилище ингредиентов может храниться до " << this->ingr_chest_max_objects() << " " << grammar::GetDeclensionInNumber(this->ingr_chest_max_objects(), grammar::EWhat::kObjU) << ".\r\n"; - // + // инфа о банке и хранилище int cost = ChestTax(); int ingr_cost = ingr_chest_tax(); int options_tax = calculate_clan_tax(); int total_tax = cost + ingr_cost + options_tax; - buffer << " " << this->chest_objcount << " " + buffer << "В хранилище вашей дружины " << this->chest_objcount << " " << grammar::GetDeclensionInNumber(this->chest_objcount, grammar::EWhat::kObject) - << " " << this->chest_weight - << " (" << cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str() << " ).\r\n" - << " " << ingr_chest_objcount_ << " " + << " общим весом в " << this->chest_weight + << " (" << cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str() << " в день).\r\n" + << "В хранилище ингредиентов " << ingr_chest_objcount_ << " " << grammar::GetDeclensionInNumber(ingr_chest_objcount_, grammar::EWhat::kObject) - << " (" << ingr_cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(ingr_cost, grammar::ECase::kNom).c_str() << " ).\r\n\r\n" - << " : " << this->bank << " " + << " (" << ingr_cost << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(ingr_cost, grammar::ECase::kNom).c_str() << " в день).\r\n\r\n" + << "Состояние казны: " << this->bank << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(this->bank, grammar::ECase::kNom).c_str() << ".\r\n" - << " : " << options_tax << " " + << "Расходы на инфраструктуру замка: " << options_tax << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(options_tax, grammar::ECase::kNom).c_str() - << " , : " << total_tax << " " - << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(total_tax, grammar::ECase::kNom).c_str() << " .\r\n"; + << " в день, Общие расходы: " << total_tax << " " + << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(total_tax, grammar::ECase::kNom).c_str() << " в день.\r\n"; if (total_tax <= 0) { - buffer << " .\r\n"; + buffer << "Ваших денег хватит на нереальное количество дней.\r\n"; } else { - buffer << " " + buffer << "Ваших денег хватит примерно на " << bank / total_tax << " " << grammar::GetDeclensionInNumber(bank / total_tax, grammar::EWhat::kDay) << ".\r\n"; } - buffer << " : " << get_gold_tax_pct() << "%\r\n"; + buffer << "Налог для ратников дружины: " << get_gold_tax_pct() << "%\r\n"; SendMsgToChar(buffer.str(), ch); exp_history.show(ch); } -// , +// клан принять, повлиять можно только на соклановцев ниже рангом void Clan::HouseAdd(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); if (buffer2.empty()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Укажите имя персонажа.\r\n", ch); return; } long unique = GetUniqueByName(buffer2); if (!unique) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестный персонаж.\r\n", ch); return; } std::string name = buffer2; native_text::capitalize_first(name); if (unique == ch->get_uid()) { - SendMsgToChar(" , ?\r\n", ch); + SendMsgToChar("Сам себя повысил, самому себе вынес благодарность?\r\n", ch); return; } - // - // + // изменение звания у членов дружины + // даже если они находятся оффлайн const auto it_member = this->m_members.find(unique); if (it_member != this->m_members.end()) { if (it_member->second->rank_num <= CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы можете менять звания только у нижестоящих членов дружины.\r\n", ch); return; } @@ -1139,7 +1139,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { GetOneParam(buffer, buffer2); if (buffer2.empty()) { - buffer = " .\r\n : "; + buffer = "Укажите звание персонажа.\r\nДоступные положения: "; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) buffer += "'" + *it + "' "; buffer += "\r\n"; @@ -1157,21 +1157,21 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { editedChar = d->character; Clan::SetClanData(d->character.get()); SendMsgToChar(d->character.get(), - "%s , %s.%s\r\n", + "%sВаше звание изменили, теперь вы %s.%s\r\n", kColorWht, (*it).c_str(), kColorNrm); - sprintf(buf, " %s", (*it).c_str()); + sprintf(buf, "Звание в дружине изменено на %s", (*it).c_str()); AddKarma(d->character.get(), buf, ch->get_name().c_str()); } - // + // оповещение соклановцев о изменении звания for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && CLAN(d->character) && CLAN(d->character)->GetRent() == this->GetRent() && editedChar != d->character) { - SendMsgToChar(d->character.get(), "%s%s %s.%s\r\n", + SendMsgToChar(d->character.get(), "%s%s теперь %s.%s\r\n", kColorWht, it_member->second->name.c_str(), (*it).c_str(), kColorNrm); } } @@ -1180,7 +1180,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } } - buffer = " , :\r\n"; + buffer = "Неверное звание, доступные положения:\r\n"; for (std::vector::const_iterator it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) { buffer += "'" + *it + "' "; } @@ -1192,40 +1192,40 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { DescriptorData *d = DescriptorByUid(unique); if (!d || !sight::CanSee(ch, d->character)) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Этого персонажа нет в игре!\r\n", ch); return; } if (d->character->IsFlagged(EPrf::kCoderinfo) || (GetRealLevel(d->character) >= kLvlGod)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не можете приписать этого игрока.\r\n", ch); return; } if (CLAN(d->character) && CLAN(ch) != CLAN(d->character)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не можете приписать члена другой дружины.\r\n", ch); return; } if (d->clan_invite) { if (d->clan_invite->clan == CLAN(ch)) { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Вы уже пригласили этого игрока, ждите реакции.\r\n", ch); return; } else { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Он уже приглашен в другую дружину, дождитесь его ответа и пригласите снова.\r\n", ch); return; } } GetOneParam(buffer, buffer2); - // 0 10 + // чтобы учесть воеводу с 0 рангом во время приписки и не дать ему приписать еще 10 воевод int rank = CLAN_MEMBER(ch)->rank_num; if (!rank) { ++rank; } if (buffer2.empty()) { - buffer = " .\r\n : "; + buffer = "Укажите звание персонажа.\r\nДоступные положения: "; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) buffer += "'" + *it + "' "; buffer += "\r\n"; @@ -1233,25 +1233,25 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { return; } - int temp_rank = rank; // rank + int temp_rank = rank; // дальше rank тоже мб использован в случае неверного выбора for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it, ++temp_rank) { if (CompareParam(buffer2, *it)) { - // - , + // не приписан - втыкаем ему приглашение и курим, пока не согласится std::shared_ptr temp_invite(new ClanInvite); temp_invite->clan = CLAN(ch); temp_invite->rank = temp_rank; - temp_invite->invite_name = " " + ch->get_name(); + temp_invite->invite_name = "Игроком " + ch->get_name(); d->clan_invite = temp_invite; buffer = kColorWht; - buffer += "$N $G , - " + *it + "."; + buffer += "$N приглашен$G в вашу дружину, статус - " + *it + "."; buffer += kColorNrm; - // + // оповещаем счастливца act(buffer.c_str(), false, ch, nullptr, d->character.get(), kToChar); buffer = kColorWht; - buffer += " '" + this->name + "', - " + *it + ".\r\n" - + " ' ', ' '.\r\n" - + " , .\r\n" - + " .\r\n"; + buffer += "Вы получили приглашение в дружину '" + this->name + "', статус - " + *it + ".\r\n" + + "Чтобы принять приглашение наберите 'клан согласен', для отказа 'клан отказать'.\r\n" + + "Никто не сможет послать вам новое приглашение до тех пор, пока вы не разберетесь с этим.\r\n" + + "Настоятельно рекомендуем ознакомиться с уставом дружины и разделом справки ПРАВИЛАДРУЖИНЫ.\r\n"; buffer += kColorNrm; SendMsgToChar(buffer, d->character.get()); @@ -1259,7 +1259,7 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } } - buffer = " , :\r\n"; + buffer = "Неверное звание, доступные положения:\r\n"; for (auto it = this->ranks.begin() + rank; it != this->ranks.end(); ++it) { buffer += "'" + *it + "' "; } @@ -1269,8 +1269,8 @@ void Clan::HouseAdd(CharData *ch, std::string &buffer) { } /** -* , -* . +* Отписывание персонажа от клана с оповещением всех заинтересованных сторон, +* выдворением за пределы замка и изменением ренты при необходимости. */ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { const auto it = m_members.find(key); @@ -1281,17 +1281,17 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { DescriptorData *k = DescriptorByUid(unique); if (k && k->character) { Clan::SetClanData(k->character.get()); - SendMsgToChar(k->character.get(), " '%s'!\r\n", this->name.c_str()); - sprintf(buf, "() '%s'", this->name.c_str()); + SendMsgToChar(k->character.get(), "Вас исключили из дружины '%s'!\r\n", this->name.c_str()); + sprintf(buf, "Исключен(а) из дружины '%s'", this->name.c_str()); AddKarma(k->character.get(), buf, reason); const auto clan = Clan::GetClanByRoom(k->character->in_room); if (clan) { char_from_room(k->character); - act("$n $g $a !", true, k->character.get(), nullptr, nullptr, kToRoom); - SendMsgToChar(" !\r\n", k->character.get()); + act("$n был$g выдворен$a за пределы замка!", true, k->character.get(), nullptr, nullptr, kToRoom); + SendMsgToChar("Вы были выдворены за пределы замка!\r\n", k->character.get()); PlaceCharToRoom(k->character.get(), GetRoomRnum(clan->out_rent)); sight::look_at_room(k->character.get(), GetRoomRnum(clan->out_rent)); - act("$n $u , - !", + act("$n свалил$u с небес, выкрикивая какие-то ругательства!", true, k->character.get(), nullptr, @@ -1302,7 +1302,7 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { Player p_vict; CharData *vict = &p_vict; if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) > -1) { - sprintf(buf, "() '%s'", this->name.c_str()); + sprintf(buf, "Исключен(а) из дружины '%s'", this->name.c_str()); AddKarma(vict, buf, reason); vict->save_char(); } @@ -1312,12 +1312,12 @@ void Clan::remove_member(const ClanMembersList::key_type &key, char *reason) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->GetRent()) { native_text::capitalize_first(name); - SendMsgToChar(d->character.get(), "%s .\r\n", name.c_str()); + SendMsgToChar(d->character.get(), "%s более не является членом вашей дружины.\r\n", name.c_str()); } } } -// house ( ) +// house изгнать (только званием ниже своего) void Clan::HouseRemove(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -1325,17 +1325,17 @@ void Clan::HouseRemove(CharData *ch, std::string &buffer) { const auto it = this->m_members.find(unique); if (buffer2.empty()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Укажите имя персонажа.\r\n", ch); } else if (!unique) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестный персонаж.\r\n", ch); } else if (unique == ch->get_uid()) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("Выглядит довольно странно...\r\n", ch); } if (it == this->m_members.end()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Он и так не приписан к вашей дружине.\r\n", ch); } else if (it->second->rank_num <= CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы можете исключить из дружины только персонажа со званием ниже вашего.\r\n", ch); } else { char tmpstr[kMaxInputLength]; sprintf(tmpstr, "%s", GET_NAME(ch)); @@ -1345,8 +1345,8 @@ void Clan::HouseRemove(CharData *ch, std::string &buffer) { void Clan::HouseLeave(CharData *ch) { if (!CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" , .\r\n" - " , ...\r\n", ch); + SendMsgToChar("Если вы хотите распустить свою дружину, то обращайтесь к Богам.\r\n" + "А если вам просто нечем заняться, то передайте воеводство и идите куда хотите...\r\n", ch); return; } @@ -1354,12 +1354,12 @@ void Clan::HouseLeave(CharData *ch) { const auto it = this->m_members.find(member_id); if (it != this->m_members.end()) { char tmpstr[kMaxInputLength]; - sprintf(tmpstr, ""); + sprintf(tmpstr, "самовыход"); remove_member(member_id, tmpstr); } } -// +// удаляет объект из хранилищ клана int Clan::delete_obj(int vnum) { int num = 0; for (auto &clan : Clan::ClanList) { @@ -1378,11 +1378,11 @@ int Clan::delete_obj(int vnum) { return num; } -// . Savers - -- -// dirty- ( ), dirty : -// , . dirty -// ޣ ( take , ). -// obj_point_update, ݣ ( ExtractObjFromWorld). +// Предмет рассыпался по таймеру внутри кланового хранилища. Savers сундука и ингр-храна -- +// dirty-трекеры (сохраняют только помеченные кланы), а распад сам по себе dirty не метил: +// файл не переписывался, и предмет возвращался из него после ребута. Метим владельца dirty и +// уменьшаем счётчик вещей (при take он уменьшается, при распаде забывали). Вызывается из +// obj_point_update, пока предмет ещё лежит в сундуке (до ExtractObjFromWorld). void Clan::OnChestObjDecay(ObjData *j) { ObjData *container = j ? j->get_in_obj() : nullptr; if (!container) { @@ -1409,13 +1409,13 @@ void Clan::OnChestObjDecay(ObjData *j) { } } -// * hcontrol outcast - , . +// * hcontrol outcast имя - отписывание любого персонажа от дружины, кроме воеводы. void Clan::hcon_outcast(CharData *ch, std::string &buffer) { std::string name; GetOneParam(buffer, name); long member_uid = GetUniqueByName(name); if (!member_uid) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестный персонаж.\r\n", ch); return; } for (const auto &clan : Clan::ClanList) { @@ -1423,30 +1423,30 @@ void Clan::hcon_outcast(CharData *ch, std::string &buffer) { if (it != clan->m_members.end()) { if (!it->second->rank_num) { SendMsgToChar(ch, - " , hcontrol destroy.\r\n"); + "Вы не можете исключить воеводу, для удаления дружины существует hcontrol destroy.\r\n"); return; } char tmpstr[kMaxInputLength]; - sprintf(tmpstr, " %s", GET_NAME(ch)); + sprintf(tmpstr, "Богом %s", GET_NAME(ch)); clan->remove_member(member_uid, tmpstr); native_text::capitalize_first(name); - SendMsgToChar(ch, "%s (a) '%s'.\r\n", name.c_str(), clan->name.c_str()); + SendMsgToChar(ch, "%s исключен(a) из дружины '%s'.\r\n", name.c_str(), clan->name.c_str()); return; } } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Он и так не состоит ни в какой дружине.\r\n", ch); } -// , , / +// бог, текст, клан/альянс void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { utils::Trim(text); - // , - , 5, ? ) + // на счет скобок я хз, по-моему так нагляднее все же в ифах, где условий штук 5, мож опять индентом пройтись? Ж) if (text.empty()) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Что вы хотите им сообщить?\r\n", ch); return; } switch (subcmd) { - // - + // большой БОГ говорит какой-то дружине case kScmdChannel: for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character @@ -1456,17 +1456,17 @@ void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { && CLAN(d->character).get() == this && !AFF_FLAGGED(d->character, EAffect::kDeafness)) { SendMsgToChar(d->character.get(), - "%s : %s'%s'%s\r\n", + "%s ВАШЕЙ дружине: %s'%s'%s\r\n", GET_NAME(ch), kColorBoldRed, text.c_str(), kColorNrm); } } - SendMsgToChar(ch, " %s: %s'%s'.%s\r\n", this->abbrev.c_str(), kColorBoldRed, text.c_str(), kColorNrm); + SendMsgToChar(ch, "Вы дружине %s: %s'%s'.%s\r\n", this->abbrev.c_str(), kColorBoldRed, text.c_str(), kColorNrm); break; - // , + // он же в канал союзников этой дружины, если они вообще есть case kScmdAchannel: for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character @@ -1476,35 +1476,35 @@ void Clan::GodToChannel(CharData *ch, std::string text, int subcmd) { && d->character.get() != ch) { if (CheckPolitics(CLAN(d->character)->GetRent()) == kPoliticsAlliance || CLAN(d->character).get() == this) { - // , + // проверка на альянс с обеих сторон, иначе это не альянс if (CLAN(d->character).get() != this) { if (CLAN(d->character)->CheckPolitics(this->rent) == kPoliticsAlliance) { SendMsgToChar(d->character.get(), - "%s : %s'%s'%s\r\n", + "%s ВАШИМ СОЮЗНИКАМ: %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); } } - // + // первоначальному клану выдается всегда else { SendMsgToChar(d->character.get(), - "%s : %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); + "%s ВАШИМ СОЮЗНИКАМ: %s'%s'%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), kColorNrm); } } } } - SendMsgToChar(ch, " %s: %s'%s'.%s\r\n", abbrev.c_str(), kColorBoldGrn, text.c_str(), kColorNrm); + SendMsgToChar(ch, "Вы союзникам %s: %s'%s'.%s\r\n", abbrev.c_str(), kColorBoldGrn, text.c_str(), kColorNrm); break; } } -// ( ), , / +// чар (или клановый бог), текст, клан/альянс void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { utils::Trim(text); if (text.empty()) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Что вы хотите сообщить?\r\n", ch); return; } @@ -1514,15 +1514,15 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { } if (ch->IsFlagged(EPlrFlag::kDumbed)) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Вам запрещено обращаться к другим игрокам!\r\n", ch); return; } switch (subcmd) { - // + // своей дружине case kScmdChannel: - // - snprintf(buf, kMaxStringLength, "%s : &R'%s'.&n\r\n", GET_NAME(ch), text.c_str()); + // вспомнить + snprintf(buf, kMaxStringLength, "%s дружине: &R'%s'.&n\r\n", GET_NAME(ch), text.c_str()); CLAN(ch)->add_remember(buf, Remember::CLAN); for (auto d = descriptor_list; d; d = d->next) { @@ -1534,7 +1534,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { && !ignores(d->character.get(), ch, EIgnore::kClan)) { snprintf(buf, kMaxStringLength, - "%s : %s'%s'.%s\r\n", + "%s дружине: %s'%s'.%s\r\n", GET_NAME(ch), kColorBoldRed, text.c_str(), @@ -1546,7 +1546,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { snprintf(buf, kMaxStringLength, - " : %s'%s'.%s\r\n", + "Вы дружине: %s'%s'.%s\r\n", kColorBoldRed, text.c_str(), kColorNrm); @@ -1555,10 +1555,10 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { break; - // + // союзникам case kScmdAchannel: - // - snprintf(buf, kMaxStringLength, "%s : &G'%s'.&n\r\n", GET_NAME(ch), text.c_str()); + // вспомнить + snprintf(buf, kMaxStringLength, "%s союзникам: &G'%s'.&n\r\n", GET_NAME(ch), text.c_str()); for (auto &clan : Clan::ClanList) { if ((CLAN(ch)->CheckPolitics(clan->GetRent()) == kPoliticsAlliance && clan->CheckPolitics(CLAN(ch)->GetRent()) == kPoliticsAlliance) @@ -1576,12 +1576,12 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { && !ignores(d->character.get(), ch, EIgnore::kAlliance)) { if (CLAN(ch)->CheckPolitics(CLAN(d->character)->GetRent()) == kPoliticsAlliance || CLAN(ch) == CLAN(d->character)) { - // , + // проверка на альянс с обеих сторон, шоб не спамили друг другу на зло if ((CLAN(d->character)->CheckPolitics(CLAN(ch)->GetRent()) == kPoliticsAlliance) || CLAN(ch) == CLAN(d->character)) { snprintf(buf, kMaxStringLength, - "%s : %s'%s'.%s\r\n", + "%s союзникам: %s'%s'.%s\r\n", GET_NAME(ch), kColorBoldGrn, text.c_str(), @@ -1595,7 +1595,7 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { snprintf(buf, kMaxStringLength, - " : %s'%s'.%s\r\n", + "Вы союзникам: %s'%s'.%s\r\n", kColorBoldGrn, text.c_str(), kColorNrm); @@ -1606,8 +1606,8 @@ void Clan::CharToChannel(CharData *ch, std::string text, int subcmd) { } // switch } -// clan victim -// +// возвращает состояние политики clan по отношению к victim +// при отсутствии подразумевается нейтралитет int Clan::CheckPolitics(int victim) { auto it = politics.find(victim); if (it != politics.end()) { @@ -1616,8 +1616,8 @@ int Clan::CheckPolitics(int victim) { return kPoliticsNeutral; } -// (state) victim -// , +// выставляем клану политику(state) по отношению к victim +// нейтралитет означает просто удаление записи, если она была void Clan::SetPolitics(int victim, int state) { auto it = politics.find(victim); if (it == politics.end() && state == kPoliticsNeutral) @@ -1628,9 +1628,9 @@ void Clan::SetPolitics(int victim, int state) { politics[victim] = state; } -const char *politicsnames[] = {"", "", ""}; +const char *politicsnames[] = {"Нейтралитет", "Война", "Альянс"}; -// , +// показывает, может ли чар писать в дрв bool Clan::check_write_board(CharData *ch) { if (this->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_BOARD]) { return true; @@ -1655,12 +1655,12 @@ void Clan::SetPk(CharData *ch, std::string buffer) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, " %d .\r\n", vnum); + SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", vnum); return; } clan->get()->change_pk_status(); - SendMsgToChar(ch, " .\r\n"); + SendMsgToChar(ch, "Статус дружины изменен.\r\n"); } bool char_to_pk_clan(CharData *ch) { @@ -1669,7 +1669,7 @@ bool char_to_pk_clan(CharData *ch) { return false; } -// +// выставляем политику с уведомлением другой дружины о войне или альянсе void Clan::ManagePolitics(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -1681,54 +1681,54 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { if (CompareParam(buffer2, (*vict)->abbrev)) break; if ((vict == Clan::ClanList.end()) || ((*vict)->m_members.size() == 0)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такой дружины.\r\n", ch); return; } if (*vict == CLAN(ch)) { - SendMsgToChar(" ? ...\r\n", ch); + SendMsgToChar("Менять политику по отношению к своей дружине? Что за бред...\r\n", ch); return; } GetOneParam(buffer, buffer2); if (buffer2.empty()) - SendMsgToChar(" : ||.\r\n", ch); - else if (CompareParam(buffer2, "")) { + SendMsgToChar("Укажите действие: нейтралитет|война|альянс.\r\n", ch); + else if (CompareParam(buffer2, "нейтралитет")) { if (CheckPolitics((*vict)->rent) == kPoliticsNeutral) { SendMsgToChar(ch, - " %s.\r\n", + "Ваша дружина уже находится в состоянии нейтралитета с дружиной %s.\r\n", (*vict)->abbrev.c_str()); return; } SetPolitics((*vict)->rent, kPoliticsNeutral); SetWait(ch, 1, false); - // + // уведомляем обе дружины for (d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { - SendMsgToChar(d->character.get(), "%s %s !%s\r\n", + SendMsgToChar(d->character.get(), "%sДружина %s заключила с вашей дружиной нейтралитет!%s\r\n", kColorWht, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { - SendMsgToChar(d->character.get(), "%s %s !%s\r\n", + SendMsgToChar(d->character.get(), "%sВаша дружина заключила с дружиной %s нейтралитет!%s\r\n", kColorWht, (*vict)->abbrev.c_str(), kColorNrm); } } } - if (!ch->IsFlagged(EPrf::kPolitMode)) // + if (!ch->IsFlagged(EPrf::kPolitMode)) // а то сам может не увидеть нафик { - SendMsgToChar(ch, "%s %s !%s\r\n", + SendMsgToChar(ch, "%sВаша дружина заключила с дружиной %s нейтралитет!%s\r\n", kColorWht, (*vict)->abbrev.c_str(), kColorNrm); } - } else if (CompareParam(buffer2, "")) { + } else if (CompareParam(buffer2, "война")) { if (CheckPolitics((*vict)->rent) == kPoliticsWar) { - SendMsgToChar(ch, " %s.\r\n", (*vict)->abbrev.c_str()); + SendMsgToChar(ch, "Ваша дружина уже воюет с дружиной %s.\r\n", (*vict)->abbrev.c_str()); return; } SetPolitics((*vict)->rent, kPoliticsWar); SetWait(ch, 1, false); - // + // тож самое for (d = descriptor_list; d; d = d->next) { if (d->character @@ -1736,13 +1736,13 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { SendMsgToChar(d->character.get(), - "%s %s !%s\r\n", + "%sДружина %s объявила вашей дружине войну!%s\r\n", kColorBoldRed, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { SendMsgToChar(d->character.get(), - "%s %s !%s\r\n", + "%sВаша дружина объявила дружине %s войну!%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); @@ -1752,29 +1752,29 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { if (!ch->IsFlagged(EPrf::kPolitMode)) { SendMsgToChar(ch, - "%s %s !%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); + "%sВаша дружина объявила дружине %s войну!%s\r\n", kColorBoldRed, (*vict)->abbrev.c_str(), kColorNrm); } - } else if (CompareParam(buffer2, "")) { + } else if (CompareParam(buffer2, "альянс")) { if (CheckPolitics((*vict)->rent) == kPoliticsAlliance) { - SendMsgToChar(ch, " %s.\r\n", (*vict)->abbrev.c_str()); + SendMsgToChar(ch, "Ваша дружина уже в альянсе с дружиной %s.\r\n", (*vict)->abbrev.c_str()); return; } SetWait(ch, 1, false); SetPolitics((*vict)->rent, kPoliticsAlliance); - // + // тож самое for (d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying && d->character->IsFlagged(EPrf::kPolitMode)) { if (CLAN(d->character) == *vict) { SendMsgToChar(d->character.get(), - "%s %s !%s\r\n", + "%sДружина %s заключила с вашей дружиной альянс!%s\r\n", kColorGrn, this->abbrev.c_str(), kColorNrm); } else if (CLAN(d->character) == CLAN(ch)) { SendMsgToChar(d->character.get(), - "%s %s!%s\r\n", + "%sВаша дружина заключила альянс с дружиной %s!%s\r\n", kColorGrn, (*vict)->abbrev.c_str(), kColorNrm); @@ -1783,26 +1783,26 @@ void Clan::ManagePolitics(CharData *ch, std::string &buffer) { } if (!ch->IsFlagged(EPrf::kPolitMode)) { - SendMsgToChar(ch, "%s %s!%s\r\n", + SendMsgToChar(ch, "%sВаша дружина заключила альянс с дружиной %s!%s\r\n", kColorGrn, (*vict)->abbrev.c_str(), kColorNrm); } } } const char *HCONTROL_FORMAT = - ": hcontrol build \r\n" + "Формат: hcontrol build \r\n" " hcontrol show\r\n" - " hcontrol destroy - \r\n" - " hcontrol outcast - \r\n" + " hcontrol destroy - удалить дружину\r\n" + " hcontrol outcast - исключить игрока из дружины\r\n" " hcontrol save\r\n" - " hcontrol title < > < >\r\n" - " hcontrol rank < > < > < >\r\n" - " hcontrol owner < >\r\n" - " hcontrol ingr \r\n" - " hcontrol exphitory < >\r\n" - " hcontrol pk \r\n"; - -// * hcontrol title - . + " hcontrol title <аббревиатура для муж рода> <аббревиатура для жен рода>\r\n" + " hcontrol rank <старое звание муж рода> <звание для муж рода> <звание для жен рода>\r\n" + " hcontrol owner <имя нового воеводы>\r\n" + " hcontrol ingr \r\n" + " hcontrol exphitory <число месяцев>\r\n" + " hcontrol pk \r\n"; + +// * hcontrol title - изменение аббревиатуры клана в титуле персонажа. void Clan::hcontrol_title(CharData *ch, std::string &text) { std::string buffer; @@ -1814,7 +1814,7 @@ void Clan::hcontrol_title(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); return; } @@ -1838,10 +1838,10 @@ void Clan::hcontrol_title(CharData *ch, std::string &text) { } } - SendMsgToChar(".\r\n", ch); + SendMsgToChar("Сделано.\r\n", ch); } -// * hcontrol rank - . +// * hcontrol rank - изменение кланового звания персонажа в титуле. void Clan::hcontrol_rank(CharData *ch, std::string &text) { std::string buffer; @@ -1853,7 +1853,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); return; } @@ -1866,7 +1866,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { return; } if (rank_male.size() > MAX_RANK_LENGHT || rank_female.size() > MAX_RANK_LENGHT) { - SendMsgToChar(ch, " %d .\r\n", MAX_RANK_LENGHT); + SendMsgToChar(ch, "Звание не должно быть длиннее %d символов.\r\n", MAX_RANK_LENGHT); return; } @@ -1883,7 +1883,7 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { } } catch (...) { - SendMsgToChar(ch, " .\r\n"); + SendMsgToChar(ch, "Ошибка в званиях дружины.\r\n"); } Clan::ClanSave(); @@ -1894,12 +1894,12 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { Clan::SetClanData(d->character.get()); } } - SendMsgToChar(".\r\n", ch); + SendMsgToChar("Сделано.\r\n", ch); } /** -* - . -* \param text - , - 0 ( ). +* Распечатка списка кланов с историей экспы за указанное кол-во месяцев. +* \param text - число последних месяцев, если пустая строка - 0 (только текущий месяц). */ void Clan::hcontrol_exphistory(CharData *ch, std::string &text) { if (!ch->IsFlagged(EPrf::kCoderinfo)) { @@ -1914,7 +1914,7 @@ void Clan::hcontrol_exphistory(CharData *ch, std::string &text) { month = std::stoi(text, nullptr, 10); } catch (const std::invalid_argument &) { - SendMsgToChar(ch, " (\"hcontrol exp <- >\")."); + SendMsgToChar(ch, "Неверный формат (\"hcontrol exp <кол-во последних месяцев>\")."); return; } } @@ -1935,7 +1935,7 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { return; } - // <> <> - buffer2, text + // <клан> <комната> - buffer2, text std::string buffer2; GetOneParam(text, buffer2); utils::Trim(text); @@ -1947,14 +1947,14 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { vnumum = std::stol(text, nullptr, 10); } catch (const std::invalid_argument &) { - SendMsgToChar(ch, " (\"hcontrol ingr <-> < >\")."); + SendMsgToChar(ch, "Неверный формат (\"hcontrol ingr <клан-рента> <комната хранилища>\")."); return; } } int room_rnum = GetRoomRnum(vnumum); if (room_rnum <= 0) { - SendMsgToChar(ch, " %d .", vnumum); + SendMsgToChar(ch, "Комнаты %d не существует.", vnumum); return; } @@ -1965,16 +1965,16 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { } } if (i == iend) { - SendMsgToChar(ch, " %d .", clan_vnum); + SendMsgToChar(ch, "Клана %d не существует.", clan_vnum); return; } if ((*i)->GetRent() / 100 != vnumum / 100) { - SendMsgToChar(ch, " %d %d.", vnumum, (*i)->GetRent()); + SendMsgToChar(ch, "Комната %d находится вне зоны замка %d.", vnumum, (*i)->GetRent()); return; } bool chest_moved = false; - // + // хран под ингры уже был if ((*i)->ingr_chest_active()) { for (auto chest : world[(*i)->get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { @@ -1994,120 +1994,120 @@ void Clan::hcontrol_set_ingr_chest(CharData *ch, std::string &text) { if (chest) { PlaceObjToRoom(chest.get(), (*i)->get_ingr_chest_room_rnum()); } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Хранилище установлено.\r\n", ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Хранилище перенесено.\r\n", ch); } } -// (hcontrol build) +// создание дружины (hcontrol build) void Clan::HcontrolBuild(CharData *ch, std::string &buffer) { - // , + // парсим все параметры, чтобы сразу проверить на ввод всех полей std::string buffer2; - // + // рента GetOneParam(buffer, buffer2); int rent = atoi(buffer2.c_str()); - // + // рента вне замка GetOneParam(buffer, buffer2); int out_rent = atoi(buffer2.c_str()); - // + // охранник GetOneParam(buffer, buffer2); MobVnum guard = atoi(buffer2.c_str()); - // + // воевода std::string owner; GetOneParam(buffer, owner); - // + // аббревиатура std::string abbrev; GetOneParam(buffer, abbrev); - // + // название клана std::string name = buffer; - // + // тут проверяем наличие все этого дела if (name.empty()) { SendMsgToChar(HCONTROL_FORMAT, ch); return; } if (!GetRoomRnum(rent)) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Комнаты %d не существует.\r\n", rent); return; } if (!GetRoomRnum(out_rent)) { - SendMsgToChar(ch, " %d .\r\n", out_rent); + SendMsgToChar(ch, "Комнаты %d не существует.\r\n", out_rent); return; } if (GetMobRnum(guard) < 0) { - SendMsgToChar(ch, " %d .\r\n", guard); + SendMsgToChar(ch, "Моба %d не существует.\r\n", guard); return; } long unique = 0; if (!(unique = GetUniqueByName(owner))) { - SendMsgToChar(ch, " %s .\r\n", owner.c_str()); + SendMsgToChar(ch, "Персонажа %s не существует.\r\n", owner.c_str()); return; } - // - + // а тут - не занят ли параметр другим кланом for (const auto &clan : Clan::ClanList) { if (clan->rent == rent) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Комната %d уже занята другой дружиной.\r\n", rent); return; } if (clan->guard == guard) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Охранник %d уже занят другой дружиной.\r\n", rent); return; } const auto it = clan->m_members.find(unique); if (it != clan->m_members.end()) { - SendMsgToChar(ch, "%s %s.\r\n", owner.c_str(), clan->abbrev.c_str()); + SendMsgToChar(ch, "%s уже приписан к дружине %s.\r\n", owner.c_str(), clan->abbrev.c_str()); return; } if (CompareParam(clan->abbrev, abbrev, 1)) { - SendMsgToChar(ch, " '%s' .\r\n", abbrev.c_str()); + SendMsgToChar(ch, "Аббревиатура '%s' уже занята другой дружиной.\r\n", abbrev.c_str()); return; } if (CompareParam(clan->name, name, 1)) { - SendMsgToChar(ch, " '%s' .\r\n", name.c_str()); + SendMsgToChar(ch, "Имя '%s' уже занято другой дружиной.\r\n", name.c_str()); return; } } - // + // собственно клан const auto tempClan = std::make_shared(); tempClan->rent = rent; tempClan->out_rent = out_rent; tempClan->chest_room = rent; tempClan->guard = guard; - // + // пишем воеводу native_text::capitalize_first(owner); tempClan->owner = owner; const auto tempMember = std::make_shared(); tempMember->name = owner; tempClan->m_members.set(unique, tempMember); - // + // названия tempClan->name = name; tempClan->builtOn = time(nullptr); tempClan->title_female = tempClan->title = tempClan->abbrev = abbrev; - // + // ранги const char - *ranks[] = {"", "", "", "", "", "", "", "", "", ""}; - // , ... + *ranks[] = {"воевода", "боярин", "десятник", "храбр", "кметь", "гридень", "муж", "вой", "отрок", "гость"}; + // женский род пока тоже самое, а то воплей будет... const char *ranks_female[] = - {"", "", "", "", "", "", "", "", "", ""}; + {"воевода", "боярин", "десятник", "храбр", "кметь", "гридень", "муж", "вой", "отрок", "гость"}; std::vector temp_ranks(ranks, ranks + 10); std::vector temp_ranks_female(ranks_female, ranks_female + 10); tempClan->ranks = temp_ranks; tempClan->bank = 1000; tempClan->ranks_female = temp_ranks_female; - // + // привилегии for (std::vector::const_iterator it = tempClan->ranks.begin(); it != tempClan->ranks.end(); ++it) { tempClan->privileges.push_back(std::bitset()); } - // + // воеводе проставим все привилегии for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { tempClan->privileges[0].set(i); } - // + // залоадим сразу хранилище const auto chest = world_objects.create_from_prototype_by_vnum(CLAN_CHEST_VNUM); if (chest) { PlaceObjToRoom(chest.get(), GetRoomRnum(tempClan->chest_room)); @@ -2117,17 +2117,17 @@ void Clan::HcontrolBuild(CharData *ch, std::string &buffer) { Clan::ClanSave(); Boards::Static::ClanInit(); - // + // уведомляем счастливых воеводу и имма DescriptorData *d = DescriptorByUid(unique); if (d) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), " . !\r\n"); + SendMsgToChar(d->character.get(), "Вы стали хозяином нового замка. Добро пожаловать!\r\n"); } - SendMsgToChar(ch, " '%s' !\r\n", abbrev.c_str()); + SendMsgToChar(ch, "Дружина '%s' создана!\r\n", abbrev.c_str()); } -// (hcontrol destroy) +// удаление дружины (hcontrol destroy) void Clan::HcontrolDestroy(CharData *ch, std::string &buffer) { int rent = atoi(buffer.c_str()); @@ -2139,14 +2139,14 @@ void Clan::HcontrolDestroy(CharData *ch, std::string &buffer) { } if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, " %d .\r\n", rent); + SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", rent); return; } char smallbuf[128]; - sprintf(smallbuf, "_"); + sprintf(smallbuf, "распущена_богами"); DestroyClan(*clan, smallbuf); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Дружина распущена.\r\n", ch); } void Clan::fix_clan_members_load_room(Clan::shared_ptr clan) { @@ -2184,7 +2184,7 @@ void Clan::fix_clan_members_load_room(Clan::shared_ptr clan) { delete cbuf; } - sprintf(buf, "CLAN: , %s [%s]", player_table[i].name().c_str(), clan->name.c_str()); + sprintf(buf, "CLAN: Роспуск, удаляю игрока %s [%s]", player_table[i].name().c_str(), clan->name.c_str()); log("%s", buf); } } @@ -2193,7 +2193,7 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { fix_clan_members_load_room(clan); const auto members = clan->m_members; // copy members - for (const auto &clanVictim : Clan::ClanList) { // ( ) + for (const auto &clanVictim : Clan::ClanList) { //для всех кланов выставляем нейтралитет (тупо удаляем) if (clan->rent != clanVictim->rent) clanVictim->SetPolitics(clan->rent, kPoliticsNeutral); } @@ -2220,7 +2220,7 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { break; } } - // , + // пуржим ингры, если есть clan->purge_ingr_chest(); clan->exp_history.fulldelete(); clan->last_exp.fulldelete(); @@ -2231,12 +2231,12 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { DescriptorData *d = DescriptorByUid(it.first); if (d) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), " . !\r\n"); + SendMsgToChar(d->character.get(), "Ваша дружина распущена. Желаем удачи!\r\n"); } Player p_vict; CharData *vict = &p_vict; if (LoadPlayerCharacter(it.second->name.c_str(), vict, ELoadCharFlags::kFindId) > -1) { - sprintf(smallbuf, "() '%s'. ()", clan->get_abbrev().c_str()); + sprintf(smallbuf, "Исключен(а) из дружины '%s'. (распущена)", clan->get_abbrev().c_str()); AddKarma(vict, smallbuf, reason); vict->save_char(); } @@ -2249,15 +2249,15 @@ void Clan::DestroyClan(Clan::shared_ptr clan, char *reason) { const char *CLAN_PKLIST_FORMAT[] = { - ": | ()\r\n" - " | ()\r\n", - " | \r\n" - " | |\r\n" + "Формат: пклист|дрлист (все)\r\n" + " пклист|дрлист имя (все)\r\n", + " пклист|дрлист добавить имя причина\r\n" + " пклист|дрлист удалить имя|все\r\n" }; /** -* / - , , -* .. - , . +* Для клановых пкл/дрл - не показываются чары в состоянии дисконета, кроме находящихся в бд, +* т.е. они стоят где-то в мире полюбому, все остальные состояния считаются как онлайн. */ bool check_online_state(long uid) { for (const auto &tch : character_list) { @@ -2272,7 +2272,7 @@ bool check_online_state(long uid) { return false; } -// * / ''. +// * Распечатка пкл/дрл с учетом режима 'пкфортмат'. void print_pkl(CharData *ch, std::ostringstream &stream, ClanPkList::const_iterator &it) { static char timeBuf[11]; @@ -2285,14 +2285,14 @@ void print_pkl(CharData *ch, std::ostringstream &stream, ClanPkList::const_itera } } -// ( ) -// - , -, +// кладем в сундук (при наличии привилегии) +// если предмет - деньги, то автоматом идут в клан-казну, контейнеры только пустые bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { const bool prohibited = ch->IsNpc() || !CLAN(ch) || GetRoomRnum(CLAN(ch)->chest_room) != ch->in_room || !CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHEST_PUT]; if (prohibited) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Не имеете таких правов!\r\n", ch); return false; } @@ -2302,11 +2302,11 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); currencies::AddHand(*ch, currencies::kGold, gold); - SendMsgToChar(ch, " ! %ld %s.\r\n", + SendMsgToChar(ch, "Вам это не положено! Вы вновь обрели %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); return true; } - // : - , + // здесь и далее: в случае переполнения - кладем сколько можем, остальное возвращаем чару if ((CLAN(ch)->bank + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; CLAN(ch)->bank += over; @@ -2316,7 +2316,7 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); SendMsgToChar(ch, - " %ld %s.\r\n", + "Вам удалось вложить в казну дружины только %ld %s.\r\n", over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); return true; @@ -2325,24 +2325,24 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { CLAN(ch)->m_members.add_money(ch->get_uid(), gold); RemoveObjFromChar(obj); ExtractObjFromWorld(obj); - SendMsgToChar(ch, " %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); + SendMsgToChar(ch, "Вы вложили в казну дружины %ld %s.\r\n", gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); } else if (obj->has_flag(EObjFlag::kNodrop) || obj->is_unrentable() || obj->has_flag(EObjFlag::kNamed) || obj->get_owner()) { - act(" $o3 $O3.", false, ch, obj, chest, kToChar); + act("Неведомая сила помешала положить $o3 в $O3.", false, ch, obj, chest, kToChar); } else if (obj->get_type() == EObjType::kContainer && obj->get_contains()) { - act(" $o5 - .", false, ch, obj, nullptr, kToChar); + act("В $o5 что-то лежит.", false, ch, obj, nullptr, kToChar); } else if (SetSystem::is_norent_set(ch, obj, true) && obj->has_flag(EObjFlag::kNotOneInClanChest)) { - snprintf(buf, kMaxStringLength, "%s - .\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); + snprintf(buf, kMaxStringLength, "%s - требуется две и более вещи из набора.\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); SendMsgToChar(utils::CAP(buf), ch); return false; } else { if ((chest->get_weight() + obj->get_weight()) > CLAN(ch)->ChestMaxWeight() || CLAN(ch)->chest_objcount == CLAN(ch)->ChestMaxObjects()) { - act(" $o3 $O3, - .", + act("Вы попытались запихнуть $o3 в $O3, но не смогли - там просто нет места.", false, ch, obj, @@ -2355,11 +2355,11 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { PlaceObjIntoObj(obj, chest); ObjSaveSync::add(ch->get_uid(), CLAN(ch)->GetRent(), ObjSaveSync::CLAN_SAVE); - CLAN(ch)->chest_log.add(fmt::format("{} {} {}{}\r\n", + CLAN(ch)->chest_log.add(fmt::format("{} сдал{} {}{}\r\n", ch->get_name(), grammar::SexEnding((ch)->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch)))); - // + // канал хранилища for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying @@ -2367,9 +2367,9 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { && CLAN(d->character) && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kTakeMode)) { - // : - // (&n), - . - SendMsgToChar(fmt::format("[]: &R'{} {} {}{}&R.'&n\r\n", + // Цвет повторяем после имени предмета: у именных вещей оно само цветное и + // закрывается сбросом (&n), из-за чего хвост фразы терял цвет. + SendMsgToChar(fmt::format("[Хранилище]: &R'{} сдал{} {}{}&R.'&n\r\n", GET_NAME(ch), grammar::SexEnding(ch->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch))), @@ -2378,7 +2378,7 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { } if (!ch->IsFlagged(EPrf::kDecayMode)) { - act(" $o3 $O3.", false, ch, obj, chest, kToChar); + act("Вы положили $o3 в $O3.", false, ch, obj, chest, kToChar); } CLAN(ch)->chest_objcount++; @@ -2388,13 +2388,13 @@ bool Clan::PutChest(CharData *ch, ObjData *obj, ObjData *chest) { return true; } -// - ( ) +// берем из клан-сундука (при наличии привилегии) bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || GetRoomRnum(CLAN(ch)->chest_room) != ch->in_room || !CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHEST_TAKE]) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Не имеете таких правов!\r\n", ch); return false; } @@ -2403,12 +2403,12 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { ObjSaveSync::add(ch->get_uid(), CLAN(ch)->GetRent(), ObjSaveSync::CLAN_SAVE); if (obj->get_carried_by() == ch) { - std::string log_text = fmt::format("{} {} {}{}\r\n", + std::string log_text = fmt::format("{} забрал{} {}{}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(ch))); CLAN(ch)->chest_log.add(log_text); - // + // канал хранилища for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && d->state == EConState::kPlaying @@ -2416,7 +2416,7 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { && CLAN(d->character) && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kTakeMode)) { - SendMsgToChar(fmt::format("[]: &R'{} {} {}{}&R.'&n\r\n", + SendMsgToChar(fmt::format("[Хранилище]: &R'{} забрал{} {}{}&R.'&n\r\n", GET_NAME(ch), grammar::SexEnding(ch->get_sex(), 1), obj->get_PName(grammar::ECase::kAcc), clan_get_custom_label(obj, CLAN(d->character))), @@ -2425,7 +2425,7 @@ bool Clan::TakeChest(CharData *ch, ObjData *obj, ObjData *chest) { } if (!ch->IsFlagged(EPrf::kTakeMode)) { - act(" $o3 $O1.", false, ch, obj, chest, kToChar); + act("Вы взяли $o3 из $O1.", false, ch, obj, chest, kToChar); } CLAN(ch)->chest_objcount--; GlobalObjects::chest_saver().mark_dirty(CLAN(ch).get()); @@ -2437,8 +2437,8 @@ void Clan::save_chest() { log("Save obj: %s", this->abbrev.c_str()); ObjSaveSync::check(this->GetRent(), ObjSaveSync::CLAN_SAVE); - // KOI8-R, : UTF-8 - // AtoL , + // Имя каталога дружины строится из байтов KOI8-R, как и до миграции: под UTF-8 байтовый + // AtoL резал русскую букву пополам, и добро дружины уезжало в каталог с другим именем // (issue #3681). std::string buffer = this->abbrev; CreateFileName(buffer); @@ -2464,22 +2464,22 @@ void Clan::save_chest() { } } -// -// write_one_object ( , -// ) +// сохраняем все сундуки в файлы +// пользует write_one_object (мне кажется это разуменее, чем плодить свои форматы везде и потом +// заниматься с ними сексом при изменении параметров на шмотках) void Clan::SaveChestAll() { GlobalObjects::chest_saver().run(); } -// -// read_one_object_new +// чтение файлов клановых сундуков +// пользует read_one_object_new для чтения шмоток плееров в ренте void Clan::ChestLoad() { ObjData *temp, *obj_next; - // TODO: / chest - // , ( ) + // TODO: при сильном желании тут можно пробегать все зоны замков или вообще все зоны/предметы и пуржить все chest + // предметы и их содержимое, на случай релоада кланов и изменения комнаты с хранилищем (чтобы в маде не пуржить руками) - // - + // на случай релоада - чистим перед этим все что было в сундуках for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { for (auto chest : world[GetRoomRnum((*clan)->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { @@ -2504,7 +2504,7 @@ void Clan::ChestLoad() { CreateFileName(buffer); std::string filename = LIB_CLANS + buffer + "/" + buffer + ".obj"; - // . . + //лоадим сундук. в зонах его лоадить не нужно. const auto chest = world_objects.create_from_prototype_by_vnum(CLAN_CHEST_VNUM); if (chest) { PlaceObjToRoom(chest.get(), GetRoomRnum((*clan)->chest_room)); @@ -2550,7 +2550,7 @@ void Clan::ChestLoad() { } if (!NamedStuff::check_named(nullptr, - obj.get()))// + obj.get()))//Если объект есть в списке именных то ему нечего делать в хранилище { PlaceObjIntoObj(obj.get(), chest.get()); } else { @@ -2561,14 +2561,14 @@ void Clan::ChestLoad() { } } -// +// смотрим чего в сундуках и берем из казны за ренту void Clan::ChestUpdate() { double i, cost; for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { cost = (*clan)->ChestTax() + (*clan)->ingr_chest_tax(); cost += (*clan)->calculate_clan_tax(); - // ( ) + // расчет и снимание за ренту (целой части по возможности) cost = (cost * kChestUpdatePeriod) / (60 * 24); (*clan)->bankBuffer += cost; @@ -2577,8 +2577,8 @@ void Clan::ChestUpdate() { (*clan)->bank -= static_cast(i); (*clan)->bankBuffer -= i; } - // - // TODO: , , + // при нулевом счете все шмотки в сундуке пуржим + // TODO: а тут придется опять искать, ибо выше сундук уже похерен, надо фиксить if ((*clan)->bank < 0) { (*clan)->bank = 0; ObjData *temp, *obj_next; @@ -2592,14 +2592,14 @@ void Clan::ChestUpdate() { break; } } - // , + // пуржим ингры, если есть (*clan)->purge_ingr_chest(); GlobalObjects::chest_saver().mark_dirty(clan->get()); } } } -// * . +// * Запись сообщения дружины в файл и поле клана. void Clan::write_mod(const std::string &arg) { std::string abbrev = this->get_abbrev(); CreateFileName(abbrev); @@ -2616,7 +2616,7 @@ void Clan::write_mod(const std::string &arg) { mod_text = arg; } -// * . +// * Распечатка сообщения дружины чару при входе. bool Clan::print_mod(CharData *ch) const { if (!mod_text.empty()) { SendMsgToChar(ch, "\r\n%s%s%s\r\n", @@ -2627,7 +2627,7 @@ bool Clan::print_mod(CharData *ch) const { return false; } -// * . +// * Загрузка сообщения дружины. void Clan::load_mod() { std::string abbrev = this->get_file_abbrev(); std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".mod"; @@ -2643,8 +2643,8 @@ void Clan::load_mod() { out.str().swap(mod_text); } -// ... '' -// / , , +// казна дружины... команды теже самые с приставкой 'казна' в начале +// смотреть/вкладывать могут все, снимать по привилегии, висит на стандартных банкирах bool Clan::BankManage(CharData *ch, char *arg) { if (ch->IsNpc() || !CLAN(ch) || GetRealLevel(ch) >= kLvlImmortal) return false; @@ -2652,13 +2652,13 @@ bool Clan::BankManage(CharData *ch, char *arg) { std::string buffer = arg, buffer2; GetOneParam(buffer, buffer2); - if (CompareParam(buffer2, "") || CompareParam(buffer2, "balance")) { + if (CompareParam(buffer2, "баланс") || CompareParam(buffer2, "balance")) { SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kClanBalance)), fmt::arg("amount", CLAN(ch)->bank), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(CLAN(ch)->bank, grammar::ECase::kNom).c_str())) + "\r\n", ch); return true; - } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "deposit")) { + } else if (CompareParam(buffer2, "вложить") || CompareParam(buffer2, "deposit")) { GetOneParam(buffer, buffer2); long gold = 0; try { @@ -2675,7 +2675,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { catch (const std::invalid_argument &) { SendMsgToChar(specials::BankMsg(specials::EBankMsg::kClanDepositFormat) + "\r\n", ch); } - // + // на случай переполнения казны if ((CLAN(ch)->bank + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; CLAN(ch)->bank += over; @@ -2685,7 +2685,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { fmt::arg("amount", over), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, nullptr, nullptr, kToRoom); - std::string log_text = fmt::format("{} %s {} {}\r\n", + std::string log_text = fmt::format("{} вложил%s в казну {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2696,14 +2696,14 @@ bool Clan::BankManage(CharData *ch, char *arg) { CLAN(ch)->m_members.add_money(ch->get_uid(), gold); SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kDeposited)), fmt::arg("amount", gold), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, 0, nullptr, kToRoom); - std::string log_text = fmt::format("{} %s {} {}\r\n", + std::string log_text = fmt::format("{} вложил%s в казну {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); return true; - } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "withdraw")) { + } else if (CompareParam(buffer2, "получить") || CompareParam(buffer2, "withdraw")) { if (!CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_BANK]) { SendMsgToChar(specials::BankMsg(specials::EBankMsg::kClanNoWithdraw) + "\r\n", ch); return true; @@ -2720,7 +2720,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { return true; } - // + // на случай переполнения персонажа if ((currencies::GetHand(*ch, currencies::kGold) + gold) < 0) { long over = std::numeric_limits::max() - CLAN(ch)->bank; currencies::AddHand(*ch, currencies::kGold, over); @@ -2730,7 +2730,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { fmt::arg("amount", over), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, 0, nullptr, kToRoom); - std::string log_text = fmt::format("{} %s {} {}\r\n", + std::string log_text = fmt::format("{} получил%s из казны {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), over, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(over, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2741,7 +2741,7 @@ bool Clan::BankManage(CharData *ch, char *arg) { currencies::AddHand(*ch, currencies::kGold, gold); SendMsgToChar(fmt::format(fmt::runtime(specials::BankMsg(specials::EBankMsg::kWithdrawn)), fmt::arg("amount", gold), fmt::arg("currency", MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str())) + "\r\n", ch); act(specials::BankMsg(specials::EBankMsg::kFinancialOp), true, ch, nullptr, nullptr, kToRoom); - std::string log_text = fmt::format("{} %s {} {}\r\n", + std::string log_text = fmt::format("{} получил%s из казны {} {}\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), gold, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold, grammar::ECase::kNom).c_str()); CLAN(ch)->chest_log.add(log_text); @@ -2761,30 +2761,30 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // , - + // есть вариант, что за время в олц в клане изменят кол-во званий if (d->clan_olc->clan->privileges.size() != d->clan_olc->privileges.size()) { SendMsgToChar( - " .\r\n" - " .\r\n" - " .\r\n", + "Во время редактирования привилегий в вашей дружине было изменено количество званий.\r\n" + "Во избежание несоответствий зайдите в меню еще раз.\r\n" + "Редактирование отменено.\r\n", d->character.get()); d->clan_olc.reset(); d->state = EConState::kPlaying; return; } - SendMsgToChar(" ? Y()/N() : ", d->character.get()); + SendMsgToChar("Вы желаете сохранить изменения? Y(Д)/N(Н) : ", d->character.get()); d->clan_olc->mode = CLAN_SAVE_MENU; return; default: if (!*arg || !num) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } - // ( 1 + ) + // тут парсим циферки уже (меню начинается с 1 + ранг ниже себя) unsigned choise = num + CLAN_MEMBER(d->character)->rank_num; if (choise >= d->clan_olc->clan->ranks.size()) { unsigned i = choise - static_cast(d->clan_olc->clan->ranks.size()); @@ -2812,14 +2812,14 @@ void Clan::Manage(DescriptorData *d, const char *arg) { return; } else if (i == 5 && !CLAN_MEMBER(d->character)->rank_num) { if (!ingr_chest_active()) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); } else { d->clan_olc->clan->disable_ingr_chest(d->character.get()); } d->clan_olc->clan->MainMenu(d); return; } else { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } @@ -2827,7 +2827,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { } if (choise >= d->clan_olc->clan->ranks.size()) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->MainMenu(d); return; } @@ -2843,25 +2843,25 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // + // выход в общее меню d->clan_olc->rank = 0; d->clan_olc->mode = CLAN_MAIN_MENU; d->clan_olc->clan->MainMenu(d); return; default: if (!*arg) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->PrivilegeMenu(d, d->clan_olc->rank); return; } if (num > CLAN_PRIVILEGES_NUM || num <= 0) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->PrivilegeMenu(d, d->clan_olc->rank); return; } - // + // мы выдаем только доступные привилегии в меню и нормально парсим их тут unsigned parse_num; for (parse_num = 0; parse_num <= CLAN_PRIVILEGES_NUM; ++parse_num) { if (d->clan_olc->privileges[CLAN_MEMBER(d->character)->rank_num][parse_num]) { @@ -2892,7 +2892,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { d->clan_olc.reset(); // Clan::ClanSave(); d->state = EConState::kPlaying; - SendMsgToChar(" .\r\n", d->character.get()); + SendMsgToChar("Изменения сохранены.\r\n", d->character.get()); return; case 'n': @@ -2900,11 +2900,11 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kEn: case rus::kEnUpper: d->clan_olc.reset(); d->state = EConState::kPlaying; - SendMsgToChar(" .\r\n", d->character.get()); + SendMsgToChar("Редактирование отменено.\r\n", d->character.get()); return; default: - SendMsgToChar(" !\r\n ? Y()/N() : ", + SendMsgToChar("Неверный выбор!\r\nВы желаете сохранить изменения? Y(Д)/N(Н) : ", d->character.get()); d->clan_olc->mode = CLAN_SAVE_MENU; return; @@ -2918,7 +2918,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // + // выход в общее меню с изменением всех званий for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (d->clan_olc->all_ranks[i]) { unsigned j = CLAN_MEMBER(d->character)->rank_num + 1; @@ -2936,13 +2936,13 @@ void Clan::Manage(DescriptorData *d, const char *arg) { default: if (!*arg) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 0); return; } if (num > CLAN_PRIVILEGES_NUM) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 0); return; } @@ -2972,7 +2972,7 @@ void Clan::Manage(DescriptorData *d, const char *arg) { case rus::kVeUpper: case 'q': case 'Q': - // + // выход в общее меню с изменением всех званий for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { if (d->clan_olc->all_ranks[i]) { unsigned j = CLAN_MEMBER(d->character)->rank_num + 1; @@ -2991,13 +2991,13 @@ void Clan::Manage(DescriptorData *d, const char *arg) { default: if (!*arg) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 1); return; } if (num > CLAN_PRIVILEGES_NUM) { - SendMsgToChar(" !\r\n", d->character.get()); + SendMsgToChar("Неверный выбор!\r\n", d->character.get()); d->clan_olc->clan->AllMenu(d, 1); return; } @@ -3027,9 +3027,9 @@ void Clan::Manage(DescriptorData *d, const char *arg) { void Clan::MainMenu(DescriptorData *d) { std::ostringstream buffer; - buffer << " .\r\n" - << " :\r\n"; - // + buffer << "Раздел добавления и удаления привилегий.\r\n" + << "Выберите редактируемое звание или действие:\r\n"; + // звания ниже своего int rank = CLAN_MEMBER(d->character)->rank_num + 1; int num = 0; for (std::vector::const_iterator it = d->clan_olc->clan->ranks.begin() + rank; @@ -3037,46 +3037,46 @@ void Clan::MainMenu(DescriptorData *d) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") " << (*it) << "\r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << " \r\n"; + << ") " << "добавить всем\r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << " \r\n"; + << ") " << "убрать у всех\r\n"; if (!CLAN_MEMBER(d->character)->rank_num) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << " \r\n" - << " + (1000 ) "; + << ") " << "Выборка из хранилища по параметрам предметов\r\n" + << " + просмотр хранилища вне замка в качестве бонуса (1000 кун в день) "; if (this->storehouse) - buffer << "()\r\n"; + buffer << "(отключить)\r\n"; else - buffer << "()\r\n"; + buffer << "(включить)\r\n"; buffer << kColorGrn << std::setw(2) << ++num << kColorNrm - << ") " << " ( 6 ) "; + << ") " << "Моментально обновлять очки дружины в топе (при отключении обновление раз в 6 часов) "; if (this->exp_info) - buffer << "()\r\n"; + buffer << "(отключить)\r\n"; else - buffer << "()\r\n"; + buffer << "(включить)\r\n"; - // (/) + // хранилище ингров (включить/переместить) buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") "; if (ingr_chest_active()) { - buffer << " \r\n"; + buffer << "Переместить в данную комнату хранилище для ингредиентов\r\n"; } else { - buffer << " (1000 /)\r\n"; + buffer << "Установить в данной комнате хранилище для ингредиентов (1000 кун/день)\r\n"; } - // () + // хранилище ингров (выключить) if (ingr_chest_active()) { buffer << kColorGrn << std::setw(2) << ++num << kColorNrm << ") " - << " " << "\r\n"; + << "Отключить хранилище для ингредиентов" << "\r\n"; } else { buffer << kColorBoldBlk << std::setw(2) << ++num - << ") " << " " + << ") " << "Отключить хранилище для ингредиентов" << kColorNrm << "\r\n"; } } - buffer << kColorGrn << " (Q)" << kColorNrm - << ") \r\n" << " :"; + buffer << kColorGrn << " В(Q)" << kColorNrm + << ") Выход\r\n" << "Ваш выбор:"; SendMsgToChar(buffer.str(), d->character.get()); d->clan_olc->mode = CLAN_MAIN_MENU; @@ -3088,12 +3088,12 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { if (d->character) { d->clan_olc.reset(); d->state = EConState::kPlaying; - SendMsgToChar(d->character.get(), " - , !"); + SendMsgToChar(d->character.get(), "Случилось что-то страшное, сообщите Богам!"); } return; } std::ostringstream buffer; - buffer << " '" << kColorBoldRed + buffer << "Список привилегий для звания '" << kColorBoldRed << d->clan_olc->clan->ranks[num] << kColorNrm << "':\r\n"; int count = 0; @@ -3104,9 +3104,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_INFO]) - buffer << "[x] ( )\r\n"; + buffer << "[x] информация о дружине (клан информация)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] информация о дружине (клан информация)\r\n"; } break; case MAY_CLAN_ADD: @@ -3114,9 +3114,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_ADD]) - buffer << "[x] ( )\r\n"; + buffer << "[x] принятие в дружину (клан принять)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] принятие в дружину (клан принять)\r\n"; } break; case MAY_CLAN_REMOVE: @@ -3124,9 +3124,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_REMOVE]) - buffer << "[x] ( )\r\n"; + buffer << "[x] изгнание из дружины (клан изгнать)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] изгнание из дружины (клан изгнать)\r\n"; } break; case MAY_CLAN_PRIVILEGES: @@ -3134,9 +3134,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_PRIVILEGES]) - buffer << "[x] ( )\r\n"; + buffer << "[x] редактирование привилегий (клан привилегии)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] редактирование привилегий (клан привилегии)\r\n"; } break; case MAY_CLAN_CHANNEL: @@ -3144,9 +3144,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHANNEL]) - buffer << "[x] - ()\r\n"; + buffer << "[x] клан-канал (гдругам)\r\n"; else - buffer << "[ ] - ()\r\n"; + buffer << "[ ] клан-канал (гдругам)\r\n"; } break; case MAY_CLAN_POLITICS: @@ -3154,9 +3154,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_POLITICS]) - buffer << "[x] ()\r\n"; + buffer << "[x] изменение политики дружины (политика)\r\n"; else - buffer << "[ ] ()\r\n"; + buffer << "[ ] изменение политики дружины (политика)\r\n"; } break; case MAY_CLAN_NEWS: @@ -3164,9 +3164,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_NEWS]) - buffer << "[x] ()\r\n"; + buffer << "[x] добавление новостей дружины (дрновости)\r\n"; else - buffer << "[ ] ()\r\n"; + buffer << "[ ] добавление новостей дружины (дрновости)\r\n"; } break; case MAY_CLAN_PKLIST: @@ -3174,9 +3174,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_PKLIST]) - buffer << "[x] - ()\r\n"; + buffer << "[x] добавление в пк-лист (пклист)\r\n"; else - buffer << "[ ] - ()\r\n"; + buffer << "[ ] добавление в пк-лист (пклист)\r\n"; } break; case MAY_CLAN_CHEST_PUT: @@ -3184,9 +3184,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHEST_PUT]) - buffer << "[x] \r\n"; + buffer << "[x] класть вещи в хранилище\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] класть вещи в хранилище\r\n"; } break; case MAY_CLAN_CHEST_TAKE: @@ -3194,9 +3194,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_CHEST_TAKE]) - buffer << "[x] \r\n"; + buffer << "[x] брать вещи из хранилища\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] брать вещи из хранилища\r\n"; } break; case MAY_CLAN_BANK: @@ -3204,9 +3204,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_BANK]) - buffer << "[x] \r\n"; + buffer << "[x] брать из казны дружины\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] брать из казны дружины\r\n"; } break; case MAY_CLAN_EXIT: @@ -3214,9 +3214,9 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->privileges[num][MAY_CLAN_EXIT]) - buffer << "[x] \r\n"; + buffer << "[x] свободный выход из дружины\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] свободный выход из дружины\r\n"; } break; case MAY_CLAN_MOD: @@ -3224,7 +3224,7 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_MOD] ? "[x]" : "[ ]") - << " \r\n"; + << " написание сообщения дружины\r\n"; } break; case MAY_CLAN_TAX: @@ -3232,7 +3232,7 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_TAX] ? "[x]" : "[ ]") - << " \r\n"; + << " установка налога для ратников\r\n"; } break; case MAY_CLAN_BOARD: @@ -3240,27 +3240,27 @@ void Clan::PrivilegeMenu(DescriptorData *d, unsigned num) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->privileges[num][MAY_CLAN_BOARD] ? "[x]" : "[ ]") - << " \r\n"; + << " сообщения в дрвече\r\n"; } break; } // case } - buffer << kColorGrn << " (Q)" << kColorNrm - << ") \r\n" << " :"; + buffer << kColorGrn << " В(Q)" << kColorNrm + << ") Выход\r\n" << "Ваш выбор:"; SendMsgToChar(buffer.str(), d->character.get()); d->clan_olc->mode = CLAN_PRIVILEGE_MENU; } -// / , -// flag 0 - , 1 - +// меню добавления/удаления привилегий у всех званий, решил не нагружать все в одну функцию +// flag 0 - добавление, 1 - удаление void Clan::AllMenu(DescriptorData *d, unsigned flag) { std::ostringstream buffer; if (flag == 0) - buffer << " , " << kColorBoldRed - << " " << kColorNrm << " :\r\n"; + buffer << "Выберите привилегии, которые вы хотите " << kColorBoldRed + << "добавить всем" << kColorNrm << " званиям:\r\n"; else - buffer << " , " << kColorBoldRed - << " " << kColorNrm << " :\r\n"; + buffer << "Выберите привилегии, которые вы хотите " << kColorBoldRed + << "убрать у всех" << kColorNrm << " званий:\r\n"; int count = 0; for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) { @@ -3270,9 +3270,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_INFO]) - buffer << "[x] ( )\r\n"; + buffer << "[x] информация о дружине (клан информация)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] информация о дружине (клан информация)\r\n"; } break; case MAY_CLAN_ADD: @@ -3280,9 +3280,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_ADD]) - buffer << "[x] ( )\r\n"; + buffer << "[x] принятие в дружину (клан принять)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] принятие в дружину (клан принять)\r\n"; } break; case MAY_CLAN_REMOVE: @@ -3290,9 +3290,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_REMOVE]) - buffer << "[x] ( )\r\n"; + buffer << "[x] изгнание из дружины (клан изгнать)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] изгнание из дружины (клан изгнать)\r\n"; } break; case MAY_CLAN_PRIVILEGES: @@ -3300,9 +3300,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_PRIVILEGES]) - buffer << "[x] ( )\r\n"; + buffer << "[x] редактирование привилегий (клан привилегии)\r\n"; else - buffer << "[ ] ( )\r\n"; + buffer << "[ ] редактирование привилегий (клан привилегии)\r\n"; } break; case MAY_CLAN_CHANNEL: @@ -3310,9 +3310,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHANNEL]) - buffer << "[x] - ()\r\n"; + buffer << "[x] клан-канал (гдругам)\r\n"; else - buffer << "[ ] - ()\r\n"; + buffer << "[ ] клан-канал (гдругам)\r\n"; } break; case MAY_CLAN_POLITICS: @@ -3320,9 +3320,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_POLITICS]) - buffer << "[x] ()\r\n"; + buffer << "[x] изменение политики дружины (политика)\r\n"; else - buffer << "[ ] ()\r\n"; + buffer << "[ ] изменение политики дружины (политика)\r\n"; } break; case MAY_CLAN_NEWS: @@ -3330,9 +3330,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_NEWS]) - buffer << "[x] ()\r\n"; + buffer << "[x] добавление новостей дружины (дрновости)\r\n"; else - buffer << "[ ] ()\r\n"; + buffer << "[ ] добавление новостей дружины (дрновости)\r\n"; } break; case MAY_CLAN_PKLIST: @@ -3340,9 +3340,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_PKLIST]) - buffer << "[x] - ()\r\n"; + buffer << "[x] добавление в пк-лист (пклист)\r\n"; else - buffer << "[ ] - ()\r\n"; + buffer << "[ ] добавление в пк-лист (пклист)\r\n"; } break; case MAY_CLAN_CHEST_PUT: @@ -3350,9 +3350,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHEST_PUT]) - buffer << "[x] \r\n"; + buffer << "[x] класть вещи в хранилище\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] класть вещи в хранилище\r\n"; } break; case MAY_CLAN_CHEST_TAKE: @@ -3360,9 +3360,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_CHEST_TAKE]) - buffer << "[x] \r\n"; + buffer << "[x] брать вещи из хранилища\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] брать вещи из хранилища\r\n"; } break; case MAY_CLAN_BANK: @@ -3370,9 +3370,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_BANK]) - buffer << "[x] \r\n"; + buffer << "[x] брать из казны дружины\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] брать из казны дружины\r\n"; } break; case MAY_CLAN_EXIT: @@ -3380,9 +3380,9 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") "; if (d->clan_olc->all_ranks[MAY_CLAN_EXIT]) - buffer << "[x] \r\n"; + buffer << "[x] свободный выход из дружины\r\n"; else - buffer << "[ ] \r\n"; + buffer << "[ ] свободный выход из дружины\r\n"; } break; case MAY_CLAN_MOD: @@ -3390,7 +3390,7 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_MOD] ? "[x]" : "[ ]") - << " \r\n"; + << " написание сообщения дружины\r\n"; } break; case MAY_CLAN_TAX: @@ -3398,7 +3398,7 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_TAX] ? "[x]" : "[ ]") - << " \r\n"; + << " установка налога для ратников\r\n"; } break; case MAY_CLAN_BOARD: @@ -3406,13 +3406,13 @@ void Clan::AllMenu(DescriptorData *d, unsigned flag) { buffer << kColorGrn << std::setw(2) << ++count << kColorNrm << ") " << (d->clan_olc->all_ranks[MAY_CLAN_BOARD] ? "[x]" : "[ ]") - << " \r\n"; + << " сообщения в дрвече\r\n"; } break; } // case } - buffer << kColorGrn << " (Q)" << kColorNrm - << ") \r\n" << " :"; + buffer << kColorGrn << " В(Q)" << kColorNrm + << ") Применить\r\n" << "Ваш выбор:"; SendMsgToChar(buffer.str(), d->character.get()); if (flag == 0) d->clan_olc->mode = CLAN_ADDALL_MENU; @@ -3427,7 +3427,7 @@ void Clan::add_offline_member(const std::string &name, int uid, int rank) { this->m_members.set(uid, tmp_member); } -// +// игрок ранг void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { if (ch->get_name_str().empty()) { log("SYSERROR: zero player name (uid = %ld) (%s:%d %s)", ch->get_uid(), @@ -3446,7 +3446,7 @@ void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { && this->GetRent() == CLAN(d->character)->GetRent() && ch != d->character.get()) { SendMsgToChar(d->character.get(), - "%s%s %s , - '%s'.%s\r\n", + "%s%s приписан%s к вашей дружине, статус - '%s'.%s\r\n", kColorWht, GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 6), @@ -3455,14 +3455,14 @@ void Clan::ClanAddMember(CharData *ch, int rank, std::string invite_name) { } } - SendMsgToChar(ch, "%s '%s', - '%s'.%s\r\n", + SendMsgToChar(ch, "%sВас приписали к дружине '%s', статус - '%s'.%s\r\n", kColorWht, this->name.c_str(), (this->ranks[rank]).c_str(), kColorNrm); - sprintf(buf, " '%s', - '%s'", this->name.c_str(), (this->ranks[rank]).c_str()); + sprintf(buf, "Принят в дружину '%s', статус - '%s'", this->name.c_str(), (this->ranks[rank]).c_str()); AddKarma(ch, buf, invite_name.c_str()); return; } -// +// передача воеводства void Clan::HouseOwner(CharData *ch, std::string &buffer) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -3470,21 +3470,21 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { DescriptorData *d = DescriptorByUid(unique); if (buffer2.empty()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Укажите имя персонажа.\r\n", ch); else if (!unique) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестный персонаж.\r\n", ch); else if (unique == ch->get_uid()) - SendMsgToChar(" ? .\r\n", ch); + SendMsgToChar("Сменить себя на самого себя? Вы бредите.\r\n", ch); else if (!d || !sight::CanSee(ch, d->character)) - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Этого персонажа нет в игре!\r\n", ch); else if (CLAN(d->character) && CLAN(ch) != CLAN(d->character)) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не можете передать свои права члену другой дружины.\r\n", ch); else { native_text::capitalize_first(buffer2); - // + // воевода идет рангом ниже this->m_members.set_rank(ch->get_uid(), 1); Clan::SetClanData(ch); - // ( - ) + // ставим нового воеводу (если он был в клане - меняем только ранг) if (CLAN(d->character)) { this->m_members.set_rank(d->character->get_uid(), 0); Clan::SetClanData(d->character.get()); @@ -3492,16 +3492,16 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { this->ClanAddMember(d->character.get(), 0, ch->get_name()); } this->owner = buffer2; - SendMsgToChar(ch, ", %s!\r\n", GET_PAD(d->character, 2)); + SendMsgToChar(ch, "Поздравляем, вы передали свои полномочия %s!\r\n", GET_PAD(d->character, 2)); if (IsMale(ch)) sprintf(buf, - "&R!!!&n %s %s %s.\r\n", + "&RВнимание!!!&n %s ушел на пенсию и добровольно передал руководство дружины %s игроку %s.\r\n", GET_NAME(ch), CLAN(d->character)->GetAbbrev(), GET_PAD(d->character, 2)); else sprintf(buf, - "&R!!!&n %s %s %s.\r\n", + "&RВнимание!!!&n %s ушла на пенсию и добровольно передала руководство дружины %s игроку %s.\r\n", GET_NAME(ch), CLAN(d->character)->GetAbbrev(), GET_PAD(d->character, 2)); @@ -3510,9 +3510,9 @@ void Clan::HouseOwner(CharData *ch, std::string &buffer) { } /** -* hcontrol owner vnum - -* , . -* . +* hcontrol owner vnum имя - передача воеводы клана внум игроку имя +* Новый воевода не может быть мембером другого клана, старый исключается из клана. +* Оба могут быть оффлайн во время выполнения команды иммом. */ void Clan::hcon_owner(CharData *ch, std::string &text) { std::string buffer; @@ -3526,7 +3526,7 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { }); if (clan == Clan::ClanList.end()) { - SendMsgToChar(ch, " %d .\r\n", vnum); + SendMsgToChar(ch, "Дружины с номером %d не существует.\r\n", vnum); return; } @@ -3535,7 +3535,7 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { long member_uid = GetUniqueByName(name); if (!member_uid) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестный персонаж.\r\n", ch); return; } @@ -3545,48 +3545,48 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { const auto it = tmp_clan->m_members.find(member_uid); if (it != tmp_clan->m_members.end()) { if (vnum != tmp_clan->GetRent()) { - SendMsgToChar(ch, "%s .\r\n", name.c_str()); + SendMsgToChar(ch, "%s состоит в другой дружине.\r\n", name.c_str()); return; } else if (!it->second->rank_num) { - SendMsgToChar(ch, "%s .\r\n", name.c_str()); + SendMsgToChar(ch, "%s и так является воеводой этой дружины.\r\n", name.c_str()); return; } } } - // + // убираем старого воеводу из клана for (const auto &it : (*clan)->m_members) { if (!it.second->rank_num) { const auto member_uid = it.first; - // , , + // ахтунг, удаляется элемент дерева, по которому мы и идем в цикле char tmpstr[kMaxInputLength]; - sprintf(tmpstr, " "); + sprintf(tmpstr, "смена воеводы"); (*clan)->remove_member(member_uid, tmpstr); break; } } - // + // вписываем нового if ((*clan)->m_members.find(member_uid) != (*clan)->m_members.end()) { - // + // уже был в клане (*clan)->m_members.set_rank(member_uid, 0); } else { (*clan)->add_offline_member(name, member_uid, 0); } - // + // новый воевода онлайн DescriptorData *d = DescriptorByUid(member_uid); if (d && d->character) { Clan::SetClanData(d->character.get()); - SendMsgToChar(d->character.get(), "%s %s. !%s\r\n", + SendMsgToChar(d->character.get(), "%sВы стали новым воеводой дружины %s. Желаем удачи!%s\r\n", kColorBoldGrn, (*clan)->get_abbrev().c_str(), kColorNrm); } if ((*clan)->m_members.size() > 0) { - // + // оповещение for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->character && CLAN(d->character) && CLAN(d->character)->GetRent() == (*clan)->GetRent()) { SendMsgToChar(d->character.get(), - "%s : %s -> %s.%s\r\n", + "%sОсуществлена принудительная смена воеводы вашей дружины: %s -> %s.%s\r\n", kColorBoldGrn, (*clan)->owner.c_str(), name.c_str(), @@ -3594,11 +3594,11 @@ void Clan::hcon_owner(CharData *ch, std::string &text) { } } } else { - (*clan)->builtOn = time(0); // + (*clan)->builtOn = time(0); //не ставит текущую дату } (*clan)->owner = name; Clan::ClanSave(); - SendMsgToChar(".\r\n", ch); + SendMsgToChar("Сделано.\r\n", ch); } void Clan::CheckPkList(CharData *ch) { @@ -3606,16 +3606,16 @@ void Clan::CheckPkList(CharData *ch) { return; ClanPkList::iterator it; for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { - // + // пкл if ((it = (*clan)->pkList.find(ch->get_uid())) != (*clan)->pkList.end()) SendMsgToChar(ch, - " '%s', : %s.\r\n", + "Находитесь в списке врагов дружины '%s', добавивший: %s.\r\n", (*clan)->name.c_str(), it->second->authorName.c_str()); - // + // дрл if ((it = (*clan)->frList.find(ch->get_uid())) != (*clan)->frList.end()) SendMsgToChar(ch, - " '%s', : %s.\r\n", + "Находитесь в списке друзей дружины '%s', добавивший: %s.\r\n", (*clan)->name.c_str(), it->second->authorName.c_str()); } @@ -3626,7 +3626,7 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { GetOneParam(buffer, buffer2); bool all{false}, name{false}; - // + // параметр сортировки enum ESortParam { SORT_STAT_BY_EXP, SORT_STAT_BY_CLANEXP, @@ -3637,39 +3637,39 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { ESortParam sortParameter; long long lSortParam; - // .. 8- - const char *pSortAlph = "֣"; - // + // т.к. в кои8-р русские буквы не попорядку + const char *pSortAlph = "яюэьыъщшчцхфутсрпонмлкизжёедгвба"; + // первая буква имени - // "!" - // : - // [!/!/!/!] [/] + // для избежания путаницы с именами фильтр начинается со знака "!" + // формат команды: + // клан стат [!опыт/!заработанным/!последнему/!имя] [имя/все] sortParameter = SORT_STAT_BY_EXP; if (buffer2.length() > 1) { - if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) // + if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "опыту"))) // опыту дружине sortParameter = SORT_STAT_BY_CLANEXP; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "заработанным"))) sortParameter = SORT_STAT_BY_MONEY; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "последнему"))) sortParameter = SORT_STAT_BY_LOGON; - else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, ""))) + else if ((buffer2[0] == '!') && (utils::IsAbbr(buffer2.c_str() + 1, "имя"))) sortParameter = SORT_STAT_BY_NAME; - // + // берем следующий параметр if (buffer2[0] == '!') GetOneParam(buffer, buffer2); } std::ostringstream out; out << kColorWht; - if (CompareParam(buffer2, "") || CompareParam(buffer2, "")) { + if (CompareParam(buffer2, "очистить") || CompareParam(buffer2, "удалить")) { if (CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас нет прав удалять статистику.\r\n", ch); return; } - // + // можно почистить тока деньги для удобства сбора с мемберов налога GetOneParam(buffer, buffer2); bool money = false; - if (CompareParam(buffer2, "") || CompareParam(buffer2, "money")) + if (CompareParam(buffer2, "деньги") || CompareParam(buffer2, "money")) money = true; for (const auto &it : m_members) { @@ -3682,41 +3682,41 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } if (money) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Статистика доходов вашей дружины очищена.\r\n", ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Статистика вашей дружины полностью очищена.\r\n", ch); } return; - } else if (CompareParam(buffer2, "")) { + } else if (CompareParam(buffer2, "все")) { all = true; - out << " "; + out << "Статистика вашей дружины "; } else if (!buffer2.empty()) { name = true; - out << " ( '" << buffer2 << "') "; + out << "Статистика вашей дружины (поиск по имени '" << buffer2 << "') "; } else { - out << " ( ) "; + out << "Статистика вашей дружины (находящиеся онлайн) "; } - // - out << "( : "; + // вывод режима сортировки + out << "(сортировка по: "; switch (sortParameter) { - case SORT_STAT_BY_EXP: out << " "; + case SORT_STAT_BY_EXP: out << "рейтинговым очкам"; break; - case SORT_STAT_BY_CLANEXP: out << " "; + case SORT_STAT_BY_CLANEXP: out << "опыту дружине"; break; - case SORT_STAT_BY_MONEY: out << " "; + case SORT_STAT_BY_MONEY: out << "заработанным кунам"; break; - case SORT_STAT_BY_LOGON: out << " "; + case SORT_STAT_BY_LOGON: out << "последнему заходу в игру"; break; - case SORT_STAT_BY_NAME: out << " "; + case SORT_STAT_BY_NAME: out << "первой букве имени"; break; - // - default: out << " "; + // этого быть не должно + default: out << "чему БОГ пошлет"; } out << "):" << "\r\n"; - // multimap + // multimap ибо могут быть совпадения std::multimap > temp_list; for (const auto &it : m_members) { it.second->level = 0; @@ -3740,7 +3740,7 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { if (tmp_time <= 0) tmp_time = time(nullptr); strftime(timeBuf, sizeof(timeBuf), "%d-%m-%Y", localtime(&tmp_time)); - // ... + // сортировка по... switch (sortParameter) { case SORT_STAT_BY_EXP: lSortParam = it.second->exp; break; @@ -3751,9 +3751,9 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { case SORT_STAT_BY_LOGON: lSortParam = GetLastlogonByUnique(it.first); break; case SORT_STAT_BY_NAME: { - // . UTF-8 , - // strpbrk , - // (issue #3681). ģ . + // Индекс первой БУКВЫ имени в алфавите. Под UTF-8 буква занимает два байта, и + // strpbrk по одному байту находил бы что попало, а то и вовсе ничего + // (issue #3681). Идём по алфавиту символ за символом. char first_letter[8] = {0}; native_text::copy_lower_char(it.second->name.c_str(), first_letter); lSortParam = -1; @@ -3765,12 +3765,12 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } } if (lSortParam < 0) { - // -- , + // не русская буква -- пусть уедет в конец, но в стабильном порядке lSortParam = static_cast(native_text::first_char_code(first_letter)); } break; } - // + // на всякий случай default: lSortParam = it.second->exp; } @@ -3778,8 +3778,8 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { } table_wrapper::Table table; - table << table_wrapper::kHeader << "" << "" << "" << "" << ". " - << " " << " " << " " << table_wrapper::kEndRow; + table << table_wrapper::kHeader << "Имя" << "Ур" << "Прв" << "Класс" << "Рейт. очков" + << "Опыта дружины" << "Внесено кун" << "Был в игре" << table_wrapper::kEndRow; std::string lvl_str; for (auto it = temp_list.rbegin(); it != temp_list.rend(); ++it) { table << it->second.second->name; @@ -3803,17 +3803,17 @@ void Clan::HouseStat(CharData *ch, std::string &buffer) { page_string(ch->desc, out.str()); } -// +// напоминалка про кончину денег в казне дружины void Clan::ChestInvoice() { for (ClanListType::const_iterator clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) { int cost = (*clan)->ChestTax() + (*clan)->ingr_chest_tax(); cost += (*clan)->calculate_clan_tax(); if (!cost) { - continue; // + continue; // чем черт не шутит } - // + // опаньки if ((*clan)->bank >= cost) { continue; } @@ -3824,7 +3824,7 @@ void Clan::ChestInvoice() { && CLAN(d->character) && CLAN(d->character) == *clan) { SendMsgToChar(d->character.get(), - "[]: %s', , !'%s\r\n", + "[Хранилище]: %s'Напоминаю, что средств в казне дружины хватит менее, чем на сутки!'%s\r\n", kColorBoldRed, kColorNrm); } @@ -3838,7 +3838,7 @@ int Clan::ChestTax() { int count = 0; for (auto chest : world[GetRoomRnum(this->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { - // + // перебираем шмот for (temp = chest->get_contains(); temp; temp = temp->get_next_content()) { cost += temp->get_rent_on(); ++count; @@ -3853,12 +3853,12 @@ int Clan::ChestTax() { } /** -* -. -* - . -* \todo . . -* \param obj - -* \param ch - -* \return 0 - -, 1 - +* Вместо спешиала теперь просто перехватываем осмотр контейнеров на случай клан-сундука. +* Смотреть могут ес-сно только соклановцы. +* \todo Вынести из класса. Да и вообще там чистить давно пора. +* \param obj - контейнер +* \param ch - смотрящий +* \return 0 - это не клан-сундук, 1 - это был он */ bool Clan::ChestShow(ObjData *obj, CharData *ch) { if (!ch->desc || !Clan::is_clan_chest(obj)) { @@ -3866,29 +3866,29 @@ bool Clan::ChestShow(ObjData *obj, CharData *ch) { } if (CLAN(ch) && GetRoomRnum(CLAN(ch)->chest_room) == obj->get_in_room()) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Хранилище вашей дружины:\r\n", ch); int cost = CLAN(ch)->ChestTax(); SendMsgToChar(ch, - " : %d, : %d %s\r\n\r\n", + "Всего вещей: %d, Рента в день: %d %s\r\n\r\n", CLAN(ch)->chest_objcount, cost, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str()); sight::list_obj_to_char(obj->get_contains(), ch, 1, 3); } else { - SendMsgToChar(" , , .\r\n", - ch); // , + SendMsgToChar("Не на что тут глазеть, пусто, вот те крест.\r\n", + ch); // засланым казачкам показываем хер, а не хранилище } return true; } -// +/- - +// +/- клан-экспы void Clan::SetClanExp(CharData *ch, int add) { - // + // шоб не читили if (GetRealLevel(ch) >= kLvlImmortal) { return; } - // + // обнулять не надо минуса CLAN_MEMBER(ch)->clan_exp += add; this->clan_exp += add; @@ -3905,7 +3905,7 @@ void Clan::SetClanExp(CharData *ch, int add) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->rent) { SendMsgToChar(d->character.get(), - "&G , %d ! !&n\r\n", + "&GВаш замок достиг нового, %d уровня! Поздравляем!&n\r\n", this->clan_level); } } @@ -3918,7 +3918,7 @@ void Clan::SetClanExp(CharData *ch, int add) { && CLAN(d->character) && CLAN(d->character)->GetRent() == this->rent) { SendMsgToChar(d->character.get(), - "%s ! %d ! !%s\r\n", + "%sВаш замок потерял уровень! Теперь он %d уровня! Поздравляем!%s\r\n", kColorBoldRed, this->clan_level, kColorNrm); } @@ -3926,9 +3926,9 @@ void Clan::SetClanExp(CharData *ch, int add) { } } -// +// добавление экспы для топа кланов и мемберу в зачетку void Clan::AddTopExp(CharData *ch, int add_exp) { - // + // шоб не читили if (GetRealLevel(ch) >= kLvlImmortal) return; @@ -3936,16 +3936,16 @@ void Clan::AddTopExp(CharData *ch, int add_exp) { if (CLAN_MEMBER(ch)->exp < 0) CLAN_MEMBER(ch)->exp = 0; - // + // в буффер или сразу в топ if (this->exp_info) { this->exp += add_exp; if (this->exp < 0) this->exp = 0; } else - this->exp_buf += add_exp; // + this->exp_buf += add_exp; // тут обнулять не надо } -// , - +// синхронизация клановой экспы для топ с буффером, если есть режим запрета показа в ран-тайме void Clan::SyncTopExp() { for (auto clan = Clan::ClanList.begin(); clan != Clan::ClanList.end(); ++clan) if (!(*clan)->exp_info) { @@ -3956,56 +3956,56 @@ void Clan::SyncTopExp() { } } -// +// установка режима оповещения об изменениях в хранилище void SetChestMode(CharData *ch, std::string &buffer) { if (ch->IsNpc()) return; if (!CLAN(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Для начала обзаведитесь дружиной.\r\n", ch); return; } - if (CompareParam(buffer, "")) { + if (CompareParam(buffer, "нет")) { ch->UnsetFlag(EPrf::kDecayMode); ch->UnsetFlag(EPrf::kTakeMode); - SendMsgToChar(".\r\n", ch); - } else if (CompareParam(buffer, "")) { + SendMsgToChar("Ладушки.\r\n", ch); + } else if (CompareParam(buffer, "рассыпание")) { ch->SetFlag(EPrf::kDecayMode); ch->UnsetFlag(EPrf::kTakeMode); - SendMsgToChar(".\r\n", ch); - } else if (CompareParam(buffer, "")) { + SendMsgToChar("Ладушки.\r\n", ch); + } else if (CompareParam(buffer, "изменение")) { ch->UnsetFlag(EPrf::kDecayMode); ch->SetFlag(EPrf::kTakeMode); - SendMsgToChar(".\r\n", ch); - } else if (CompareParam(buffer, "")) { + SendMsgToChar("Ладушки.\r\n", ch); + } else if (CompareParam(buffer, "полный")) { ch->SetFlag(EPrf::kDecayMode); ch->SetFlag(EPrf::kTakeMode); - SendMsgToChar(".\r\n", ch); + SendMsgToChar("Ладушки.\r\n", ch); } else { - SendMsgToChar(" .\r\n" - " : <|||>\r\n" - " - \r\n" - " - \r\n" - " - / \r\n" - " - \r\n", ch); + SendMsgToChar("Задается режим оповещения об изменениях в хранилище дружины.\r\n" + "Формат команды: режим хранилище <нет|рассыпание|изменение|полный>\r\n" + " нет - выключение канала хранилища\r\n" + " рассыпание - получать только сообщения о рассыпании вещей\r\n" + " изменение - получать только сообщения о взятии/добавлении вещей\r\n" + " полный - получать оба вида сообщений\r\n", ch); return; } } -// , +// шоб не засорять в режиме, а выдать строку сразу std::string GetChestMode(CharData *ch) { if (ch->IsFlagged(EPrf::kDecayMode)) { if (ch->IsFlagged(EPrf::kTakeMode)) - return ""; + return "полный"; else - return ""; + return "рассыпание"; } else if (ch->IsFlagged(EPrf::kTakeMode)) - return ""; + return "изменение"; else - return ""; + return "выкл"; } -// ? +// неужто реально глючит? int Clan::GetRent() { return this->rent; } @@ -4014,7 +4014,7 @@ int Clan::GetOutRent() { return this->out_rent; } -// * , , +// * Удаление чара из клана, клан берется не через поля чара, а ищем по всем кланам void Clan::remove_from_clan(long unique) { for (const auto &clan : Clan::ClanList) { const auto it = clan->m_members.find(unique); @@ -4073,8 +4073,8 @@ bool ClanSystem::is_ingr_chest(ObjData *obj) { } /** -* / . -* \param enter 1 - , 0 - . +* Оповещение соклановцев о входе/выходе друг друга. +* \param enter 1 - вход чара, 0 - выход. */ void Clan::clan_invoice(CharData *ch, bool enter) { if (ch->IsNpc() || !CLAN(ch)) { @@ -4088,19 +4088,19 @@ void Clan::clan_invoice(CharData *ch, bool enter) { && CLAN(d->character) == CLAN(ch) && d->character->IsFlagged(EPrf::kClanmembersMode)) { if (enter) { - SendMsgToChar(d->character.get(), "%s%s %s %s .%s\r\n", - kColorBoldBlk, IsMale(ch) ? "" : "", GET_NAME(ch), + SendMsgToChar(d->character.get(), "%sДружинни%s %s вош%s в мир.%s\r\n", + kColorBoldBlk, IsMale(ch) ? "к" : "ца", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 5), kColorNrm); } else { - SendMsgToChar(d->character.get(), "%s%s %s %s .%s\r\n", - kColorBoldBlk, IsMale(ch) ? "" : "", GET_NAME(ch), + SendMsgToChar(d->character.get(), "%sДружинни%s %s покинул%s мир.%s\r\n", + kColorBoldBlk, IsMale(ch) ? "к" : "ца", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), kColorNrm); } } } } -// +// иммское где для кланхранилищ std::string Clan::print_imm_where_obj(const ObjData *obj) { std::string str; for (Clan::shared_ptr ptr_clan : Clan::ClanList) { @@ -4108,7 +4108,7 @@ std::string Clan::print_imm_where_obj(const ObjData *obj) { if (Clan::is_clan_chest(chest)) { for (ObjData *chest_content = chest->get_contains(); chest_content; chest_content = chest_content->get_next_content()) { if (obj->get_id() == chest_content->get_id()) { - str = fmt::format("{} '{}'.\r\n", + str = fmt::format("наход{}ся в хранилище дружины '{}'.\r\n", grammar::ObjPluralVerbEnding(((chest_content))->get_sex()), ptr_clan->GetAbbrev()); return str; @@ -4139,13 +4139,13 @@ int Clan::print_spell_locate_object(CharData *ch, int count, std::string name) { continue; } - sprintf(buf, "%s %s '%s'.", + sprintf(buf, "%s наход%sся в хранилище дружины '%s'.", temp->get_short_description().c_str(), grammar::ObjPluralVerbEnding((temp)->get_sex()), (*clan)->GetAbbrev()); // CAP(buf); if (privilege::IsGrGod(ch)) { - sprintf(buf2, " Vnum : %d", GET_OBJ_VNUM(temp)); + sprintf(buf2, " Vnum предмета: %d", GET_OBJ_VNUM(temp)); strcat(buf, buf2); } strcat(buf, "\r\n"); @@ -4197,7 +4197,7 @@ void Clan::add_remember(std::string text, int flag) { break; default: - log("SYSERROR: , flag: %d, func: %s", + log("SYSERROR: мы не должны были сюда попасть, flag: %d, func: %s", flag, __func__); break; } @@ -4228,12 +4228,12 @@ std::string Clan::get_remember(unsigned int num, int flag) const { break; } default: - log("SYSERROR: , flag: %d, func: %s", + log("SYSERROR: мы не должны были сюда попасть, flag: %d, func: %s", flag, __func__); break; } if (buffer.empty()) { - buffer = " .\r\n"; + buffer = "Вам нечего вспомнить.\r\n"; } return buffer; } @@ -4255,7 +4255,7 @@ void Clan::init_ingr_chest() { return; } - // + // на случай релоада for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { ObjData *obj_next; @@ -4277,7 +4277,7 @@ void Clan::init_ingr_chest() { log(" IngrChest load error '%d'! (%s %s %d)", GetRent(), __FILE__, __func__, __LINE__); return; } - // + //лоадим в комнату сам хран PlaceObjToRoom(chest.get(), get_ingr_chest_room_rnum()); FILE *fl = fopen(filename.c_str(), "r+b"); @@ -4335,13 +4335,13 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || CLAN(ch)->GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Не имеете таких правов!\r\n", ch); return false; } if (obj->get_type() != EObjType::kMagicComponent && obj->get_type() != EObjType::kCraftMaterial) { - SendMsgToChar(ch, "%s - .\r\n", + SendMsgToChar(ch, "%s - Хранилище ингредиентов не предназначено для предметов данного типа.\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); if (obj->get_type() == EObjType::kMoney) { @@ -4349,7 +4349,7 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { RemoveObjFromChar(obj); ExtractObjFromWorld(obj); currencies::AddHand(*ch, currencies::kGold, howmany); - SendMsgToChar(ch, " %d %s.\r\n", howmany, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(howmany, grammar::ECase::kNom).c_str()); + SendMsgToChar(ch, "Вы вновь обрели %d %s.\r\n", howmany, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(howmany, grammar::ECase::kNom).c_str()); } } else if (obj->has_flag(EObjFlag::kNodrop) || obj->has_flag(EObjFlag::kZonedecay) @@ -4357,10 +4357,10 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { || obj->has_flag(EObjFlag::kNorent) || obj->get_rent_off() < 0 || obj->get_rnum() <= kNothing) { - act(" $o3 $O3.", false, ch, obj, chest, kToChar); + act("Неведомая сила помешала положить $o3 в $O3.", false, ch, obj, chest, kToChar); } else { if (CLAN(ch)->ingr_chest_objcount_ >= CLAN(ch)->ingr_chest_max_objects()) { - act(" $o3 $O3, - .", + act("Вы попытались запихнуть $o3 в $O3, но не смогли - там просто нет места.", false, ch, obj, @@ -4371,7 +4371,7 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { dungeons::SwapOriginalObject(obj); RemoveObjFromChar(obj); PlaceObjIntoObj(obj, chest); - act(" $o3 $O3.", false, ch, obj, chest, kToChar); + act("Вы положили $o3 в $O3.", false, ch, obj, chest, kToChar); CLAN(ch)->ingr_chest_objcount_++; GlobalObjects::ingr_chest_saver().mark_dirty(CLAN(ch).get()); } @@ -4381,14 +4381,14 @@ bool Clan::put_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { bool Clan::take_ingr_chest(CharData *ch, ObjData *obj, ObjData *chest) { if (ch->IsNpc() || !CLAN(ch) || CLAN(ch)->GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Не имеете таких правов!\r\n", ch); return false; } RemoveObjFromObj(obj); PlaceObjToInventory(obj, ch); if (obj->get_carried_by() == ch) { - act(" $o3 $O1.", false, ch, obj, chest, kToChar); + act("Вы взяли $o3 из $O1.", false, ch, obj, chest, kToChar); CLAN(ch)->ingr_chest_objcount_--; GlobalObjects::ingr_chest_saver().mark_dirty(CLAN(ch).get()); } @@ -4403,20 +4403,20 @@ bool ClanSystem::show_ingr_chest(ObjData *obj, CharData *ch) { if (CLAN(ch) && CLAN(ch)->ingr_chest_active() && CLAN(ch)->GetRent() / 100 == GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Хранилище ингредиентов вашей дружины:\r\n", ch); int cost = CLAN(ch)->ingr_chest_tax(); - SendMsgToChar(ch, " : %d/%d, : %d %s\r\n\r\n", + SendMsgToChar(ch, "Всего вещей: %d/%d, Рента в день: %d %s\r\n\r\n", CLAN(ch)->get_ingr_chest_objcount(), CLAN(ch)->ingr_chest_max_objects(), cost, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom).c_str()); sight::list_obj_to_char(obj->get_contains(), ch, 1, 4); } else { - SendMsgToChar(" , , .\r\n", ch); + SendMsgToChar("Не на что тут глазеть, пусто, вот те крест.\r\n", ch); } return true; } -// * . +// * Расчет суточной ренты хранилища ингров. int Clan::ingr_chest_tax() { if (!ingr_chest_active()) { return false; @@ -4439,7 +4439,7 @@ int Clan::ingr_chest_tax() { return cost; } -// * -. +// * Очистка хранилища ингров при нулевом клан-банке. void Clan::purge_ingr_chest() { if (!ingr_chest_active()) { return; @@ -4469,9 +4469,9 @@ int Clan::calculate_clan_tax() const { } bool Clan::ingr_chest_active() const { - // ( ) - // : , olc, etc. - // . issue #3191. + // Распущенная дружина (без членов) не должна считаться владельцем + // сундука для ингров: платить налог, отображать его в olc, etc. + // См. issue #3191. if (m_members.empty()) { return false; } @@ -4480,12 +4480,12 @@ bool Clan::ingr_chest_active() const { void Clan::set_ingr_chest(CharData *ch) { if (GetRent() / 100 != GET_ROOM_VNUM(ch->in_room) / 100) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Данная комната находится вне зоны вашего замка.\r\n", ch); return; } bool chest_moved = false; - // + // хран под ингры уже был if (ingr_chest_active()) { for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { @@ -4505,22 +4505,22 @@ void Clan::set_ingr_chest(CharData *ch) { if (chest) { PlaceObjToRoom(chest.get(), get_ingr_chest_room_rnum()); } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Хранилище установлено.\r\n", ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Хранилище перенесено.\r\n", ch); } } void Clan::disable_ingr_chest(CharData *ch) { if (!ingr_chest_active()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас и так нет хранилища для ингредиентов.\r\n", ch); return; } for (auto chest : world[get_ingr_chest_room_rnum()]->contents) { if (is_ingr_chest(chest)) { if (chest->get_contains()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Во избежание недоразумений отключить можно только пустое хранилище.\r\n", ch); return; } ExtractObjFromWorld(chest); @@ -4528,7 +4528,7 @@ void Clan::disable_ingr_chest(CharData *ch) { } } ingr_chest_room_rnum_ = 0; - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Хранилище отключено.\r\n", ch); } int Clan::ingr_chest_max_objects() { @@ -4545,34 +4545,34 @@ void save_chest_log() { } } -// * ''. +// * Генерация справки 'сайтыдружин'. void init_xhelp() { std::stringstream out; - out << " , .\r\n" - " , ,\r\n" - " , \r\n" - " , , , \r\n" - " .\r\n\r\n" - " :\r\n\r\n"; + out << " В данном разделе приведены адреса сайтов, принадлежащим той или иной дружине.\r\n" + "Как правило, на подобных сайтах вы можете ознакомиться с уставом дружины,\r\n" + "узнать условия вступления в дружину, а также получить довольно много информации\r\n" + "о политике дружины, ее составе, важных событиях, участие в которых принимали ее\r\n" + "ратники и многое другое.\r\n\r\n" + " Список сайтов дружин:\r\n\r\n"; for (const auto &i : Clan::ClanList) { out << " $COLORW" << fmt::format("{:<7}", i->GetAbbrev()) << "$COLORn -- $COLORC" - << (i->get_web_url().empty() ? "$COLORW[ ]" : i->get_web_url()) + << (i->get_web_url().empty() ? "$COLORW[ НЕТ ИНФОРМАЦИИ ]" : i->get_web_url()) << "$COLORn\r\n"; } - out << "\r\n :$COLORc www.mud.ru$COLORn\r\n" - << " :$COLORc mudhistory.nm.ru$COLORn\r\n" - << "\r\n. :$COLORC $COLORn\r\n"; + out << "\r\n Официальный сайт мада МПМ Былины:$COLORc www.mud.ru$COLORn\r\n" + << " Сайт истории мада МПМ Былины:$COLORc mudhistory.nm.ru$COLORn\r\n" + << "\r\nСм. также:$COLORC ДРУЖИНЫ $COLORn\r\n"; - HelpSystem::add_dynamic("", out.str()); + HelpSystem::add_dynamic("САЙТЫДРУЖИН", out.str()); HelpSystem::add_dynamic("CLANSITES", out.str()); HelpSystem::add_dynamic("INTERNETLINKS", out.str()); } const char *GOLD_TAX_FORMAT = - " : < 0 25>\r\n" - " .\r\n"; + "Формат команды: клан налог <число от 0 до 25>\r\n" + "Устанавливает процент автоматических отчислений в казну дружины.\r\n"; void tax_manage(CharData *ch, std::string &buffer) { if (!CLAN(ch)) return; @@ -4583,7 +4583,7 @@ void tax_manage(CharData *ch, std::string &buffer) { int tax = std::stoi(buffer, nullptr, 10); if (tax <= MAX_GOLD_TAX_PCT) { CLAN(ch)->set_gold_tax_pct(tax); - SendMsgToChar(ch, " %d%%\r\n", tax); + SendMsgToChar(ch, "Налог для ратников дружины установлен в %d%%\r\n", tax); } else { SendMsgToChar(GOLD_TAX_FORMAT, ch); } @@ -4592,7 +4592,7 @@ void tax_manage(CharData *ch, std::string &buffer) { SendMsgToChar(GOLD_TAX_FORMAT, ch); } } else { - SendMsgToChar(ch, " : %ld%%\r\n%s", + SendMsgToChar(ch, "Текущий налог для ратников дружины: %ld%%\r\n%s", CLAN(ch)->get_gold_tax_pct(), GOLD_TAX_FORMAT); } } @@ -4607,27 +4607,27 @@ long do_gold_tax(CharData *ch, long gold) { const long tax = (gold * CLAN(ch)->get_gold_tax_pct()) / 100; if (tax <= 0) return false; - // TODO: desc_count ? + // TODO: мб вынести как в desc_count для версии с окончаниями? if ((tax % 100 >= 11 && tax % 100 <= 14) || tax % 10 >= 5 || tax % 10 == 0) { SendMsgToChar(ch, - "%ld %s .\r\n", + "%ld %s было немедленно отправлено в казну вашей дружины.\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } else if (tax % 10 == 1) { SendMsgToChar(ch, - "%ld %s .\r\n", + "%ld %s была немедленно отправлена в казну вашей дружины.\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } else { SendMsgToChar(ch, - "%ld %s .\r\n", + "%ld %s были немедленно отправлены в казну вашей дружины.\r\n", tax, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(tax, grammar::ECase::kNom).c_str()); } - // 1 , + // 1 куну за транзакцию, если сумма налога позволяет const long real_tax = tax > 1 ? tax - 1 : tax; CLAN(ch)->set_bank(CLAN(ch)->get_bank() + real_tax); CLAN_MEMBER(ch)->money += real_tax; - // , + // возврат полного налога, снятого с чара return tax; } return false; @@ -4637,7 +4637,7 @@ long do_gold_tax(CharData *ch, long gold) { //////////////////////////////////////////////////////////////////////////////// -// <> +// клан сайт <текст> void Clan::house_web_url(CharData *ch, const std::string &buffer) { const unsigned MAX_URL_LENGTH = 40; std::istringstream tmp(buffer); @@ -4646,17 +4646,17 @@ void Clan::house_web_url(CharData *ch, const std::string &buffer) { if (url.size() > MAX_URL_LENGTH) { url = url.substr(0, MAX_URL_LENGTH); - SendMsgToChar(ch, " %u .\r\n", MAX_URL_LENGTH); + SendMsgToChar(ch, "Строка была обрезана до %u символов.\r\n", MAX_URL_LENGTH); } if (url.empty()) { - SendMsgToChar(" .\r\n" - " '' .\r\n", ch); + SendMsgToChar("Адрес сайта вашей дружины удален.\r\n" + "Обновление справки 'сайтыдружин' состоится в течении минуты.\r\n", ch); this->web_url_.clear(); } else { this->web_url_ = url; - SendMsgToChar(" .\r\n" - " '' .\r\n", ch); + SendMsgToChar("Адрес сайта вашей дружины установлен.\r\n" + "Обновление справки 'сайтыдружин' состоится в течении минуты.\r\n", ch); snprintf(buf, sizeof(buf), "%s sets new clan website: %s", GET_NAME(ch), url.c_str()); mudlog(buf, LGH, kLvlImmortal, SYSLOG, true); @@ -4665,8 +4665,8 @@ void Clan::house_web_url(CharData *ch, const std::string &buffer) { HelpSystem::need_update = true; } -// ( " ", ..): -// - +// для использования с кланами (в "клан лог", сообщениях хранилища и т.д.): +// возвращает клан-метку с ведущим пробелом std::string clan_get_custom_label(ObjData *obj, Clan::shared_ptr clan) { if (obj->get_custom_label() && !obj->get_custom_label()->text_label.empty() @@ -4739,8 +4739,8 @@ long Clan::get_bank() const { return bank; } -// , , -// , +// проверка, находится ли левый чар в замке, +// если да, то выпинываем его оттуда void ClanSystem::check_player_in_house() { for (auto d = descriptor_list; d; d = d->next) { if (d->character @@ -4748,17 +4748,17 @@ void ClanSystem::check_player_in_house() { const auto clan = Clan::GetClanByRoom(d->character->in_room); if (clan) { char_from_room(d->character); - act("$n $g $a !", true, d->character.get(), 0, 0, kToRoom); - SendMsgToChar(" !\r\n", d->character.get()); + act("$n был$g выдворен$a за пределы замка!", true, d->character.get(), 0, 0, kToRoom); + SendMsgToChar("Вы были выдворены за пределы замка!\r\n", d->character.get()); char_to_room(d->character, GetRoomRnum(clan->GetOutRent())); sight::look_at_room(d->character.get(), GetRoomRnum(clan->GetOutRent())); - act("$n $u , - !", true, d->character.get(), 0, 0, kToRoom); + act("$n свалил$u с небес, выкрикивая какие-то ругательства!", true, d->character.get(), 0, 0, kToRoom); } } } } -// , - +// показывает, является ли чар союзником такой-то дружине bool ClanSystem::is_alliance(CharData *ch, char *clan_abbr) { std::string abbrev = clan_abbr; if (!CLAN(ch)) { @@ -4787,7 +4787,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; std::string buffer = argument; std::ostringstream buffer3; - buffer3 << ", :\r\n"; + buffer3 << "Дружины, находящиеся в состоянии войны:\r\n"; if (!buffer.empty()) { Clan::ClanListType::const_iterator clan1; for (clan1 = Clan::ClanList.begin(); clan1 != Clan::ClanList.end(); ++clan1) { @@ -4797,7 +4797,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan1 == Clan::ClanList.end() || (*clan1)->m_members.size() == 0) { - SendMsgToChar(" \r\n", ch); + SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); return; } @@ -4808,7 +4808,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if ((*clan1)->CheckPolitics((*clan2)->rent) == kPoliticsWar) { - buffer3 << " " << (*clan1)->abbrev << " " << (*clan2)->abbrev << "\r\n"; + buffer3 << " " << (*clan1)->abbrev << " против " << (*clan2)->abbrev << "\r\n"; } } } else { @@ -4819,7 +4819,7 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan1->CheckPolitics(clan2->rent) == kPoliticsWar) { - buffer3 << " " << clan1->abbrev << " " << clan2->abbrev << "\r\n"; + buffer3 << " " << clan1->abbrev << " против " << clan2->abbrev << "\r\n"; } } } @@ -4829,16 +4829,16 @@ void DoShowWars(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } -// - , , -// 34 , -// +// обработка клан-канала и канала союзников, как игрока, так и имма +// клановые БОГи ниже 34 не могут говорить другим дружинам, и им и остальным спокойнее +// для канала союзников нужен обоюдный альянс дружин void DoClanChannel(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (ch->IsNpc()) return; std::string buffer = argument; - // 34 - + // большой неклановый или 34 клановый БОГ говорит какой-то дружине if (privilege::IsImpl(ch) || (privilege::IsGrGod(ch) && !CLAN(ch))) { std::string buffer2; GetOneParam(buffer, buffer2); @@ -4851,29 +4851,29 @@ void DoClanChannel(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } if (clan == Clan::ClanList.end()) { - if (!CLAN(ch)) // 34 - SendMsgToChar(" .\r\n", ch); - else // 34 , - + if (!CLAN(ch)) // неклановый 34 ошибся аббревиатурой + SendMsgToChar("Дружина с такой аббревиатурой не найдена.\r\n", ch); + else // клановый 34 ошибиться не может, идет в его клан-канал { - buffer = argument; // + buffer = argument; // финт ушами CLAN(ch)->CharToChannel(ch, buffer, subcmd); } return; } (*clan)->GodToChannel(ch, buffer, subcmd); - // + // остальные говорят только в свою дружину } else { if (!CLAN(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не принадлежите ни к одной дружине.\r\n", ch); return; } - // - , + // ограничения на клан-канал не канают на любое звание, если это БОГ if (!privilege::IsImmortal(ch) && (!(CLAN(ch))->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_CHANNEL] || ch->IsFlagged(EPlrFlag::kDumbed))) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не можете пользоваться каналом дружины.\r\n", ch); return; } @@ -4887,16 +4887,16 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } std::string buffer = argument; if (buffer.empty()) { - // + // сортировка кланов по экспе std::multimap sort_clan; for (const auto &clan : Clan::ClanList) { sort_clan.insert(std::make_pair(clan->exp, clan)); } std::ostringstream out; - // \todo table_wrapper::Table . + // \todo Тут нужно использовать table_wrapper::Table а не формат. std::string_view clanTopFormat{" {:5} {:6} {:<30} {:14}{:14} {:9}\r\n"}; - out << " :\r\n" - << " # 30 \r\n\r\n"; + out << "В игре зарегистрированы следующие дружины:\r\n" + << " # Название Всего опыта За 30 дней Человек\r\n\r\n"; int count = 1; for (const auto &it : reverse(sort_clan)) { if (it.second->m_members.size() == 0) { @@ -4926,22 +4926,22 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (clan == Clan::ClanList.end()) { - if (CompareParam(buffer, "")) { + if (CompareParam(buffer, "все")) { all = true; } else { - SendMsgToChar(" \r\n", ch); + SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); return; } } - // - + // спам-контроль применяем только если запросили полный список или не свою дружину if (all || !ch->player_specials->clan || !CompareParam(ch->player_specials->clan->GetAbbrev(), (*clan)->abbrev)) { if (PerformWhoSpamcontrol(ch, kWhoListclan)) { return; } } - // ( all) + // строится список членов дружины или всех дружин (по флагу all) std::vector temp_list; for (auto d = descriptor_list; d; d = d->next) { if (d->character @@ -4955,14 +4955,14 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } - // + // до кучи сортировка по рангам std::sort(temp_list.begin(), temp_list.end(), SortRank()); std::ostringstream buffer2; - buffer2 << " :\r\n" << " # \r\n\r\n"; + buffer2 << "В игре зарегистрированы следующие дружины:\r\n" << " # Глава Название\r\n\r\n"; std::string_view clanFormat{" {:5} {:6} {:15} {}\r\n"}; std::string_view memberFormat{" {:>10} {}{}{} {}{}{}\r\n"}; - // - + // если искали конкретную дружину - выводим ее if (!all) { buffer2 << fmt::format(fmt::runtime(clanFormat), 1, (*clan)->abbrev, (*clan)->owner, (*clan)->name); for (const auto &it : temp_list) { @@ -4970,11 +4970,11 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { : (*clan)->ranks_female[CLAN_MEMBER(it)->rank_num]), GetPkNameColor(it), (it)->GetNameWithTitleOrRace(), kColorNrm, kColorBoldRed, - (it->IsFlagged(EPlrFlag::kKiller) ? "()" : ""), + (it->IsFlagged(EPlrFlag::kKiller) ? "(ДУШЕГУБ)" : ""), kColorNrm); } } - // ( '' ) + // просто выводим все дружины и всех членов (без параметра 'все' в списке будут только дружины) else { int count = 1; for (auto clan_i = Clan::ClanList.begin(); clan_i != Clan::ClanList.end(); ++clan_i) { @@ -4989,7 +4989,7 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { buffer2 << fmt::format(fmt::runtime(memberFormat), (*clan_i)->ranks[CLAN_MEMBER(it)->rank_num], GetPkNameColor(it), it->GetNameWithTitleOrRace(), kColorNrm, kColorBoldRed, - (it->IsFlagged(EPlrFlag::kKiller) ? "()" : ""), + (it->IsFlagged(EPlrFlag::kKiller) ? "(ДУШЕГУБ)" : ""), kColorNrm); } } @@ -4997,13 +4997,13 @@ void DoClanList(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } - buffer2 << "\r\n - " << temp_list.size() << "\r\n"; + buffer2 << "\r\nВсего игроков - " << temp_list.size() << "\r\n"; SendMsgToChar(buffer2.str(), ch); } void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } std::string buffer = argument, buffer2; @@ -5012,23 +5012,23 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { //std::string frmt{"%s [%s] :: %s\r\n%s\r\n\r\n"}; if (buffer2.empty()) { - // , + // выводим список тех, кто онлайн SendMsgToChar(ch, - "%s :%s\r\n\r\n", + "%sОтображаются только находящиеся в игре персонажи:%s\r\n\r\n", kColorWht, kColorNrm); ClanPkList::const_iterator it; - // , - - + // вобщем чтобы словить чаров, находящихся в лд - придется гонять по чарактер-листу for (const auto &tch : character_list) { if (tch->IsNpc()) continue; - // + // пкл if (!subcmd) { it = CLAN(ch)->pkList.find(tch->get_uid()); if (it != CLAN(ch)->pkList.end()) print_pkl(ch, info, it); } - // + // дрл else { it = CLAN(ch)->frList.find(tch->get_uid()); if (it != CLAN(ch)->frList.end()) @@ -5036,55 +5036,55 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } } if (info.str().empty()) - info << " .\r\n"; + info << "Записи в выбранном списке отсутствуют.\r\n"; page_string(ch->desc, info.str()); - } else if (CompareParam(buffer2, "") || CompareParam(buffer2, "all")) { - // - SendMsgToChar(ch, "%s :%s\r\n\r\n", kColorWht, kColorNrm); - // + } else if (CompareParam(buffer2, "все") || CompareParam(buffer2, "all")) { + // выводим весь список + SendMsgToChar(ch, "%sСписок отображается полностью:%s\r\n\r\n", kColorWht, kColorNrm); + // пкл if (!subcmd) for (ClanPkList::const_iterator it = CLAN(ch)->pkList.begin(); it != CLAN(ch)->pkList.end(); ++it) print_pkl(ch, info, it); - // + // дрл else for (ClanPkList::const_iterator it = CLAN(ch)->frList.begin(); it != CLAN(ch)->frList.end(); ++it) print_pkl(ch, info, it); if (info.str().empty()) - info << " .\r\n"; + info << "Записи в выбранном списке отсутствуют.\r\n"; page_string(ch->desc, info.str()); - } else if ((CompareParam(buffer2, "") || CompareParam(buffer2, "add")) + } else if ((CompareParam(buffer2, "добавить") || CompareParam(buffer2, "add")) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { - // + // добавляем нового GetOneParam(buffer, buffer2); if (buffer2.empty()) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Кого добавить то?\r\n", ch); return; } long unique = GetUniqueByName(buffer2, 1); if (!unique) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Интересующий вас персонаж не найден.\r\n", ch); return; } if (unique < 0) { - SendMsgToChar(" , ....\r\n", ch); + SendMsgToChar("Не дело это, Богов добавлять куда не надо....\r\n", ch); return; } const auto it = CLAN(ch)->m_members.find(unique); if (it != CLAN(ch)->m_members.end()) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Давайте не будем засорять список всяким бредом?\r\n", ch); return; } if (buffer.empty()) { - SendMsgToChar(" , .\r\n", ch); + SendMsgToChar("Потрудитесь прокомментировать, за что вы его так.\r\n", ch); return; } - // + // тож надо проверять ClanPkList::iterator it2; if (!subcmd) { it2 = CLAN(ch)->pkList.find(unique); @@ -5094,20 +5094,20 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if ((!subcmd && it2 != CLAN(ch)->pkList.end()) || (subcmd && it2 != CLAN(ch)->frList.end())) { - // , + // уид тут саавсем не обязательно, что валидный const auto rank_it = CLAN(ch)->m_members.find(it2->second->author); if (rank_it != CLAN(ch)->m_members.end() && rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { if (!subcmd) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Ваша жертва уже добавлена в список врагов старшим по званию.\r\n", ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Персонаж уже добавлен в список друзей старшим по званию.\r\n", ch); } return; } } - // / + // собственно пишем новую жертву/друга ClanPkPtr tempRecord(new ClanPk); tempRecord->author = ch->get_uid(); tempRecord->authorName = GET_NAME(ch); @@ -5124,13 +5124,13 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (d && d->character->IsFlagged(EPrf::kPklMode)) { if (!subcmd) { SendMsgToChar(d->character.get(), - "%s '%s' !%s\r\n", + "%sДружина '%s' добавила вас в список своих врагов!%s\r\n", kColorBoldRed, CLAN(ch)->name.c_str(), kColorNrm); } else { SendMsgToChar(d->character.get(), - "%s '%s' !%s\r\n", + "%sДружина '%s' добавила вас в список своих друзей!%s\r\n", kColorGrn, CLAN(ch)->name.c_str(), kColorNrm); @@ -5138,32 +5138,32 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SetWait(ch, 1, false); } - SendMsgToChar(", .\r\n", ch); - } else if ((CompareParam(buffer2, "") || CompareParam(buffer2, "delete")) + SendMsgToChar("Ладушки, добавили.\r\n", ch); + } else if ((CompareParam(buffer2, "удалить") || CompareParam(buffer2, "delete")) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { - // + // удаление записи GetOneParam(buffer, buffer2); - if (CompareParam(buffer2, "", 1) || CompareParam(buffer2, "all", 1)) { + if (CompareParam(buffer2, "все", 1) || CompareParam(buffer2, "all", 1)) { if (CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Полная очистка списка доступна только воеводе.\r\n", ch); return; } - // + // пкл if (!subcmd) CLAN(ch)->pkList.erase(CLAN(ch)->pkList.begin(), CLAN(ch)->pkList.end()); - // + // дрл else CLAN(ch)->frList.erase(CLAN(ch)->frList.begin(), CLAN(ch)->frList.end()); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Список очищен.\r\n", ch); return; } long unique = GetUniqueByName(buffer2, true); if (unique <= 0) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Интересующий вас персонаж не найден.\r\n", ch); return; } - // , , subcmd + // пкл, раздельно они мне больше нравятся, чем по пять раз subcmd проверять bool removed = false; if (!subcmd) { ClanPkList::iterator it; @@ -5172,13 +5172,13 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { const auto pk_rank_it = CLAN(ch)->m_members.find(it->second->author); if (pk_rank_it != CLAN(ch)->m_members.end() && pk_rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Ваша жертва была добавлена старшим по званию.\r\n", ch); return; } CLAN(ch)->pkList.erase(it); removed = true; } - // + // дрл } else { ClanPkList::iterator it; it = CLAN(ch)->frList.find(unique); @@ -5186,7 +5186,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { const auto fr_rank_it = CLAN(ch)->m_members.find(it->second->author); if (fr_rank_it != CLAN(ch)->m_members.end() && fr_rank_it->second->rank_num < CLAN_MEMBER(ch)->rank_num) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Персонаж был добавлен старшим по званию.\r\n", ch); return; } CLAN(ch)->frList.erase(it); @@ -5195,19 +5195,19 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } if (removed) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Запись удалена.\r\n", ch); DescriptorData *d; if ((d = DescriptorByUid(unique)) && d->character->IsFlagged(EPrf::kPklMode)) { if (!subcmd) { SendMsgToChar(d->character.get(), - "%s '%s' !%s\r\n", + "%sДружина '%s' удалила вас из списка своих врагов!%s\r\n", kColorGrn, CLAN(ch)->name.c_str(), kColorNrm); } else { SendMsgToChar(d->character.get(), - "%s '%s' !%s\r\n", + "%sДружина '%s' удалила вас из списка своих друзей!%s\r\n", kColorBoldRed, CLAN(ch)->name.c_str(), kColorNrm); @@ -5215,22 +5215,22 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SetWait(ch, 1, false); } } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Запись не найдена.\r\n", ch); } } else { utils::Trim(buffer); bool online = 1; - if (CompareParam(buffer, "all") || CompareParam(buffer, "")) + if (CompareParam(buffer, "all") || CompareParam(buffer, "все")) online = 0; if (online) SendMsgToChar(ch, - "%s :%s\r\n\r\n", + "%sОтображаются только находящиеся в игре персонажи:%s\r\n\r\n", kColorWht, kColorNrm); else - SendMsgToChar(ch, "%s :%s\r\n\r\n", kColorWht, kColorNrm); + SendMsgToChar(ch, "%sСписок отображается полностью:%s\r\n\r\n", kColorWht, kColorNrm); std::ostringstream out; @@ -5252,7 +5252,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { if (!out.str().empty()) page_string(ch->desc, out.str()); else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("По вашему запросу никого не найдено.\r\n", ch); if (CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PKLIST]) { buffer = CLAN_PKLIST_FORMAT[0]; buffer += CLAN_PKLIST_FORMAT[1]; @@ -5263,7 +5263,7 @@ void DoClanPkList(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } } -// ( house) +// обработка клановых привилегий (команда house) void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; @@ -5271,31 +5271,31 @@ void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { std::string buffer = argument, buffer2; GetOneParam(buffer, buffer2); - // , + // если игрок неклановый, то есть вариант с приглашением if (!CLAN(ch)) { - if (CompareParam(buffer2, "") && ch->desc->clan_invite) + if (CompareParam(buffer2, "согласен") && ch->desc->clan_invite) ch->desc->clan_invite->clan->ClanAddMember(ch, ch->desc->clan_invite->rank, ch->desc->clan_invite->invite_name); - else if (CompareParam(buffer2, "") && ch->desc->clan_invite) { + else if (CompareParam(buffer2, "отказать") && ch->desc->clan_invite) { ch->desc->clan_invite.reset(); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Приглашение дружины отклонено.\r\n", ch); return; } else - SendMsgToChar(" ,\r\n" - " , ' ' ' '.\r\n" - " , .\r\n", + SendMsgToChar("Данная команда доступна только высоко привилегированным членам дружин,\r\n" + "а если вас пригласили вступить в оную, то так и пишите 'клан согласен' или 'клан отказать'.\r\n" + "Никто не сможет послать вам новое приглашение до тех пор, пока вы не разберетесь с этим.\r\n", ch); return; } - if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_INFO]) + if (CompareParam(buffer2, "информация") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_INFO]) CLAN(ch)->HouseInfo(ch); - else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_ADD]) + else if (CompareParam(buffer2, "принять") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_ADD]) CLAN(ch)->HouseAdd(ch, buffer); - else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_REMOVE]) + else if (CompareParam(buffer2, "изгнать") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_REMOVE]) CLAN(ch)->HouseRemove(ch, buffer); - else if (CompareParam(buffer2, "") + else if (CompareParam(buffer2, "привилегии") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_PRIVILEGES]) { std::shared_ptr temp_clan_olc(new ClanOLC); temp_clan_olc->clan = CLAN(ch); @@ -5303,59 +5303,59 @@ void DoHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { ch->desc->clan_olc = temp_clan_olc; ch->desc->state = EConState::kClanedit; CLAN(ch)->MainMenu(ch->desc); - } else if (CompareParam(buffer2, "") && !CLAN_MEMBER(ch)->rank_num) + } else if (CompareParam(buffer2, "воевода") && !CLAN_MEMBER(ch)->rank_num) CLAN(ch)->HouseOwner(ch, buffer); - else if (CompareParam(buffer2, "")) + else if (CompareParam(buffer2, "статистика")) CLAN(ch)->HouseStat(ch, buffer); - else if (CompareParam(buffer2, "", true) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_EXIT]) + else if (CompareParam(buffer2, "покинуть", true) && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_EXIT]) CLAN(ch)->HouseLeave(ch); - else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_TAX]) + else if (CompareParam(buffer2, "налог") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_TAX]) tax_manage(ch, buffer); - else if (CompareParam(buffer2, "") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_MOD]) { + else if (CompareParam(buffer2, "сообщение") && CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_MOD]) { prepare_write_mod(ch, buffer); - } else if (CompareParam(buffer2, "")) { + } else if (CompareParam(buffer2, "пк")) { CLAN(ch)->pk_log.print(ch); - } else if (CompareParam(buffer2, "")) { + } else if (CompareParam(buffer2, "лог")) { CLAN(ch)->chest_log.print(ch, buffer); - } else if (CompareParam(buffer2, "") && !CLAN_MEMBER(ch)->rank_num) { + } else if (CompareParam(buffer2, "сайт") && !CLAN_MEMBER(ch)->rank_num) { CLAN(ch)->house_web_url(ch, buffer); } else { - // - buffer = " :\r\n"; + // обработка списка доступных команд по званию персонажа + buffer = "Доступные вам привилегии дружины:\r\n"; for (unsigned i = 0; i < CLAN_PRIVILEGES_NUM; ++i) if (CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][i]) buffer += HOUSE_FORMAT[i]; - // + // воевода до кучи может сам сменить у дружины воеводу if (!CLAN_MEMBER(ch)->rank_num) { - buffer += " ()\r\n"; - buffer += " ( ' ')\r\n"; + buffer += " клан воевода (имя)\r\n"; + buffer += " клан сайт (адрес сайта вашей дружины для 'справка сайтыдружин')\r\n"; } if (CLAN(ch)->storehouse) - buffer += " <>\r\n"; - buffer += " "; + buffer += " хранилище <фильтры>\r\n"; + buffer += " клан статистика "; if (!CLAN_MEMBER(ch)->rank_num) - buffer += " <||| >\r\n"; + buffer += " <имя|все|очистить|очистить деньги>\r\n"; else - buffer += " <|>\r\n"; + buffer += " <имя|все>\r\n"; if (!CLAN(ch)->privileges[CLAN_MEMBER(ch)->rank_num][MAY_CLAN_POLITICS]) { - buffer += " ( )\r\n"; + buffer += " политика (только просмотр)\r\n"; } - buffer += " ( )\r\n"; - buffer += " < | >\r\n"; + buffer += " клан пк (список последних сражений)\r\n"; + buffer += " клан лог <без параметров|строка поиска>\r\n"; SendMsgToChar(buffer, ch); } } -// ( , ) +// ктодружина (список соклановцев, находящихся онлайн) void DoWhoClan(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } std::ostringstream buffer; - buffer << fmt::format(" : {}{}{}.\r\n", kColorBoldRed, CLAN(ch)->abbrev, kColorNrm); - buffer << fmt::format(" {} :{}\r\n\r\n", kColorWht, kColorNrm); + buffer << fmt::format(" Ваша дружина: {}{}{}.\r\n", kColorBoldRed, CLAN(ch)->abbrev, kColorNrm); + buffer << fmt::format(" {}Сейчас в игре Ваши соратники:{}\r\n\r\n", kColorWht, kColorNrm); DescriptorData *d; int num = 0; @@ -5364,13 +5364,13 @@ void DoWhoClan(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/) { buffer << " " << d->character->race_or_title().c_str() << "\r\n"; ++num; } - buffer << "\r\n : " << num << ".\r\n"; + buffer << "\r\n Всего: " << num << ".\r\n"; SendMsgToChar(buffer.str(), ch); } void DoShowPolitics(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } @@ -5382,8 +5382,8 @@ void DoShowPolitics(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int p1 = 0, p2 = 0; std::ostringstream buffer2; - buffer2 << " :\r\n" << - " \r\n"; + buffer2 << "Отношения Вашей дружины с другими дружинами:\r\n" << + "Название Отношение Вашей дружины Отношение к вашей дружине\r\n"; for (const auto &clanVictim : Clan::ClanList) { if ((clanVictim == CLAN(ch)) || ((*clanVictim).m_members.size() == 0)) { @@ -5408,7 +5408,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } std::string buffer = argument; std::ostringstream buffer3; - buffer3 << ", :\r\n"; + buffer3 << "Дружины, находящиеся в состоянии союза:\r\n"; if (!buffer.empty()) { Clan::ClanListType::const_iterator clan1; @@ -5419,7 +5419,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if (clan1 == Clan::ClanList.end() || (*clan1)->m_members.size() == 0) { - SendMsgToChar(" \r\n", ch); + SendMsgToChar("Такая дружина не зарегистрирована\r\n", ch); return; } @@ -5430,7 +5430,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if ((*clan1)->CheckPolitics((*clan2)->rent) == kPoliticsAlliance) { - buffer3 << " " << (*clan1)->abbrev << " " << (*clan2)->abbrev << "\r\n"; + buffer3 << " " << (*clan1)->abbrev << " помогает " << (*clan2)->abbrev << "\r\n"; } } } else { @@ -5441,7 +5441,7 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } if (clan1->CheckPolitics(clan2->rent) == kPoliticsAlliance) { - buffer3 << " " << clan1->abbrev << " " << clan2->abbrev << "\r\n"; + buffer3 << " " << clan1->abbrev << " помогает " << clan2->abbrev << "\r\n"; } } } @@ -5451,19 +5451,19 @@ void do_show_alliance(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) } -// - + +// вобщем это копи-паст из биржи + флаги void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc() || !CLAN(ch)) { - SendMsgToChar("?\r\n", ch); + SendMsgToChar("Чаво?\r\n", ch); return; } if (!CLAN(ch)->storehouse) { - SendMsgToChar(" ! :(\r\n", ch); + SendMsgToChar("Ваш воевода зажал денег и отключил эту возможность! :(\r\n", ch); return; } char *stufina = one_argument(argument, arg); - if (!str_cmp(arg, "") || !str_cmp(arg, "all")) { + if (!str_cmp(arg, "все") || !str_cmp(arg, "all")) { for (auto chest : world[GetRoomRnum(CLAN(ch)->chest_room)]->contents) { if (Clan::is_clan_chest(chest)) { Clan::ChestShow(chest, ch); @@ -5471,9 +5471,9 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - if (utils::IsAbbr(arg, "") || utils::IsAbbr(arg, "identify") || utils::IsAbbr(arg, "")) { + if (utils::IsAbbr(arg, "характеристики") || utils::IsAbbr(arg, "identify") || utils::IsAbbr(arg, "опознать")) { if ((currencies::GetBank(*ch, currencies::kGold) < kChestIdentPay) && (GetRealLevel(ch) < kLvlImplementator)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас недостаточно денег в банке для такого исследования.\r\n", ch); return; } @@ -5481,11 +5481,11 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (Clan::is_clan_chest(chest)) { ObjData *tmp_obj = get_obj_in_list_vis(ch, stufina, chest->get_contains()); if (tmp_obj) { - SendMsgToChar(ch, " : %s\r\n", stufina); + SendMsgToChar(ch, "Характеристики предмета: %s\r\n", stufina); MortShowObjValues(tmp_obj, ch, 200); currencies::RemoveBank(*ch, currencies::kGold, kChestIdentPay); SendMsgToChar(ch, - "%s %d %s%s\r\n", + "%sЗа информацию о предмете с вашего банковского счета сняли %d %s%s\r\n", kColorBoldGrn, kChestIdentPay, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(kChestIdentPay, grammar::ECase::kNom).c_str(), @@ -5494,7 +5494,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } } } - sprintf(buf1, " %s ! .\r\n", stufina); + sprintf(buf1, "Ничего похожего на %s в хранилище ненайдено! Будьте внимательнее.\r\n", stufina); SendMsgToChar(buf1, ch); return; } @@ -5504,7 +5504,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!filter.parse_filter(ch, filter, argument)) { return; } - SendMsgToChar(ch, ": %s\r\n", filter.print().c_str()); + SendMsgToChar(ch, "Выборка: %s\r\n", filter.print().c_str()); SetWait(ch, 1, false); std::string out; @@ -5522,7 +5522,7 @@ void DoStoreHouse(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!out.empty()) page_string(ch->desc, out); else - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Ничего не найдено.\r\n", ch); } void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -5530,12 +5530,12 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { int cnt = 0, gold_total = 0; if (!CLAN(ch)) { - SendMsgToChar(" - .\r\n", ch); + SendMsgToChar("Сначала вступите в какой-нибудь клан.\r\n", ch); return; } if (GET_ROOM_VNUM(ch->in_room) != CLAN(ch)->GetRent()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Получить клановую экипировку вы можете только в центре вашего замка.\r\n", ch); return; } @@ -5578,8 +5578,8 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } if (!cnt) { - act("$n $g \r\n", false, ch, nullptr, nullptr, kToRoom); - act(" \r\n", false, ch, nullptr, nullptr, kToChar); + act("$n открыл$g крышку сундука со стандартной экипировкой\r\n", false, ch, nullptr, nullptr, kToRoom); + act("Вы открыли крышку сундука со стандартной экипировкой\r\n", false, ch, nullptr, nullptr, kToChar); } int gold = obj->get_cost(); @@ -5588,27 +5588,27 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { currencies::RemoveHand(*ch, currencies::kGold, gold); gold_total += gold; } else { - SendMsgToChar(ch, " !\r\n"); + SendMsgToChar(ch, "Кончились денюжки!\r\n"); break; } PlaceObjToInventory(obj.get(), ch); cnt++; - sprintf(buf, "$n $g %s ", obj->get_PName(grammar::ECase::kNom).c_str()); - sprintf(buf2, " %s ", obj->get_PName(grammar::ECase::kNom).c_str()); + sprintf(buf, "$n взял$g %s из сундука", obj->get_PName(grammar::ECase::kNom).c_str()); + sprintf(buf2, "Вы взяли %s из сундука", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, ch, 0, 0, kToRoom); act(buf2, false, ch, 0, 0, kToChar); } if (cnt) { - sprintf(buf2, "\r\n %d %s.", gold_total, + sprintf(buf2, "\r\nЭкипировка обошлась вам в %d %s.", gold_total, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(gold_total, grammar::ECase::kNom).c_str()); - act("\r\n$n $g ", false, ch, 0, 0, kToRoom); + act("\r\n$n закрыл$g крышку сундука", false, ch, 0, 0, kToRoom); act(buf2, false, ch, 0, 0, kToChar); } else { - act("\r\n$n $u , $y", false, ch, 0, 0, kToRoom); - act("\r\n , ", + act("\r\n$n порыл$u в сундуке со стандартной экипировкой, но ничего не наш$y", false, ch, 0, 0, kToRoom); + act("\r\nВы порылись в сундуке со стандартной экипировкой, но не нашли ничего подходящего", false, ch, 0, @@ -5619,7 +5619,7 @@ void do_clanstuff(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SetWait(ch, 1, false); } -// hcontrol +// божественный hcontrol void DoHcontrol(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) return; diff --git a/src/gameplay/communication/boards/boards_changelog_loaders.cpp b/src/gameplay/communication/boards/boards_changelog_loaders.cpp index 3f13c0a45..fccd26846 100644 --- a/src/gameplay/communication/boards/boards_changelog_loaders.cpp +++ b/src/gameplay/communication/boards/boards_changelog_loaders.cpp @@ -24,7 +24,7 @@ void ChangeLogLoaderImplementation::add_message(const std::string &author, const std::string &desc, time_t parsed_time) { const auto message = std::make_shared(); - // + // имя автора std::string author_copy = author; const std::size_t e_pos = author_copy.find('<'); const std::size_t s_pos = author_copy.find(' '); @@ -39,7 +39,7 @@ void ChangeLogLoaderImplementation::add_message(const std::string &author, utils::Trim(desc_copy); message->text = desc_copy; - // + // из текста первая строка в заголовок std::string subj(message->text.begin(), std::find(message->text.begin(), message->text.end(), '\n')); /* if (subj.size() > 40) { @@ -204,8 +204,8 @@ bool GitChangeLogLoader::GitCommitReader::next_line(std::string &buffer, std::is if (!std::getline(is, buffer)) { return false; } - // Changelog KOI8-R ; - // (issue #3681). KOI8-R . + // Changelog лежит на диске в KOI8-R и читается потоком; границу кодировки проходим здесь + // (issue #3681). Под KOI8-R это тождество. buffer = native_text::from_disk_line(buffer.c_str()); ++line; diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 3fb85bdd7..a1834d206 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -41,21 +41,21 @@ extern int invalid_anti_class(CharData *ch, const ObjData *obj); namespace Parcel { void parcel_log(const char *format, ...) __attribute__((format(printf, 1, 2))); -const int KEEP_TIMER = 60 * 24 * 3; // 3 ( ) -const int SEND_COST = 100; // -const int RESERVED_COST_COEFF = 3; // 3 -const int MAX_SLOTS = 25; // -const int RETURNED_TIMER = -1; // +const int KEEP_TIMER = 60 * 24 * 3; // 3 суток ждет на почте (в минутах) +const int SEND_COST = 100; // в любом случае снимается за посылку шмотки +const int RESERVED_COST_COEFF = 3; // цена ренты за 3 дня +const int MAX_SLOTS = 25; // сколько шмоток может находиться в отправке от одного игрока +const int RETURNED_TIMER = -1; // при развороте посылки идет двойной таймер шмоток без капания ренты const char *FILE_NAME = LIB_USERDATA"parcel.db"; -// +// для возврата посылки отправителю const bool RETURN_WITH_MONEY = 1; const bool RETURN_NO_MONEY = 0; -// +// доставленные с ребута посылки static int was_sended = 0; -// ( ) +// для групповой отсылки шмоток (чтобы не спамить на каждую) static std::string send_buffer; static int send_cost_buffer = 0; static int send_reserved_buffer = 0; @@ -64,10 +64,10 @@ class Node { public: Node(int money, const ObjData::shared_ptr &obj) : money_(money), timer_(0), obj_(obj) {}; Node() : money_(0), timer_(0), obj_(nullptr) {}; - int money_; // - int timer_; // ( KEEP_TIMER ) + int money_; // резервированные средства + int timer_; // сколько минут шмотка уже ждет получателя (при значении выще KEEP_TIMER возвращается отправителю) ObjData::shared_ptr - obj_; // ( , - ) + obj_; // шмотка (здесь же берется таймер, при уходе в ноль - пурж и возврат оставшегося резерва) }; class LoadNode { @@ -77,13 +77,13 @@ class LoadNode { long target; }; -typedef std::map > SenderListType; -typedef std::map ParcelListType; +typedef std::map > SenderListType; +typedef std::map ParcelListType; -ParcelListType parcel_list; // -SenderListType return_list; // +ParcelListType parcel_list; // список посылок +SenderListType return_list; // временный список на возврат -// * . +// * Отдельный лог для отладки. void parcel_log(const char *format, ...) { const auto filename = runtime_config.log_dir() + "/parcel.log"; static FILE *file = 0; @@ -106,18 +106,18 @@ void parcel_log(const char *format, ...) { fflush(file); } -// * ( ) . +// * Уведомление чара (если он онлайна) о новой посылке. void invoice(long uid) { DescriptorData *d = DescriptorByUid(uid); if (d) { if (!has_parcel(d->character.get())) { - SendMsgToChar(d->character.get(), "%s , !%s\r\n", + SendMsgToChar(d->character.get(), "%sВам пришла посылка, зайдите на почту и распишитесь!%s\r\n", kColorWht, kColorNrm); } } } -// * . +// * Добавление шмотки в список посылок. void add_parcel(long target, long sender, const Node &tmp_node) { invoice(target); ParcelListType::iterator it = parcel_list.find(target); @@ -139,7 +139,7 @@ void add_parcel(long target, long sender, const Node &tmp_node) { } } -// * ( ). +// * Сколько всего предметов уже посылается данным персонажем (для ограничения). int total_sended(CharData *ch) { int sended = 0; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -153,23 +153,23 @@ int total_sended(CharData *ch) { return sended; } -// * . +// * Проверка возможности отправить шмотку почтой. bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { if (obj->has_flag(EObjFlag::kNodrop) || obj->is_unrentable() || obj->has_flag(EObjFlag::kDecay) || obj->get_owner()) { - snprintf(buf, kMaxStringLength, "$n $g : '%s - !'\r\n", + snprintf(buf, kMaxStringLength, "$n сказал$g вам : '%s - мы не отправляем такие вещи!'\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; } else if (obj->get_type() == EObjType::kContainer && obj->get_contains()) { - snprintf(buf, kMaxStringLength, "$n $g : ' %s - .'\r\n", obj->get_PName(grammar::ECase::kPre).c_str()); + snprintf(buf, kMaxStringLength, "$n сказал$g вам : 'В %s что-то лежит.'\r\n", obj->get_PName(grammar::ECase::kPre).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; } else if (SetSystem::is_big_set(obj)) { - snprintf(buf, kMaxStringLength, "$n $g : '%s .'\r\n", + snprintf(buf, kMaxStringLength, "$n сказал$g вам : '%s является частью большого набора предметов.'\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); act(buf, false, mailman, 0, ch, kToVict); return 0; @@ -181,17 +181,17 @@ bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { if (invalid_anti_class(&t_vict, obj)) { switch ((&t_vict)->get_sex()) { case EGender::kMale: - act("$n $g : ' - .'\r\n", + act("$n сказал$g вам : 'Знаю я такого добра молодца - эта вещь явно на него не налезет.'\r\n", false, mailman, 0, ch, kToVict); break; case EGender::kFemale: - act("$n $g : ' - .'\r\n", + act("$n сказал$g вам : 'Знаю я такую красну девицу - эта вещь явно на нее не налезет.'\r\n", false, mailman, 0, ch, kToVict); break; default: - act("$n $g : ' - .'\r\n", + act("$n сказал$g вам : 'Знаю я сие чудо бесполое - эта вещь явно на него не налезет.'\r\n", false, mailman, 0, ch, kToVict); } return 0; @@ -199,7 +199,7 @@ bool can_send(CharData *ch, CharData *mailman, ObjData *obj, long vict_uid) { return 1; } -// , +// получаем все объекты, которые были отправлены чару std::vector get_objs(long char_uid) { std::vector buf_vector; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -213,10 +213,10 @@ std::vector get_objs(long char_uid) { return buf_vector; } -// * (/ , ). +// * Отправка предмета (снятие/резервирование денег, вывод из списка предметов). void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { if (!ch || !mailman || !vict_uid || !obj) { - log("Parcel: : %d, %d, %d, %d (%s %s %d)", + log("Parcel: нулевой входной параметр: %d, %d, %d, %d (%s %s %d)", ch ? 1 : 0, mailman ? 1 : 0, vict_uid ? 1 : 0, obj ? 1 : 0, __FILE__, __func__, __LINE__); return; } @@ -227,30 +227,30 @@ void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { const int total_cost = reserved_cost + SEND_COST; if (currencies::GetTotal(*ch, currencies::kGold) < total_cost) { - act("$n $g : ' !'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'Да у тебя ведь нет столько денег!'", false, mailman, 0, ch, kToVict); return; } if (total_sended(ch) >= MAX_SLOTS) { - act("$n $g : ' ! , !'", + act("$n сказал$g вам : 'Ты уже и так отправил кучу вещей! Подожди, пока их получат адресаты!'", false, mailman, 0, ch, kToVict); return; } std::string name = GetNameByUnique(vict_uid); if (name.empty()) { - act("$n $g : ' , !'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'Ошибка в имени получателя, сообщите Богам!'", false, mailman, 0, ch, kToVict); return; } if (SetSystem::is_norent_set(ch, obj) && SetSystem::is_norent_set(GET_OBJ_VNUM(obj), get_objs(ch->get_uid()))) { - snprintf(buf, kMaxStringLength, "%s - .\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); + snprintf(buf, kMaxStringLength, "%s - требуется две и более вещи из набора.\r\n", obj->get_PName(grammar::ECase::kNom).c_str()); SendMsgToChar(utils::CAP(buf), ch); return; } name_convert(name); if (send_buffer.empty()) - send_buffer += ": " + name + ", :\r\n"; + send_buffer += "Адресат: " + name + ", отправлено:\r\n"; snprintf(buf, sizeof(buf), "%s%s%s\r\n", kColorWht, obj->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); send_buffer += buf; @@ -269,16 +269,16 @@ void send_object(CharData *ch, CharData *mailman, long vict_uid, ObjData *obj) { check_auction(nullptr, obj); } -// * , (' )'. +// * Отправка предмета, дергается из спешиала почты ('отправить имя предмет)'. void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (ch->IsNpc()) return; if (ch->get_uid() == vict_uid) { - act("$n $g : ' !'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'Не загружай понапрасну почту!'", false, mailman, 0, ch, kToVict); return; } if (NORENTABLE(ch)) { - act("$n $g : ' , !'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'Да у тебя руки по локоть в крови, проваливай!'", false, mailman, 0, ch, kToVict); return; } @@ -290,17 +290,17 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (is_number(tmp_arg)) { int amount = atoi(tmp_arg); - if (!strn_cmp("coin", tmp_arg2, 4) || !strn_cmp("", tmp_arg2, 5) || !str_cmp("", tmp_arg2)) { - act("$n $g : ' .'", + if (!strn_cmp("coin", tmp_arg2, 4) || !strn_cmp("кун", tmp_arg2, 5) || !str_cmp("денег", tmp_arg2)) { + act("$n сказал$g вам : 'Для перевода денег воспользуйтесь услугами банка.'", false, mailman, 0, ch, kToVict); return; - } else if (!str_cmp("", tmp_arg2) || !str_cmp("all", tmp_arg2)) { + } else if (!str_cmp("все", tmp_arg2) || !str_cmp("all", tmp_arg2)) { if (!ch->carrying) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас ведь ничего нет.\r\n", ch); return; } for (obj = ch->carrying; obj && amount; obj = next_obj) { @@ -309,9 +309,9 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { send_object(ch, mailman, vict_uid, obj); } } else if (!*tmp_arg2) { - SendMsgToChar(ch, " %d ?\r\n", amount); + SendMsgToChar(ch, "Чего %d вы хотите отправить?\r\n", amount); } else if (!(obj = get_obj_in_list_vis(ch, tmp_arg2, ch->carrying))) { - SendMsgToChar(ch, " '%s'.\r\n", tmp_arg2); + SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg2); } else { while (obj && amount--) { next_obj = get_obj_in_list_vis(ch, tmp_arg2, obj->get_next_content()); @@ -323,17 +323,17 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { int dotmode = find_all_dots(tmp_arg); if (dotmode == kFindIndiv) { if (!(obj = get_obj_in_list_vis(ch, tmp_arg, ch->carrying))) { - SendMsgToChar(ch, " '%s'.\r\n", tmp_arg); + SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg); return; } send_object(ch, mailman, vict_uid, obj); } else { if (dotmode == kFindAlldot && !*tmp_arg) { - SendMsgToChar(" \"\" ?\r\n", ch); + SendMsgToChar("Отправить \"все\" какого типа предметов?\r\n", ch); return; } if (!ch->carrying) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас ведь ничего нет.\r\n", ch); } else { bool has_items = false; for (obj = ch->carrying; obj; obj = next_obj) { @@ -346,13 +346,13 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { } } if (!has_items) - SendMsgToChar(ch, " '%s'.\r\n", tmp_arg); + SendMsgToChar(ch, "У вас нет '%s'.\r\n", tmp_arg); } } } if (!send_buffer.empty()) { - snprintf(buf, sizeof(buf), " %d %s %d %s 3 .\r\n", + snprintf(buf, sizeof(buf), "с вас удержано %d %s и еще %d %s зарезервировано на 3 дня хранения.\r\n", send_cost_buffer, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(send_cost_buffer, grammar::ECase::kNom).c_str(), send_reserved_buffer, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(send_reserved_buffer, grammar::ECase::kNom).c_str()); send_buffer += buf; @@ -364,10 +364,10 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { } } -// * (''). , . +// * Дергается из спешиала почты ('почта'). Распечатка отправленных посылок, которые еще не доставлены. void print_sending_stuff(CharData *ch) { std::stringstream out; - out << "\r\n :"; + out << "\r\nВаши текущие посылки:"; bool print = false; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { SenderListType::const_iterator it2 = it->second.find(ch->get_uid()); @@ -375,7 +375,7 @@ void print_sending_stuff(CharData *ch) { print = true; std::string name = GetNameByUnique(it->first); name_convert(name); - out << "\r\n: " << name << ", :\r\n" << kColorWht; + out << "\r\nАдресат: " << name << ", отправлено:\r\n" << kColorWht; int money = 0; for (std::list::const_iterator it3 = it2->second.begin(); it3 != it2->second.end(); ++it3) { @@ -383,14 +383,14 @@ void print_sending_stuff(CharData *ch) { money += it3->money_; } out << kColorNrm - << money << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str() << " 3 .\r\n"; + << money << " " << MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str() << " зарезервировано на 3 дня хранения.\r\n"; } } if (print) SendMsgToChar(out.str(), ch); } -// * . +// * Для учитывания предметов на почте в локейте. std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { for (auto i : parcel_list) { for (auto k : i.second) { @@ -404,7 +404,7 @@ std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { continue; } if (obj->get_id() == o.obj_->get_id()) { - return fmt::format("{} {} .\r\n", o.obj_->get_short_description().c_str(), grammar::ObjPluralVerbEnding((o.obj_)->get_sex())); + return fmt::format("{} наход{}ся у почтового голубя в инвентаре.\r\n", o.obj_->get_short_description().c_str(), grammar::ObjPluralVerbEnding((o.obj_)->get_sex())); } } } @@ -412,7 +412,7 @@ std::string PrintSpellLocateObject(CharData *ch, ObjData *obj) { return {}; } -// * - . +// * Есть ли на чара какие-нить посылки. bool has_parcel(CharData *ch) { ParcelListType::const_iterator it = parcel_list.find(ch->get_uid()); if (it != parcel_list.end()) @@ -421,7 +421,7 @@ bool has_parcel(CharData *ch) { return false; } -// * . +// * Возврат зарезервированных денег отправителю. void return_money(std::string const &name, int money, bool add) { if (money <= 0) { log("WARNING: money=%d (%s %s %d)", money, __FILE__, __func__, __LINE__); @@ -432,11 +432,11 @@ void return_money(std::string const &name, int money, bool add) { if ((vict = get_player_of_name(name.c_str()))) { if (add) { currencies::AddBank(*vict, currencies::kGold, money); - SendMsgToChar(vict, "%s %d %s %s.\r\n", + SendMsgToChar(vict, "%sВы получили %d %s банковским переводом от почтовой службы%s.\r\n", kColorWht, money, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(money, grammar::ECase::kNom).c_str(), kColorNrm); } } else { - vict = new Player; // TODO: + vict = new Player; // TODO: переделать на стек if (LoadPlayerCharacter(name.c_str(), vict, ELoadCharFlags::kFindId) < 0) { delete vict; return; @@ -447,41 +447,41 @@ void return_money(std::string const &name, int money, bool add) { } } -// * - . +// * Экстра-описание на самой посылке при получении. void fill_ex_desc(CharData *ch, ObjData *obj, std::string sender) { size_t size = std::max(strlen(GET_NAME(ch)), sender.size()); std::stringstream out; out.setf(std::ios_base::left); - out << " \r\n" - " - ...\r\n" - " :\r\n\r\n"; + out << " Разваливающийся на глазах ящик с явными признаками\r\n" + "неуклюжего взлома - царская служба безопасности бдит...\r\n" + "На табличке сбоку видны надписи:\r\n\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; - out << "| : " << fmt::format("{:>{}}", sender, size) - << " |\r\n| : " << fmt::format("{:>{}}", GET_NAME(ch), size) << " |\r\n"; + out << "| Отправитель: " << fmt::format("{:>{}}", sender, size) + << " |\r\n| Получатель: " << fmt::format("{:>{}}", GET_NAME(ch), size) << " |\r\n"; out << std::setw(size + 16) << std::setfill('-') << " " << std::setfill(' ') << "\r\n"; - obj->set_ex_description(" parcel box case chest", out.str().c_str()); + obj->set_ex_description("посылка бандероль пакет ящик parcel box case chest", out.str().c_str()); } -// * , . +// * Расчет стоимости ренты за предмет, пока он лежал на почте. int calculate_timer_cost(std::list::iterator const &it) { return static_cast((get_object_low_rent(it->obj_.get()) / (24.0 * 60.0)) * it->timer_); } -// * . +// * Генерим сам контейнер посылку. ObjData *create_parcel() { const auto obj = world_objects.create_blank(); - obj->set_aliases(" parcel box case chest"); - obj->set_short_description(""); - obj->set_description("- ."); - obj->set_PName(grammar::ECase::kNom, ""); - obj->set_PName(grammar::ECase::kGen, ""); - obj->set_PName(grammar::ECase::kDat, ""); - obj->set_PName(grammar::ECase::kAcc, ""); - obj->set_PName(grammar::ECase::kIns, ""); - obj->set_PName(grammar::ECase::kPre, ""); + obj->set_aliases("посылка бандероль пакет ящик parcel box case chest"); + obj->set_short_description("посылка"); + obj->set_description("Кто-то забыл здесь свою посылку."); + obj->set_PName(grammar::ECase::kNom, "посылка"); + obj->set_PName(grammar::ECase::kGen, "посылки"); + obj->set_PName(grammar::ECase::kDat, "посылке"); + obj->set_PName(grammar::ECase::kAcc, "посылку"); + obj->set_PName(grammar::ECase::kIns, "посылкой"); + obj->set_PName(grammar::ECase::kPre, "посылке"); obj->set_sex(EGender::kFemale); obj->set_type(EObjType::kContainer); obj->set_wear_flags(to_underlying(EWearFlag::kTake)); @@ -496,13 +496,13 @@ ObjData *create_parcel() { return obj.get(); } -// * , . (''). +// * Получение посылки на почте, дергается из спешиала почты. ('получить'). void receive(CharData *ch, CharData *mailman) { if (((ch->in_room == r_helled_start_room) || (ch->in_room == r_named_start_room) || (ch->in_room == r_unreg_start_room)) && has_parcel(ch)) { - act("$n $g : ' - , .'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'А посылку-то не получишь, сюда не доставляем.'", false, mailman, 0, ch, kToVict); return; } @@ -518,15 +518,15 @@ void receive(CharData *ch, CharData *mailman) { int money = 0; for (std::list::iterator it3 = it2->second.begin(); it3 != it2->second.end(); ++it3) { money += it3->money_ - calculate_timer_cost(it3); - // . + // добавляем в глоб.список и кладем в посылку PlaceObjIntoObj(it3->obj_.get(), obj); } return_money(name, money, RETURN_WITH_MONEY); PlaceObjToInventory(obj, ch); - snprintf(buf, kMaxStringLength, "$n $g ( %s).", name.c_str()); + snprintf(buf, kMaxStringLength, "$n дал$g вам посылку (отправитель %s).", name.c_str()); act(buf, false, mailman, 0, ch, kToVict); - act("$N $G $n2 .", false, ch, 0, mailman, kToRoom); + act("$N дал$G $n2 посылку.", false, ch, 0, mailman, kToRoom); ++was_sended; } ObjSaveSync::add(ch->get_uid(), ch->get_uid(), ObjSaveSync::PARCEL_SAVE); @@ -534,17 +534,17 @@ void receive(CharData *ch, CharData *mailman) { } } -// * , , . +// * Отправка сообщения через письмо, с уведомлением чару, если тот онлайн. void create_mail(int to_uid, int from_uid, char *text) { mail::add(to_uid, from_uid, text); const DescriptorData *i = DescriptorByUid(to_uid); if (i) { - SendMsgToChar(i->character.get(), "%s , !%s\r\n", + SendMsgToChar(i->character.get(), "%sВам пришло письмо, зайдите на почту и распишитесь!%s\r\n", kColorWht, kColorNrm); } } -// * ( ). +// * Формирование временного списка возвращенных предметов (из основного удалены). void prepare_return(const long uid, const std::list::iterator &it) { Node tmp_node(0, it->obj_); tmp_node.timer_ = RETURNED_TIMER; @@ -559,7 +559,7 @@ void prepare_return(const long uid, const std::list::iterator &it) { } } -// * ( ). +// * Возврат предметов из временного списка (перекидывание их в основной список посылок). void return_parcel() { for (SenderListType::iterator it = return_list.begin(); it != return_list.end(); ++it) { for (std::list::iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { @@ -571,19 +571,19 @@ void return_parcel() { return_list.clear(); } -// * . +// * Дикей предмета на почте и уведомление об этом отправителя и получателя через письма. void extract_parcel(int sender_uid, int target_uid, const std::list::iterator &it) { - snprintf(buf, kMaxStringLength, " : %s %s .\r\n", + snprintf(buf, kMaxStringLength, "С прискорбием сообщаем вам: %s рассыпал%s в прах.\r\n", it->obj_->get_short_description().c_str(), grammar::ObjSexEnding((it->obj_)->get_sex(), 2)); char *tmp = str_dup(buf); - // -1 + // -1 в качестве ид отправителя при получении подставит в имя почтовую службу create_mail(sender_uid, -1, tmp); create_mail(target_uid, -1, tmp); free(tmp); - // ( ) + // возврат оставшихся зарезервированных кун отправителю (у развернутых уже ноль) if (it->money_ && it->timer_ != RETURNED_TIMER) { int money_return = it->money_ - calculate_timer_cost(it); std::string name = GetNameByUnique(sender_uid); @@ -593,31 +593,31 @@ void extract_parcel(int sender_uid, int target_uid, const std::list::itera ExtractObjFromWorld(it->obj_.get()); } -// * . +// * Генерация письма о возврате посылки. void return_invoice(int uid, ObjData *obj) { - snprintf(buf, kMaxStringLength, " : %s.\r\n", + snprintf(buf, kMaxStringLength, "Посылка возвращена отправителю: %s.\r\n", obj->get_short_description().c_str()); char *tmp = str_dup(buf); create_mail(uid, -1, tmp); free(tmp); } -// * , . +// * Чтение записи, включающей инфу почты и сам предмет после нее. LoadNode parcel_read_one_object(char **data, int *error) { LoadNode tmp_node; *error = 1; - // (#) + // Станем на начало предмета (#) for (; **data != '#'; (*data)++) if (!**data || **data == '$') return tmp_node; - // # + // Пропустим # (*data)++; char buffer[kMaxStringLength]; *error = 2; - // + // отправитель if (!get_buf_line(data, buffer)) return tmp_node; *error = 3; @@ -625,7 +625,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 4; - // + // получатель if (!get_buf_line(data, buffer)) return tmp_node; *error = 5; @@ -633,7 +633,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 6; - // + // зарезервированная сумма if (!get_buf_line(data, buffer)) return tmp_node; *error = 7; @@ -641,7 +641,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; *error = 8; - // + // таймер ожидания на почте if (!get_buf_line(data, buffer)) return tmp_node; *error = 9; @@ -657,7 +657,7 @@ LoadNode parcel_read_one_object(char **data, int *error) { return tmp_node; } -// * . ( ). +// * Загрузка предметов и тех.информации (при ребуте). void load() { FILE *fl; if (!(fl = fopen(FILE_NAME, "r"))) { @@ -696,13 +696,13 @@ void load() { return; } add_parcel(node.target, node.sender, node.obj_node); - // + // из глобального списка изымаем } free(readdata); } -// * . ( ). +// * Сохранение предметов и тех.информации (при апдейте таймеров). void save() { log("Save obj: parcel"); ObjSaveSync::check(0, ObjSaveSync::PARCEL_SAVE); @@ -719,7 +719,7 @@ void save() { } out << "$\n$\n"; - // + // скидываем в файл std::ofstream file(FILE_NAME); if (!file.is_open()) { log("SYSERR: error opening file: %s! (%s %s %d)", FILE_NAME, __FILE__, __func__, __LINE__); @@ -731,7 +731,7 @@ void save() { return; } -// * + . / . +// * Обновление таймеров у предметов + таймеров ожидания на почте. Пурж/возврат по надобности. void update_timers() { for (ParcelListType::iterator it = parcel_list.begin(); it != parcel_list.end(); /* empty */) { for (SenderListType::iterator it2 = it->second.begin(); it2 != it->second.end(); /* empty */) { @@ -739,19 +739,19 @@ void update_timers() { for (std::list::iterator it3 = it2->second.begin(); it3 != it2->second.end(); it3 = tmp_it) { tmp_it = it3; ++tmp_it; - // ObjDecayManager::process_tick ԣ - // decay_manager, shared_ptr - // . ObjData::get_timer() - // - ( > 0), - // timer == 0 -- - // #3239 . + // После ObjDecayManager::process_tick объекты с истёкшим + // дедлайном удаляются из индексов decay_manager, но shared_ptr + // здесь продолжает их держать. ObjData::get_timer() в этом + // случае возвращает прото-таймер (обычно > 0), и проверка + // timer == 0 такие объекты пропускает -- симптоматика ровно + // как в #3239 для депо. const bool expired_by_decay = !world_objects.decay_manager().contains(it3->obj_.get()); if (expired_by_decay || it3->obj_->get_timer() == 0) { extract_parcel(it2->first, it->first, it3); it2->second.erase(it3); } else { if (it3->timer_ == RETURNED_TIMER) { - // , , + // шмотка уже развернута отправителю, рента не капает, но таймер идет два раза if (it3->obj_->get_timer() > 1) it3->obj_->dec_timer(); if (it3->obj_->get_timer() == 0) { @@ -763,10 +763,10 @@ void update_timers() { if (it3->timer_ >= KEEP_TIMER) { return_invoice(it->first, it3->obj_.get()); prepare_return(it2->first, it3); - // + // тут надо штатно уменьшить счетчики у плеера std::string name = GetNameByUnique(it2->first); return_money(name, it3->money_, RETURN_NO_MONEY); - // ( return_parcel) + // и удалить запись (она уйдет в разворот позже в return_parcel) it2->second.erase(it3); } } @@ -786,7 +786,7 @@ void update_timers() { save(); } -// * 'show stats' . +// * Иммский 'show stats' интересу и статистики ради. void show_stats(CharData *ch) { int targets = 0, returned = 0, objs = 0, reserved_money = 0; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -800,7 +800,7 @@ void show_stats(CharData *ch) { } } } - SendMsgToChar(ch, " : %d, %d\r\n", objs, was_sended); + SendMsgToChar(ch, " Почта: предметов в ожидании %d, доставлено с ребута %d\r\n", objs, was_sended); } int delete_obj(int vnum) { @@ -818,7 +818,7 @@ int delete_obj(int vnum) { return num; } -// * '' . +// * Иммское 'где' для учета предметов на почте. bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { bool found = false; for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { @@ -829,7 +829,7 @@ bool print_imm_where_obj(CharData *ch, const ObjData *arg, int num) { std::string sender = GetNameByUnique(it2->first); found = true; - SendMsgToChar(fmt::format("{:2}. [{:6}] {:<25} - {} (: {}, : {}).\r\n", + SendMsgToChar(fmt::format("{:2}. [{:6}] {:<25} - наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", num++, GET_OBJ_VNUM(it3->obj_.get()), it3->obj_->get_short_description(), @@ -855,7 +855,7 @@ std::string FindParcelObj(const ObjData *obj) { native_text::capitalize_first(target); native_text::capitalize_first(sender); - str = fmt::format("{} (: {}, : {}).\r\n", + str = fmt::format("наход{}ся на почте (отправитель: {}, получатель: {}).\r\n", grammar::ObjPluralVerbEnding((it3->obj_)->get_sex()), sender.c_str(), target.c_str()); @@ -867,7 +867,7 @@ std::string FindParcelObj(const ObjData *obj) { return ""; } -// * . +// * Обновление полей объектов при изменении их прототипа через олц. void olc_update_from_proto(int robj_num, ObjData *olc_proto) { for (ParcelListType::const_iterator it = parcel_list.begin(); it != parcel_list.end(); ++it) { for (SenderListType::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { @@ -880,7 +880,7 @@ void olc_update_from_proto(int robj_num, ObjData *olc_proto) { } } -// * . +// * Поиск цели для каста локейта. ObjData *locate_object(const char *str) { for (ParcelListType::const_iterator i = parcel_list.begin(); i != parcel_list.end(); ++i) { for (SenderListType::const_iterator k = i->second.begin(); k != i->second.end(); ++k) { @@ -894,7 +894,7 @@ ObjData *locate_object(const char *str) { return nullptr; } -// * . +// * Возврат всех ждущих посылок их отправителю. void bring_back(CharData *ch, CharData *mailman) { int money = 0; bool empty = true; @@ -906,15 +906,15 @@ void bring_back(CharData *ch, CharData *mailman) { } empty = false; ObjData *obj = create_parcel(); - fill_ex_desc(ch, obj, std::string(" ")); + fill_ex_desc(ch, obj, std::string("Отдел возвратов")); for (std::list::iterator l = k->second.begin(); l != k->second.end(); ++l) { money += l->money_ - calculate_timer_cost(l); PlaceObjIntoObj(l->obj_.get(), obj); } PlaceObjToInventory(obj, ch); - snprintf(buf, kMaxStringLength, "$n $g ."); + snprintf(buf, kMaxStringLength, "$n дал$g вам посылку."); act(buf, false, mailman, 0, ch, kToVict); - act("$N $G $n2 .", false, ch, 0, mailman, kToRoom); + act("$N дал$G $n2 посылку.", false, ch, 0, mailman, kToRoom); i->second.erase(k); if (i->second.empty()) { @@ -924,13 +924,13 @@ void bring_back(CharData *ch, CharData *mailman) { } } if (!empty && money > 0) { - act("$n $g : ' .'", + act("$n сказал$g вам : 'За экстренный возврат посылок с вас удержана половина зарезервированных кун.'", false, mailman, 0, ch, kToVict); std::string name = GET_NAME(ch); return_money(name, money / 2, RETURN_WITH_MONEY); ObjSaveSync::add(ch->get_uid(), ch->get_uid(), ObjSaveSync::PARCEL_SAVE); } else if (empty) { - act("$n $g : ' !'", false, mailman, 0, ch, kToVict); + act("$n сказал$g вам : 'У нас нет ни одной вашей посылки!'", false, mailman, 0, ch, kToVict); } } diff --git a/src/gameplay/crafting/item_creation.cpp b/src/gameplay/crafting/item_creation.cpp index 246f3c0b3..6c8bfe51c 100644 --- a/src/gameplay/crafting/item_creation.cpp +++ b/src/gameplay/crafting/item_creation.cpp @@ -58,99 +58,99 @@ struct create_item_type created_item[] = {310, 0x3A, 4, 10, {{COAL_PROTO, 0, 0}}, ESkill::kReforging, WEAR_TAKE_HEAD}, {312, 0, 4, 40, {{WOOD_PROTO, TETIVA_PROTO, 0}}, ESkill::kCreateBow, WEAR_TAKE_BOTHS} }; -const char *create_item_name[] = {"", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", +const char *create_item_name[] = {"шелепуга", + "меч", + "сабля", + "нож", + "топор", + "плеть", + "дубина", + "кольчуга", + "наручи", + "поножи", + "шлем", + "лук", "\n" }; const struct make_skill_type make_skills[] = { - {" ", "", ESkill::kMakeStaff}, - {" ", "", ESkill::kMakeBow}, - {" ", "", ESkill::kMakeWeapon}, - {" ", "", ESkill::kMakeArmor}, - {" ", "", ESkill::kMakeWear}, - {" ", ".", ESkill::kMakeJewel}, - {" ", "", ESkill::kMakeAmulet}, -// { " ","", ESkill::kMakePotion }, - {"\n", "\n", ESkill::kUndefined} // + {"смастерить предмет", "предметы", ESkill::kMakeStaff}, + {"смастерить лук", "луки", ESkill::kMakeBow}, + {"выковать оружие", "оружие", ESkill::kMakeWeapon}, + {"выковать доспех", "доспех", ESkill::kMakeArmor}, + {"сшить одежду", "одежда", ESkill::kMakeWear}, + {"смастерить диковину", "артеф.", ESkill::kMakeJewel}, + {"смастерить оберег", "оберег", ESkill::kMakeAmulet}, +// { "сварить отвар","варево", ESkill::kMakePotion }, + {"\n", "\n", ESkill::kUndefined} // Терминатор }; const char *create_weapon_quality[] = {"RESERVED", "RESERVED", "RESERVED", "RESERVED", "RESERVED", - " ", // <3 // - " ", // 3 // - " ", - " ", // 4 // - " ", - " ", // 5 // - " ", - " ", // 6 // - " ", - " ", // 7 // - " ", - " ", // 8 // - " ", - " ", // 9 // - " ", - " ", // 10 // - " ", - " ", // 11 // - " ", - ", ", // 12 // - " , ", - ", ", // 13 // - " , ", - ", ", // 14 // - " , ", - ", ", // 15 // - ", ", - ", ", // 16 // - ", ", - ", ", // 17 // - ", ", - ", ", // 18 // - ", ", - ", ", // 19 // - ", ", - ", ", // 20 // - ", ", - ", ", // 21 // - ", ", - ", ", // 22 // - ", ", - ", ", // 23 // - ", , ", - ", ", // 24 // - ", ", - ", ", // 25 // - ", ", - ", ", // 26 - ", ", - ", ", // 27 - ", ", - ", ", // >= 28 + "невообразимо ужасного качества", // <3 // + "ужасного качества", // 3 // + "более чем жуткого качества", + "жуткого качества", // 4 // + "более чем отвратительного качества", + "отвратительного качества", // 5 // + "более чем плохого качества", + "плохого качества", // 6 // + "хуже чем среднего качества", + "среднего качества", // 7 // + "лучше чем среднего качества", + "неплохого качества", // 8 // + "более чем неплохого качества", + "хорошего качества", // 9 // + "более чем хорошего качества", + "превосходного качества", // 10 // + "более чем превосходного качества", + "великолепного качества", // 11 // + "более чем великолепного качества", + "качества, достойного учеников мастера", // 12 // + "лучшего качества, чем делают ученики мастера", + "качества, достойного мастера", // 13 // + "лучшего качества, чем делают мастера", + "качества, достойного великого мастера", // 14 // + "лучшего качества, чем делают великие мастера", + "качества, достойного руки сына боярского", // 15 // + "качества, более чем достойного руки сына боярского", + "качества, достойного руки боярина", // 16 // + "качества, более чем достойного руки боярина", + "качества, достойного руки сына воеводы", // 17 // + "качества, более чем достойного руки сына воеводы", + "качества, достойного руки воеводы", // 18 // + "качества, более чем достойного руки воеводы", + "качества, достойного руки княжича", // 19 // + "качества, более чем достойного руки княжича", + "качества, достойного руки князя", // 20 // + "качества, более чем достойного руки князя", + "качества, достойного руки героя", // 21 // + "качества, более чем достойного руки героя", + "качества, достойного руки великого героя", // 22 // + "качества, более чем достойного руки великого героя", + "качества, которое знали только древние титаны", // 23 // + "качества, лучшего чем то, которое знали древние титаны", + "качества, достойного младших богов", // 24 // + "качества, более чем достойного младших богов", + "качества, достойного богов", // 25 // + "качества, более чем достойного богов", + "качества, которого достигают немногие", // 26 + "качества, которого достигают немногие", + "качества, которого достигают немногие", // 27 + "качества, которого достигают немногие", + "качества, достойного старших богов", // >= 28 "\n" }; MakeReceptList make_recepts; -// // +// Функция вывода в поток // CharData *&operator<<(CharData *&ch, const string &p) { SendMsgToChar(p, ch); return ch; } -// DataNode; def / . +// Целочисленный атрибут DataNode; def при отсутствии/некорректном значении. static int RecipeAttrInt(parser_wrapper::DataNode &node, const char *key, int def) { const char *v = node.GetValue(key); if (!v || !*v) { @@ -163,7 +163,7 @@ static int RecipeAttrInt(parser_wrapper::DataNode &node, const char *key, int de } } -// cfg/craft/item_creation.xml cfg_manager (boot + reload makeitems). +// Чтение cfg/craft/item_creation.xml через cfg_manager (boot + reload makeitems). void ItemCreationLoader::Load(parser_wrapper::DataNode data) { make_recepts.clear(); for (auto &rnode : data.Children("recipe")) { @@ -209,7 +209,7 @@ void ItemCreationLoader::Reload(parser_wrapper::DataNode data) { Load(std::move(data)); } -// () vnum , 1. +// Наименьший свободный (неиспользуемый) vnum рецепта, начиная с 1. static int FirstFreeRecipeVnum() { std::set used; for (size_t i = 0; i < make_recepts.size(); ++i) { @@ -238,7 +238,7 @@ std::vector ItemCreationLoader::ListElements() con } cfg_manager::ValidationResult ItemCreationLoader::Validate(parser_wrapper::DataNode &doc) const { - // vnum' . + // Проверяем валидность умений и УНИКАЛЬНОСТЬ vnum'ов рецептов. std::set seen; for (auto &node : doc.Children()) { if (std::string(node.GetName()) != "recipe") { @@ -269,9 +269,9 @@ cfg_manager::ValidationResult ItemCreationLoader::Validate(parser_wrapper::DataN parser_wrapper::DataNode ItemCreationLoader::FindElementNode(parser_wrapper::DataNode root, const std::string &id) const { - // vnum ( `id`-). - // ( Children("recipe")): , , - // Children() . + // Рецепт опознается по целочисленному vnum (не по `id`-атрибуту). Идем по всем детям с проверкой + // имени тега (а не Children("recipe")): узел, скопированный из отфильтрованного диапазона, унес бы + // фильтр и сломал бы свой собственный Children() ниже. for (auto &child : root.Children()) { if (std::string(child.GetName()) == "recipe" && id == child.GetValue("vnum")) { return child; @@ -284,12 +284,12 @@ std::string ItemCreationLoader::CanonicalElementId(const std::string &id) const if (id.empty()) { return ""; } - // "new"/"auto"/"next" -- vnum. + // "new"/"auto"/"next" -- создать рецепт с первым свободным vnum. if (id == "new" || id == "auto" || id == "next") { return std::to_string(FirstFreeRecipeVnum()); } - // ( vnum - // -- FindElementNode ). + // Иначе ключом может быть только неотрицательное целое (свободный vnum для нового рецепта или + // существующий -- последний обрабатывается через FindElementNode как редактирование). for (const char c : id) { if (c < '0' || c > '9') { return ""; @@ -312,17 +312,17 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ char tmpbuf[kMaxInputLength]; MakeRecept *trec; if (make_recepts.size() == 0) { - SendMsgToChar(" .", ch); + SendMsgToChar("Рецепты в этом мире не определены.", ch); return; } - // . - tmpstr = "### : \r\n"; + // Выдаем список рецептов всех рецептов как в магазине. + tmpstr = "### Б Умение Предмет Составляющие: \r\n"; tmpstr += "-------------------------------------------------------------------------------------------------------------------------------------------------------\r\n"; for (size_t i = 0; i < make_recepts.size(); i++) { int j = 0; - skill_name = ""; - obj_name = ""; + skill_name = "Нет"; + obj_name = "Нет"; trec = make_recepts[i]; auto obj = GetObjectPrototype(trec->obj_proto); if (obj) { @@ -344,12 +344,12 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ if (obj) { obj_name = utils::RemoveColors(obj->get_PName(grammar::ECase::kNom).substr(0, native_text::char_offset(obj->get_PName(grammar::ECase::kNom), 34))); } else { - obj_name = ""; + obj_name = "Нет"; } strcpy(tmpbuf, fmt::format(" {:<35}({:5})", obj_name, trec->parts[j].proto).c_str()); if (j > 0) { if (j % 2 == 0) { - // 2; + // разбиваем строчки если ингров больше 2; tmpstr += "\r\n"; tmpstr += " ->"; } @@ -364,18 +364,18 @@ void do_list_make(CharData *ch, char * /*argument*/, int/* cmd*/, int/* subcmd*/ page_string(ch->desc, tmpstr); return; } -// . +// Создание любого предмета из компонента. void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { - // . - // - // - // , , ( ) - // make - // () . - // - // + // Тут творим предмет. + // Если прислали без параметра то выводим список всех рецептов + // доступных для изготовления персонажу из его ингров + // Мастерить можно лук, посох , и диковину(аналог артефакта) + // суб команда make + // Выковать можно клинок и доспех (щит) умения разные. название одно + // Сварить отвар + // Сшить одежду if ((subcmd == MAKE_WEAR) && (!GetSkill(ch, ESkill::kMakeWear))) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вас этому никто не научил.\r\n", ch); return; } string tmpstr; @@ -384,23 +384,23 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { char tmpbuf[kMaxInputLength]; //int used_skill = subcmd; argument = one_argument(argument, tmpbuf); - // ... + // Разбираем в зависимости от того что набрали ... список объектов switch (subcmd) { case (MAKE_POTION): - // . - tmpstr = " :\r\n"; + // Варим отвар. + tmpstr = "Вы можете сварить:\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakePotion); break; case (MAKE_WEAR): - // . - tmpstr = " :\r\n"; + // Шьем одежку. + tmpstr = "Вы можете сшить:\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeWear); break; - case (MAKE_METALL): tmpstr = " :\r\n"; + case (MAKE_METALL): tmpstr = "Вы можете выковать:\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeWeapon); make_recepts.can_make(ch, &canlist, ESkill::kMakeArmor); break; - case (MAKE_CRAFT): tmpstr = " :\r\n"; + case (MAKE_CRAFT): tmpstr = "Вы можете смастерить:\r\n"; make_recepts.can_make(ch, &canlist, ESkill::kMakeStaff); make_recepts.can_make(ch, &canlist, ESkill::kMakeBow); make_recepts.can_make(ch, &canlist, ESkill::kMakeJewel); @@ -408,12 +408,12 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { break; } if (canlist.size() == 0) { - // . - SendMsgToChar(" .\r\n", ch); + // Чар не может сделать ничего. + SendMsgToChar("Вы ничего не можете сделать.\r\n", ch); return; } if (!*tmpbuf) { - // . + // Выводим тут список предметов которые можем сделать. for (size_t i = 0; i < canlist.size(); i++) { auto tobj = GetObjectPrototype(canlist[i]->obj_proto); if (!tobj) @@ -424,7 +424,7 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { SendMsgToChar(tmpstr.c_str(), ch); return; } - // , . + // Адресуемся по списку либо по номеру, либо по названию с номером. tmpstr = string(tmpbuf); size_t i = atoi(tmpbuf); if ((i > 0) && (i <= canlist.size()) @@ -433,7 +433,7 @@ void do_make_item(CharData *ch, char *argument, int/* cmd*/, int subcmd) { } else { trec = canlist.get_by_name(tmpstr); if (trec == nullptr) { - tmpstr = ", .\r\n"; + tmpstr = "Похоже, у вас творческий кризис.\r\n"; SendMsgToChar(tmpstr.c_str(), ch); return; } @@ -461,23 +461,23 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { weight = MIN(obj->get_weight() - 2, obj->get_weight() * prob / percent); } if (weight < created_item[obj_type].min_weight) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас не хватило материала.\r\n", ch); } else if (prob * 5 < percent) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас ничего не получилось.\r\n", ch); } else { const auto tobj = world_objects.create_from_prototype_by_vnum(created_item[obj_type].obj_vnum); if (!tobj) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Образец был невозвратимо утерян.\r\n", ch); } else { tobj->set_weight(MIN(weight, created_item[obj_type].max_weight)); tobj->set_cost(2 * obj->get_cost() / 3); tobj->set_owner(ch->get_uid()); tobj->set_extra_flag(EObjFlag::kTransformed); - // . - // 5+ 3 : 2% 0.5% - // 2 5%, 1% - // 1 20% 4% - // . . . + // ковка объектов со слотами. + // для 5+ мортов имеем шанс сковать стаф с 3 слотами: базовый 2% и по 0.5% за морт + // для 2 слотов базовый шанс 5%, 1% за каждый морт + // для 1 слота базово 20% и 4% за каждый морт + // Карачун. Поправлено. Расчет не через морты а через скил. if (skill == ESkill::kReforging) { if (GetSkill(ch, skill) >= 105 && number(1, 100) <= 2 + (GetSkill(ch, skill) - 105) / 10) { tobj->set_extra_flag(EObjFlag::kHasThreeSlots); @@ -494,21 +494,21 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { case 3: case 4: case 11: { - // . . - // MAX(<>, <>/100*< >-< 0 25% >) - // , + // Карачун. Таймер должен зависить от таймера прототипа. + // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) + // В минимуме один день реала, в максимуме таймер из прототипа const int timer_value = tobj->get_timer() / 100 * GetSkill(ch, skill) - number(0, tobj->get_timer() / 100 * 25); const int timer = MAX(ObjData::ONE_DAY, timer_value); tobj->set_timer(timer); - sprintf(buf, " %d \n", tobj->get_timer() / 24 / 60); + sprintf(buf, "Ваше изделие продержится примерно %d дней\n", tobj->get_timer() / 24 / 60); act(buf, false, ch, tobj.get(), 0, kToChar); tobj->set_material(obj->get_material()); - // . . - // tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); - // MAX(<>, <>/100*< >-< 0 25% >) - // 100, 100. . + // Карачун. Так логичнее. + // было tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); + // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) + // при расчете числа умножены на 100, перед приравниванием делятся на 100. Для не потерять десятые. tobj->set_maximum_durability( MAX(20000, 35000 / 100 * GetSkill(ch, skill) - number(0, 35000 / 100 * 25)) / 100); tobj->set_current_durability(tobj->get_maximum_durability()); @@ -522,24 +522,24 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { sdice += tobj->get_weight() / 10; tobj->set_val(1, ndice); tobj->set_val(2, sdice); - // tobj->set_wear_flags(created_item[obj_type].wear); wear + // tobj->set_wear_flags(created_item[obj_type].wear); пусть wear флаги будут из прототипа if (skill != ESkill::kCreateBow) { if (tobj->get_weight() < 14 && percent * 4 > prob) { tobj->set_wear_flag(EWearFlag::kHold); } - to_room = "$n $g $o3."; + to_room = "$n выковал$g $o3."; average = (((float) sdice + 1) * (float) ndice / 2.0); if (average < 3.0) { - sprintf(txtbuff, " $o3 %s.", create_weapon_quality[(int) (2.5 * 2)]); + sprintf(txtbuff, "Вы выковали $o3 %s.", create_weapon_quality[(int) (2.5 * 2)]); } else if (average <= 27.5) { - sprintf(txtbuff, " $o3 %s.", create_weapon_quality[(int) (average * 2)]); + sprintf(txtbuff, "Вы выковали $o3 %s.", create_weapon_quality[(int) (average * 2)]); } else { - sprintf(txtbuff, " $o3 %s!", create_weapon_quality[56]); + sprintf(txtbuff, "Вы выковали $o3 %s!", create_weapon_quality[56]); } to_char = (char *) txtbuff; } else { - to_room = "$n $g $o3."; - to_char = " $o3."; + to_room = "$n смастерил$g $o3."; + to_char = "Вы смастерили $o3."; } break; } @@ -557,28 +557,28 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { tobj->set_extra_flag(EObjFlag::kDecay); tobj->set_extra_flag(EObjFlag::kNosell); tobj->set_wear_flags(created_item[obj_type].wear); - to_room = "$n $g $o3."; - to_char = " $o3."; + to_room = "$n создал$g $o3."; + to_char = "Вы создали $o3."; break; case 7: // smith armor case 8: case 9: case 10: { - // . . - // MAX(<>, <>/100*< >-< 0 25% >) - // , + // Карачун. Таймер должен зависить от таймера прототипа. + // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) + // В минимуме один день реала, в максимуме таймер из прототипа const int timer_value = tobj->get_timer() / 100 * GetSkill(ch, skill) - number(0, tobj->get_timer() / 100 * 25); const int timer = MAX(ObjData::ONE_DAY, timer_value); tobj->set_timer(timer); - sprintf(buf, " %d \n", tobj->get_timer() / 24 / 60); + sprintf(buf, "Ваше изделие продержится примерно %d дней\n", tobj->get_timer() / 24 / 60); act(buf, false, ch, tobj.get(), 0, kToChar); tobj->set_material(obj->get_material()); - // . . - // tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); - // MAX(<>, <>/100*< >-< 0 25% >) - // 100, 100. . + // Карачун. Так логичнее. + // было tobj->get_maximum_durability() = MAX(50, MIN(300, 300 * prob / percent)); + // Формула MAX(<минимум>, <максимум>/100*<процент скила>-<рандом от 0 до 25% максимума>) + // при расчете числа умножены на 100, перед приравниванием делятся на 100. Для не потерять десятые. tobj->set_maximum_durability( MAX(20000, 10000 / 100 * GetSkill(ch, skill) - number(0, 15000 / 100 * 25)) / 100); tobj->set_current_durability(tobj->get_maximum_durability()); @@ -591,8 +591,8 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { tobj->set_val(0, ndice); tobj->set_val(1, sdice); tobj->set_wear_flags(created_item[obj_type].wear); - to_room = "$n $g $o3."; - to_char = " $o3."; + to_room = "$n выковал$g $o3."; + to_char = "Вы выковали $o3."; break; } } // switch @@ -606,11 +606,11 @@ void go_create_weapon(CharData *ch, ObjData *obj, int obj_type, ESkill skill) { } if (ch->GetCarryingQuantity() >= CAN_CARRY_N(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не сможете унести столько предметов.\r\n", ch); PlaceObjToRoom(tobj.get(), ch->in_room); CheckObjDecay(tobj.get()); } else if (ch->GetCarryingWeight() + tobj->get_weight() > CAN_CARRY_W(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не сможете унести такой вес.\r\n", ch); PlaceObjToRoom(tobj.get(), ch->in_room); CheckObjDecay(tobj.get()); } else { @@ -640,16 +640,16 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (ch->IsNpc() || !GetSkill(ch, skill_id)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вас этому никто не научил.\r\n", ch); return; } argument = one_argument(argument, arg1); if (!*arg1) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar(" ?\r\n", ch); + case ESkill::kReforging: SendMsgToChar("Во что вы хотите перековать?\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar(" ?\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar("Что вы хотите смастерить?\r\n", ch); break; default: break; } @@ -658,9 +658,9 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) obj_type = search_block(arg1, create_item_name, false); if (-1 == obj_type) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar(" :\r\n", ch); + case ESkill::kReforging: SendMsgToChar("Перековать можно в :\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar(" :\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar("Смастерить можно :\r\n", ch); break; default: break; } @@ -674,9 +674,9 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (created_item[obj_type].skill != skill_id) { switch (skill_id) { - case ESkill::kReforging: SendMsgToChar(" .\r\n", ch); + case ESkill::kReforging: SendMsgToChar("Данный предмет выковать нельзя.\r\n", ch); break; - case ESkill::kCreateBow: SendMsgToChar(" .\r\n", ch); + case ESkill::kCreateBow: SendMsgToChar("Данный предмет смастерить нельзя.\r\n", ch); break; default: break; } @@ -685,16 +685,16 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) for (i = 0; i < MAX_PROTO; proto[i++] = nullptr); argument = one_argument(argument, arg2); if (!*arg2) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вам нечего перековывать.\r\n", ch); return; } if (!(obj = get_obj_in_list_vis(ch, arg2, ch->carrying))) { - sprintf(buf, " '%s'.\r\n", arg2); + sprintf(buf, "У Вас нет '%s'.\r\n", arg2); SendMsgToChar(buf, ch); return; } if (obj->get_contains()) { - act(" $o5 - .", false, ch, obj, 0, kToChar); + act("В $o5 что-то лежит.", false, ch, obj, 0, kToChar); return; } if (obj->get_type() == EObjType::kMagicIngredient) { @@ -711,31 +711,31 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } if (i >= MAX_PROTO) { if (found) { - act(", - $o1.", false, ch, obj, 0, kToChar); + act("Похоже, вы уже что-то используете вместо $o1.", false, ch, obj, 0, kToChar); } else { - act(", $o .", false, ch, obj, 0, kToChar); + act("Похоже, $o не подойдет для этого.", false, ch, obj, 0, kToChar); } return; } } else { if (created_item[obj_type].material_bits && !IS_SET(created_item[obj_type].material_bits, (1 << obj->get_material()))) { - act("$o $G .", false, ch, obj, 0, kToChar); + act("$o сделан$G из неподходящего материала.", false, ch, obj, 0, kToChar); return; } } switch (skill_id) { case ESkill::kReforging: - // - // . + // Проверяем повторно из чего сделан объект + // Чтобы не было абъюза с перековкой из угля. if (created_item[obj_type].material_bits && !IS_SET(created_item[obj_type].material_bits, (1 << obj->get_material()))) { - act("$o $G .", false, ch, obj, 0, kToChar); + act("$o сделан$G из неподходящего материала.", false, ch, obj, 0, kToChar); return; } if (!privilege::IsImmortal(ch)) { if (!ROOM_FLAGGED(ch->in_room, ERoomFlag::kForge)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вам нужно попасть в кузницу для этого.\r\n", ch); return; } for (coal = ch->carrying; coal; coal = coal->get_next_content()) { @@ -756,10 +756,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) && !proto[i]) { rnum = GetObjRnum(created_item[obj_type].proto[i]); if (rnum < 0) { - act(" .", false, ch, 0, 0, kToChar); + act("У вас нет необходимого ингредиента.", false, ch, 0, 0, kToChar); } else { const ObjData obj(*obj_proto[rnum]); - act(" $o1 .", false, ch, &obj, 0, kToChar); + act("У вас не хватает $o1 для этого.", false, ch, &obj, 0, kToChar); } found = false; } @@ -795,10 +795,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) && !proto[i]) { rnum = GetObjRnum(created_item[obj_type].proto[i]); if (rnum < 0) { - act(" .", false, ch, 0, 0, kToChar); + act("У вас нет необходимого ингредиента.", false, ch, 0, 0, kToChar); } else { const ObjData obj(*obj_proto[rnum]); - act(" $o1 .", false, ch, &obj, 0, kToChar); + act("У вас не хватает $o1 для этого.", false, ch, &obj, 0, kToChar); } found = false; } @@ -820,10 +820,10 @@ void do_transform_weapon(CharData *ch, char *argument, int/* cmd*/, int subcmd) } // ***************************** MakeReceptList::MakeReceptList() { - // . + // Инициализация класса листа . } MakeReceptList::~MakeReceptList() { - // . + // Разрушение списка. clear(); } void MakeReceptList::add(MakeRecept *recept) { @@ -838,7 +838,7 @@ bool by_skill(MakeRecept *const &lhs, MakeRecept *const &rhs) { return ((lhs->skill) > (rhs->skill)); } void MakeReceptList::sort() { - // . + // Сделать сортировку по умениям. recepts.sort(by_skill); return; } @@ -846,7 +846,7 @@ size_t MakeReceptList::size() { return recepts.size(); } void MakeReceptList::clear() { - // + // Очищаем список list::iterator p; p = recepts.begin(); while (p != recepts.end()) { @@ -869,19 +869,19 @@ MakeRecept *MakeReceptList::operator[](size_t i) { return (nullptr); } MakeRecept *MakeReceptList::get_by_name(string &rname) { - // . - // . + // Ищем по списку рецепт с таким именем. + // Ищем по списку рецепты которые чар может изготовить. list::iterator p = recepts.begin(); int k = 1; // count // split string by '.' character and convert first part into number (store to k) size_t i = rname.find("."); if (std::string::npos != i) // TODO: Check me. { - // . + // Строка не найдена. if (0 < i) { k = atoi(rname.substr(0, i).c_str()); if (k <= 0) { - return nullptr; // -3.xxx + return nullptr; // Если ввели -3.xxx } } rname = rname.substr(i + 1); @@ -908,7 +908,7 @@ MakeRecept *MakeReceptList::get_by_name(string &rname) { return nullptr; } MakeReceptList *MakeReceptList::can_make(CharData *ch, MakeReceptList *canlist, ESkill use_skill) { - // . + // Ищем по списку рецепты которые чар может изготовить. list::iterator p; p = recepts.begin(); while (p != recepts.end()) { @@ -922,7 +922,7 @@ MakeReceptList *MakeReceptList::can_make(CharData *ch, MakeReceptList *canlist, return (canlist); } ObjData *get_obj_in_list_ingr(int num, - ObjData *list) // VNUM VALUE 1 + ObjData *list) //Ингридиентом является или сам прототип с VNUM или альтернатива с VALUE 1 равным внум прототипа { ObjData *i; for (i = list; i; i = i->get_next_content()) { @@ -939,7 +939,7 @@ ObjData *get_obj_in_list_ingr(int num, return nullptr; } MakeRecept::MakeRecept() : skill(ESkill::kUndefined) { - locked = true; // . + locked = true; // по умолчанию рецепт залочен. vnum = 0; obj_proto = 0; for (int i = 0; i < MAX_PARTS; i++) { @@ -952,16 +952,16 @@ int MakeRecept::can_make(CharData *ch) { int i; ObjData *ingrobj = nullptr; // char tmpbuf[kMaxInputLength]; - // locked + // Сделать проверку на поле locked if (!ch) return (false); if (locked) return (false); - // . + // Сделать проверку наличия скилла у игрока. if (ch->IsNpc() || !GetSkill(ch, skill)) { return (false); } - // + // Делаем проверку может ли чар сделать предмет такого типа if (skill == ESkill::kMakeStaff) { return 0; } @@ -971,17 +971,17 @@ int MakeRecept::can_make(CharData *ch) { } if (GetObjRnum(parts[i].proto) < 0) return (false); - //SendMsgToChar(" .\r\n",ch); // + //SendMsgToChar("Образец был невозвратимо утерян.\r\n",ch); //леший знает чего тут надо писать if (!(ingrobj = get_obj_in_list_ingr(parts[i].proto, ch->carrying))) { - //sprintf(tmpbuf," '%d' '%d'.\r\n",obj_proto,parts[i].proto); + //sprintf(tmpbuf,"Для '%d' у вас нет '%d'.\r\n",obj_proto,parts[i].proto); //SendMsgToChar(tmpbuf,ch); return (false); } int ingr_lev = get_ingr_lev(ingrobj); - // - // . + // Если чар ниже уровня ингридиента то он не может делать рецепты с его + // участием. if (!privilege::IsImpl(ch) && (ingr_lev > (GetRealLevel(ch) + 2 * remort::GetRealRemort(ch)))) { - SendMsgToChar(" - .\r\n", ch); + SendMsgToChar("Вы слишком малого уровня и вам что-то не подходит для шитья.\r\n", ch); return (false); } } @@ -989,12 +989,12 @@ int MakeRecept::can_make(CharData *ch) { } int MakeRecept::get_ingr_lev(ObjData *ingrobj) { - // ... + // Получаем уровень ингредиента ... if (ingrobj->get_type() == EObjType::kMagicIngredient) { - // 128 + // Получаем уровень игредиента до 128 return (GET_OBJ_VAL(ingrobj, 0) >> 8); } else if (ingrobj->get_type() == EObjType::kMagicComponent) { - // 26 . + // У ингров типа 26 совпадает уровень и сила. return GET_OBJ_VAL(ingrobj, IM_POWER_SLOT); } else if (ingrobj->get_type() == EObjType::kCraftMaterial) { return GET_OBJ_VAL(ingrobj, 0); @@ -1004,7 +1004,7 @@ int MakeRecept::get_ingr_lev(ObjData *ingrobj) { } int MakeRecept::get_ingr_pow(ObjData *ingrobj) { - // ... + // Получаем силу ингредиента ... if (ingrobj->get_type() == EObjType::kMagicIngredient || ingrobj->get_type() == EObjType::kCraftMaterial) { return GET_OBJ_VAL(ingrobj, 2); @@ -1047,7 +1047,7 @@ void MakeRecept::make_value_wear(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_ obj->set_val(0, ((GetRealInt(ch) * GetRealInt(ch) / 10 + GetSkill(ch, ESkill::kMakeWear)) / 100 + (GET_OBJ_VAL(ingrs[0], 3) + 1)) * wearkoeff - / 100); //=((*/10+)/100+.)*. + / 100); //АС=((инта*инта/10+умелка)/100+левл.шкуры)*коэф.части тела if (CAN_WEAR(obj, EWearFlag::kBody)) //0.9 { wearkoeff = 90; @@ -1078,7 +1078,7 @@ void MakeRecept::make_value_wear(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_ } obj->set_val(1, (GetSkill(ch, ESkill::kMakeWear) / 25 + (GET_OBJ_VAL(ingrs[0], 3) + 1)) * wearkoeff - / 100); //=(%/25+.)*. + / 100); //броня=(%умелки/25+левл.шкуры)*коэф.части тела } float MakeRecept::count_mort_requred(ObjData *obj) { float result = 0.0; @@ -1092,11 +1092,11 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float total_weight = 0.0; - // APPLY_x + // аффекты APPLY_x for (int k = 0; k < kMaxObjAffect; k++) { if (obj->get_affected(k).location == 0) continue; - // , + // случай, если один аффект прописан в нескольких полях for (int kk = 0; kk < kMaxObjAffect; kk++) { if (obj->get_affected(k).location == obj->get_affected(kk).location && k != kk) { @@ -1115,7 +1115,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { weight, GET_OBJ_VNUM(obj)); total_weight += pow(weight, SQRT_MOD); } - // + + // савесы которые с минусом должны тогда понижать вес если в + else if ((obj->get_affected(k).modifier > 0) && ((obj->get_affected(k).location == EApply::kAc) || (obj->get_affected(k).location == EApply::kSavingWill) || (obj->get_affected(k).location == EApply::kSavingCritical) || @@ -1124,7 +1124,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float weight = count_affect_weight(obj->get_affected(k).location, 0 - obj->get_affected(k).modifier); total_weight -= pow(weight, -SQRT_MOD); } - // - + //Добавленый кусок учет савесов с - значениями else if ((obj->get_affected(k).modifier < 0) && ((obj->get_affected(k).location == EApply::kAc) || (obj->get_affected(k).location == EApply::kSavingWill) || @@ -1134,7 +1134,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { float weight = count_affect_weight(obj->get_affected(k).location, obj->get_affected(k).modifier); total_weight += pow(weight, SQRT_MOD); } - // + //Добавленый кусок учет отрицательного значения но не савесов else if ((obj->get_affected(k).modifier < 0) && ((obj->get_affected(k).location != EApply::kAc) && (obj->get_affected(k).location != EApply::kSavingWill) && @@ -1145,7 +1145,7 @@ float MakeRecept::count_mort_requred(ObjData *obj) { total_weight -= pow(weight, -SQRT_MOD); } } - // AFF_x equipment_affect + // аффекты AFF_x через equipment_affect for (const auto &m : equipment_affect) { if (obj->GetEEquipmentAffect(m.aff_pos)) { if (m.aff_affect == EAffect::kAirShield) { @@ -1222,77 +1222,77 @@ float MakeRecept::count_affect_weight(int num, int mod) { } void MakeRecept::make_object(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_PARTS], int ingr_cnt) { int i, j; - // + //ставим именительные именительные падежи в алиасы sprintf(buf, "%s %s %s %s", obj->get_PName(grammar::ECase::kNom).c_str(), ingrs[0]->get_PName(grammar::ECase::kGen).c_str(), ingrs[1]->get_PName(grammar::ECase::kIns).c_str(), ingrs[2]->get_PName(grammar::ECase::kIns).c_str()); obj->set_aliases(buf); - for (i = grammar::ECase::kFirstCase; i <= grammar::ECase::kLastCase; i++) // + for (i = grammar::ECase::kFirstCase; i <= grammar::ECase::kLastCase; i++) // ставим падежи в имя с учетов ингров { auto name_case = static_cast(i); sprintf(buf, "%s", obj->get_PName(name_case).c_str()); - strcat(buf, " "); + strcat(buf, " из "); strcat(buf, ingrs[0]->get_PName(grammar::ECase::kGen).c_str()); - strcat(buf, " "); + strcat(buf, " с "); strcat(buf, ingrs[1]->get_PName(grammar::ECase::kIns).c_str()); - strcat(buf, " "); + strcat(buf, " и "); strcat(buf, ingrs[2]->get_PName(grammar::ECase::kIns).c_str()); obj->set_PName(name_case, buf); - if (i == 0) // + if (i == 0) // именительный падеж { obj->set_short_description(buf); if (GET_OBJ_SEX(obj) == EGender::kMale) { - snprintf(buf2, kMaxStringLength, " %s .", buf); + snprintf(buf2, kMaxStringLength, "Брошенный %s лежит тут.", buf); } else if (GET_OBJ_SEX(obj) == EGender::kFemale) { - snprintf(buf2, kMaxStringLength, " %s .", buf); + snprintf(buf2, kMaxStringLength, "Брошенная %s лежит тут.", buf); } else if (GET_OBJ_SEX(obj) == EGender::kPoly) { - snprintf(buf2, kMaxStringLength, " %s .", buf); + snprintf(buf2, kMaxStringLength, "Брошенные %s лежат тут.", buf); } - obj->set_description(buf2); // + obj->set_description(buf2); // описание на земле } } - obj->set_is_rename(true); // + obj->set_is_rename(true); // ставим флаг что объект переименован auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[0]->get_affect_flags(), get_ingr_pow(ingrs[0])); obj->SetEquipmentAffectFlags(temp_flags); - // ... , 0 , 1 + // перносим эффекты ... с ингров на прототип, 0 объект шкура переносим все, с остальных 1 рандом merge_extra_flags(ch, obj, ingrs[0], get_ingr_pow(ingrs[0])); auto temp_affected = obj->get_all_affected(); add_affects(ch, temp_affected, ingrs[0]->get_all_affected(), get_ingr_pow(ingrs[0])); obj->set_all_affected(temp_affected); - add_rnd_skills(ch, ingrs[0], obj); // - obj->set_extra_flag(EObjFlag::kNoalter); // + add_rnd_skills(ch, ingrs[0], obj); //переносим случайную умелку со шкуры + obj->set_extra_flag(EObjFlag::kNoalter); // нельзя сфрешить черным свитком obj->set_timer((GET_OBJ_VAL(ingrs[0], 3) + 1) * 1000 - + GetSkill(ch, ESkill::kMakeWear) / 2 * number(160, 220)); // - obj->set_craft_timer(obj->get_timer()); // . + + GetSkill(ch, ESkill::kMakeWear) / 2 * number(160, 220)); // таймер зависит в основном от умелки + obj->set_craft_timer(obj->get_timer()); // запомним таймер созданной вещи для правильного отображения при осм для ее сост. for (j = 1; j < ingr_cnt; j++) { int i, raffect = 0; - for (i = 0; i < kMaxObjAffect; i++) // + for (i = 0; i < kMaxObjAffect; i++) // посмотрим скока аффектов { if (ingrs[j]->get_affected(i).location == EApply::kNone) { break; } } - if (i > 0) // > 0 + if (i > 0) // если > 0 переносим случайный { raffect = number(0, i - 1); for (int i = 0; i < kMaxObjAffect; i++) { const auto &ra = ingrs[j]->get_affected(raffect); if (obj->get_affected(i).location - == ra.location) // , + == ra.location) // если аффект такой уже висит и он меньше, переставим значение { if (obj->get_affected(i).modifier < ra.modifier) { obj->set_affected(i, obj->get_affected(i).location, ra.modifier); } break; } - if (obj->get_affected(i).location == EApply::kNone) // + if (obj->get_affected(i).location == EApply::kNone) // добавляем афф на свободное место { - if (number(1, 100) > GET_OBJ_VAL(ingrs[j], 2)) // + if (number(1, 100) > GET_OBJ_VAL(ingrs[j], 2)) // проверим фейл на силу ингра { break; } @@ -1301,14 +1301,14 @@ void MakeRecept::make_object(CharData *ch, ObjData *obj, ObjData *ingrs[MAX_PART } } } - // ... c . + // переносим аффекты ... c ингров на прототип. auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[j]->get_affect_flags(), get_ingr_pow(ingrs[j])); obj->SetEquipmentAffectFlags(temp_flags); - // ... . + // перносим эффекты ... с ингров на прототип. merge_extra_flags(ch, obj, ingrs[j], get_ingr_pow(ingrs[j])); - // 1 - add_rnd_skills(ch, ingrs[j], obj); // + // переносим 1 рандом аффект + add_rnd_skills(ch, ingrs[j], obj); //переноси случайную умелку с ингров } } static std::string craft_recipe_name(int recipe_id) { @@ -1334,39 +1334,39 @@ static void record_craft_success(int recipe_id, ESkill skill) { }); } -// +// создать предмет по рецепту int MakeRecept::make(CharData *ch) { char tmpbuf[kMaxStringLength];//, tmpbuf2[kMaxStringLength]; ObjData *ingrs[MAX_PARTS]; string tmpstr, charwork, roomwork, charfail, roomfail, charsucc, roomsucc, chardam, roomdam, tagging, itemtag; int dam = 0; bool make_fail; - // 1. + // 1. Проверить есть ли скилл у чара if (ch->IsNpc() || !GetSkill(ch, skill)) { - SendMsgToChar(" c .\r\n", ch); + SendMsgToChar("Странно что вам вообще пришло в голову cделать это.\r\n", ch); return (false); } - // 2. + // 2. Проверить есть ли ингры у чара if (!can_make(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("У вас нет составляющих для этого.\r\n", ch); return (false); } if (ch->get_move() < MIN_MAKE_MOVE) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы слишком устали и вам ничего не хочется делать.\r\n", ch); return (false); } auto tobj = GetObjectPrototype(obj_proto); if (!tobj) { return 0; } - // + // Проверяем возможность создания предмета if (!privilege::IsImmortal(ch) && (skill == ESkill::kMakeStaff)) { const ObjData obj(*tobj); - act(" $o3.", false, ch, &obj, 0, kToChar); + act("Вы не готовы к тому чтобы сделать $o3.", false, ch, &obj, 0, kToChar); return (false); } - // - // 3. + // Прогружаем в массив реальные ингры + // 3. Проверить уровни ингров и чара int ingr_cnt = 0, ingr_lev, i, craft_weight, ingr_pow; for (i = 0; i < MAX_PARTS; i++) { if (parts[i].proto == 0) @@ -1374,124 +1374,124 @@ int MakeRecept::make(CharData *ch) { ingrs[i] = get_obj_in_list_ingr(parts[i].proto, ch->carrying); ingr_lev = get_ingr_lev(ingrs[i]); if (!privilege::IsImpl(ch) && (ingr_lev > (GetRealLevel(ch) + 2 * remort::GetRealRemort(ch)))) { - tmpstr = " " + ingrs[i]->get_PName(grammar::ECase::kAcc) - + "\r\n " + tobj->get_PName(grammar::ECase::kIns) + ".\r\n"; + tmpstr = "Вы побоялись испортить " + ingrs[i]->get_PName(grammar::ECase::kAcc) + + "\r\n и прекратили работу над " + tobj->get_PName(grammar::ECase::kIns) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); return (false); }; ingr_pow = get_ingr_pow(ingrs[i]); if (ingr_pow < parts[i].min_power) { - tmpstr = "$o " + tobj->get_PName(grammar::ECase::kGen) + "."; + tmpstr = "$o не подходит для изготовления " + tobj->get_PName(grammar::ECase::kGen) + "."; act(tmpstr.c_str(), false, ch, ingrs[i], 0, kToChar); return (false); } ingr_cnt++; } //int stat_bonus; - // . + // Делаем всякие доп проверки для различных умений. switch (skill) { case ESkill::kMakeWeapon: case ESkill::kMakeArmor: - // . + // Проверяем есть ли тут наковальня или комната кузня. if ((!ROOM_FLAGGED(ch->in_room, ERoomFlag::kForge)) && (!privilege::IsImmortal(ch))) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вам нужно попасть в кузницу для этого.\r\n", ch); return (false); } - charwork = " $o3."; - roomwork = "$n $g $g ."; - charfail = " ."; - roomfail = " $n1 ."; - charsucc = " $o3."; - roomsucc = "$n $G $o3."; - chardam = " ."; - roomdam = " $n1, $s ."; - tagging = " $o5."; - itemtag = " $o5 '$g $n'."; + charwork = "Вы поместили заготовку на наковальню и начали ковать $o3."; + roomwork = "$n поместил$g закотовку на наковальню и начал$g ковать."; + charfail = "Заготовка под вашими ударами покрылась сетью трещин и раскололась."; + roomfail = "Под ударами молота $n1 заготовка раскололась."; + charsucc = "Вы выковали $o3."; + roomsucc = "$n выковал$G $o3."; + chardam = "Заготовка выскочила из под молота и больно ударила вас."; + roomdam = "Заготовка выскочила из под молота $n1, больно $s ударив."; + tagging = "Вы поставили свое клеймо на $o5."; + itemtag = "На $o5 стоит клеймо 'Выковал$g $n'."; dam = 70; - // + // Бонус сила //stat_bonus = number(0, GetRealStr(ch)); break; - case ESkill::kMakeBow: charwork = " $o3."; - roomwork = "$n $g - $o3."; - charfail = " $o $U ."; - roomfail = " $o $U $n1."; - charsucc = " c $o3."; - roomsucc = "$n $g $o3."; - chardam = "$o3 $U ."; - roomdam = "$o3 $U $n2."; - tagging = " $o5."; - itemtag = " $o5 '$g $n'."; - // + case ESkill::kMakeBow: charwork = "Вы начали мастерить $o3."; + roomwork = "$n начал$g мастерить что-то очень напоминающее $o3."; + charfail = "С громким треском $o сломал$U в ваших неумелых руках."; + roomfail = "С громким треском $o сломал$U в неумелых руках $n1."; + charsucc = "Вы cмастерили $o3."; + roomsucc = "$n смастерил$g $o3."; + chardam = "$o3 с громким треском сломал$U оцарапав вам руки."; + roomdam = "$o3 с громким треском сломал$U оцарапав руки $n2."; + tagging = "Вы вырезали свое имя на $o5."; + itemtag = "На $o5 видна метка 'Смастерил$g $n'."; + // Бонус ловкость //stat_bonus = number(0, GetRealDex(ch)); dam = 40; break; - case ESkill::kMakeWear: charwork = " $o3."; - roomwork = "$n $g $g ."; - charfail = " ."; - roomfail = "$n $u - , ."; - charsucc = " $o3."; - roomsucc = "$n $g $o3."; - chardam = " . ."; - roomdam = "$n $g . \r\n ."; - tagging = " $o2 ."; - itemtag = " $o5 '$g $n'."; - // , :)) + case ESkill::kMakeWear: charwork = "Вы взяли в руку иголку и начали шить $o3."; + roomwork = "$n взял$g в руку иголку и начал$g увлеченно шить."; + charfail = "У вас ничего не получилось сшить."; + roomfail = "$n пытал$u что-то сшить, но ничего не вышло."; + charsucc = "Вы сшили $o3."; + roomsucc = "$n сшил$g $o3."; + chardam = "Игла глубоко вошла в вашу руку. Аккуратнее надо быть."; + roomdam = "$n глубоко воткнул$g иглу в себе в руку. \r\nА с виду вполне нормальный человек."; + tagging = "Вы пришили к $o2 бирку со своим именем."; + itemtag = "На $o5 вы заметили бирку 'Сшил$g $n'."; + // Бонус тело , не спрашивайте почему :)) //stat_bonus = number(0, GetRealCon(ch)); dam = 30; break; - case ESkill::kMakeAmulet: charwork = " $o3."; - roomwork = "$n $g - $g -."; - charfail = " "; - roomfail = "$n $u - , ."; - charsucc = " $o3."; - roomsucc = "$n $g $o3."; - chardam = " , ."; - roomdam = " . $n2 ! \r\n"; - tagging = " $o1 ."; - itemtag = " $o1 '$n', , ."; + case ESkill::kMakeAmulet: charwork = "Вы взяли в руки необходимые материалы и начали мастерить $o3."; + roomwork = "$n взял$g в руки что-то и начал$g увлеченно пыхтеть над чем-то."; + charfail = "У вас ничего не получилось"; + roomfail = "$n пытал$u что-то сделать, но ничего не вышло."; + charsucc = "Вы смастерили $o3."; + roomsucc = "$n смастерил$g $o3."; + chardam = "Яркая вспышка дала вам знать, что с магией необходимо быть поосторожней."; + roomdam = "Яркая вспышка осветила все вокруг. $n2 стоит быть аккуратнее с магией! \r\n"; + tagging = "На обратной стороне $o1 вы нацарапали свое имя."; + itemtag = "На обратной стороне $o1 вы заметили слово '$n', видимо, это имя создателя этой чудной вещицы."; dam = 30; break; - case ESkill::kMakeJewel: charwork = " $o3."; - roomwork = "$n - ."; - charfail = " $o5."; - roomfail = " $n, $s ."; - charsucc = " $o3."; - roomsucc = "$n $g $o3."; - chardam = " .\r\n !"; - roomdam = " $n2."; - tagging = " $o2 ."; - itemtag = " $o1 'C $n4'."; - // + case ESkill::kMakeJewel: charwork = "Вы начали мастерить $o3."; + roomwork = "$n начал мастерить какую-то диковинку."; + charfail = "Ваше неудачное движение повредило $o5."; + roomfail = "Неудачное движение $n, сделало $s работу бессмысленной."; + charsucc = "Вы смастерили $o3."; + roomsucc = "$n смастерил$g $o3."; + chardam = "Мелкий кусочек материала отскочил и попал вам в глаз.\r\nЭто было больно!"; + roomdam = "Мелкий кусочек материала отскочил и попал в глаз $n2."; + tagging = "Вы приладили к $o2 табличку со своим именем."; + itemtag = "С нижней стороны $o1 укреплена табличка 'Cделано $n4'."; + // Бонус харя //stat_bonus = number(0, GetRealCha(ch)); dam = 30; break; - case ESkill::kMakeStaff: charwork = " $o3."; - roomwork = "$n -, ."; - charfail = "$o3 ."; - roomfail = " $n1 , ."; + case ESkill::kMakeStaff: charwork = "Вы начали мастерить $o3."; + roomwork = "$n начал мастерить что-то, посылая всех к чертям."; + charfail = "$o3 осветил комнату магическим светом и истаял."; + roomfail = "Предмет в руках $n1 вспыхнул, озарив комнату магическим светом и истаял."; charsucc = - " $o3 .\r\n, $E ."; - roomsucc = "$n $g $o3. ."; - chardam = "$o . ."; - roomdam = "$o $n1, .\r\n ."; - tagging = " $o2 ."; - itemtag = " ' $n4'."; - // . + "Тайные знаки нанесенные на $o3 вспыхнули и погасли.\r\nДа, $E хорошо послужит своему хозяину."; + roomsucc = "$n смастерил$g $o3. Вы почувствовали скрытую силу спрятанную в этом предмете."; + chardam = "$o взорвался в ваших руках. Вас сильно обожгло."; + roomdam = "$o взорвался в руках $n1, опалив его.\r\nВокруг приятно запахло жаренным мясом."; + tagging = "Вы начертили на $o2 свое имя."; + itemtag = "Среди рунных знаков видна надпись 'Создано $n4'."; + // Бонус ум. //stat_bonus = number(0, GetRealInt(ch)); dam = 70; break; - case ESkill::kMakePotion: charwork = " , $o3."; - roomwork = "$n ."; - charfail = " ."; + case ESkill::kMakePotion: charwork = "Вы достали небольшой горшочек и развели под ним огонь, начав варить $o3."; + roomwork = "$n достал горшочек и поставил его на огонь."; + charfail = "Вы не уследили как зелье закипело и пролилось в огонь."; roomfail = - " $g $n ,\r\n ."; - charsucc = " ."; - roomsucc = "$n $g $o3. , ."; - chardam = " , ."; - roomdam = " $o4 $n1, $s."; - tagging = " $o2 ."; - itemtag = " $o1 ' $n4'"; - // + "Зелье которое варил$g $n закипело и пролилось в огонь,\r\n распространив по комнате ужасную вонь."; + charsucc = "Зелье удалось вам на славу."; + roomsucc = "$n сварил$g $o3. Приятный аромат зелья из горшочка, так и манит вас."; + chardam = "Вы опрокинули горшочек с зельем на себя, сильно ошпарившись."; + roomdam = "Горшочек с $o4 опрокинулся на $n1, ошпарив $s."; + tagging = "Вы на прикрепили к $o2 бирку со своим именем."; + itemtag = "На $o1 вы заметили бирку 'Сварено $n4'"; + // Бонус мудра //stat_bonus = number(0, GetRealWis(ch)); dam = 40; break; @@ -1500,15 +1500,15 @@ int MakeRecept::make(CharData *ch) { const ObjData object(*tobj); act(charwork.c_str(), false, ch, &object, 0, kToChar); act(roomwork.c_str(), false, ch, &object, 0, kToRoom); - // - // = 50% - // > 15 0% - // * 2 - random(30) < 15 - - // (...) - // . + // Считаем вероятность испортить отдельный ингридиент + // если уровень чара = уровню ингра то фейл 50% + // если уровень чара > уровня ингра на 15 то фейл 0% + // уровень чара * 2 - random(30) < 15 - фейл то пропадает весь материал + // Выдается Вы испортили (...) + // и выходим. make_fail = false; - // . - // . + // Это средний уровень получившегося предмета. + // использует при расчете макс количества предметов в мире. int created_lev = 0; int used_non_ingrs = 0; for (i = 0; i < ingr_cnt; i++) { @@ -1518,12 +1518,12 @@ int MakeRecept::make(CharData *ch) { } else { created_lev += ingr_lev; } - // . + // Шанс испортить не ингредиент всетаки есть. if ((number(0, 30) < (5 + ingr_lev - GetRealLevel(ch) - 2 * remort::GetRealRemort(ch))) && !privilege::IsImpl(ch)) { - tmpstr = " " + ingrs[i]->get_PName(grammar::ECase::kAcc) + ".\r\n"; + tmpstr = "Вы испортили " + ingrs[i]->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); - //extract_obj(ingrs[i]); // - // () + //extract_obj(ingrs[i]); //заменим на обнуление веса + //чтобы не крешило дальше в обработке фейла (Купала) IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); make_fail = true; @@ -1532,11 +1532,11 @@ int MakeRecept::make(CharData *ch) { created_lev = created_lev / MAX(1, (ingr_cnt - used_non_ingrs)); int j; int craft_move = MIN_MAKE_MOVE + (created_lev / 2) - 1; - // + // Снимаем мувы за умение if (ch->get_move() < craft_move) { ch->set_move(0); - // . - tmpstr = " " + tobj->get_PName(grammar::ECase::kAcc) + ".\r\n"; + // Вам не хватило сил доделать. + tmpstr = "Вам не хватило сил доделать " + tobj->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); make_fail = true; } else { @@ -1546,72 +1546,72 @@ int MakeRecept::make(CharData *ch) { } TrainSkill(ch, skill, !make_fail, nullptr); - // 4. . + // 4. Считаем сколько материала треба. if (!make_fail) { for (i = 0; i < ingr_cnt; i++) { - if (skill == ESkill::kMakeWear && i == 0) // + if (skill == ESkill::kMakeWear && i == 0) //для шитья всегда раскраиваем шкуру { IS_CARRYING_W(ch) -= ingrs[0]->get_weight(); - ingrs[0]->set_weight(0); // - tmpstr = " " + ingrs[0]->get_PName(grammar::ECase::kAcc) + ".\r\n"; + ingrs[0]->set_weight(0); // шкуру дикеим полностью + tmpstr = "Вы раскроили полностью " + ingrs[0]->get_PName(grammar::ECase::kAcc) + ".\r\n"; SendMsgToChar(tmpstr.c_str(), ch); continue; } // - // = . + + // нужный материал = мин.материал + // random(100) - skill - // < 20 . + rand(./3) - // < 50 .*rand(1,2) + rand(./3) - // > 50 .*rand(2,5) + rand(./3) + // если она < 20 то мин.вес + rand(мин.вес/3) + // если она < 50 то мин.вес*rand(1,2) + rand(мин.вес/3) + // если она > 50 мин.вес*rand(2,5) + rand(мин.вес/3) if (get_ingr_lev(ingrs[i]) == -1) - continue; // . . + continue; // Компонент не ингр. пропускаем. craft_weight = parts[i].min_weight + number(0, (parts[i].min_weight / 3) + 1); j = number(0, 100) - CalcCurrentSkill(ch, skill, 0); if ((j >= 20) && (j < 50)) craft_weight += parts[i].min_weight * number(1, 2); else if (j > 50) craft_weight += parts[i].min_weight * number(2, 5); - // 5. . - // . + // 5. Делаем проверку есть ли столько материала. + // если не хватает то удаляем игридиент и фейлим. int state = craft_weight; - // , , , (make_fail) , ? - //SendMsgToChar(ch, " %d %d %d\r\n", state, ingrs[i]->get_weight(), ingr_cnt); + // Обсчет веса ингров в цикле, если не хватило веса берем следующий ингр в инве, если не хватает, делаем фэйл (make_fail) и брекаем внешний цикл, смысл дальше ингры смотреть? + //SendMsgToChar(ch, "Требуется вес %d вес ингра %d требуемое кол ингров %d\r\n", state, ingrs[i]->get_weight(), ingr_cnt); int obj_vnum_tmp = GET_OBJ_VNUM(ingrs[i]); while (state > 0) { - // - // . , , . + //Переделаем слегка логику итераций + //Сперва проверяем сколько нам нужно. Если вес ингра больше, чем требуется, то вычитаем вес и останавливаем итерацию. if (ingrs[i]->get_weight() > state) { ingrs[i]->sub_weight(state); - SendMsgToChar(ch, " %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); + SendMsgToChar(ch, "Вы использовали %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); IS_CARRYING_W(ch) -= state; break; } - // , , , . + //Если вес ингра ровно столько, сколько требуется, вычитаем вес, ломаем ингр и останавливаем итерацию. else if (ingrs[i]->get_weight() == state) { IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); - SendMsgToChar(ch, " %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); + SendMsgToChar(ch, "Вы полностью использовали %s.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); //extract_obj(ingrs[i]); break; } - // , , , . + //Если вес ингра меньше, чем требуется, то вычтем этот вес из того, сколько требуется. else { state = state - ingrs[i]->get_weight(); SendMsgToChar(ch, - " %s .\r\n", + "Вы полностью использовали %s и начали искать следующий ингредиент.\r\n", ingrs[i]->get_PName(grammar::ECase::kAcc).c_str()); std::string tmpname = std::string(ingrs[i]->get_PName(grammar::ECase::kGen).c_str()); IS_CARRYING_W(ch) -= ingrs[i]->get_weight(); ingrs[i]->set_weight(0); ExtractObjFromWorld(ingrs[i]); ingrs[i] = nullptr; - // , . + //Если некст ингра в инве нет, то сообщаем об этом и идем в фэйл. Некст ингры все равно проверяем if (!get_obj_in_list_ingr(obj_vnum_tmp, ch->carrying)) { - SendMsgToChar(ch, " %s.\r\n", tmpname.c_str()); + SendMsgToChar(ch, "У вас в инвентаре больше нет %s.\r\n", tmpname.c_str()); make_fail = true; break; } - // + //Подцепляем некст ингр и идем в нашу проверку заново else { ingrs[i] = get_obj_in_list_ingr(obj_vnum_tmp, ch->carrying); } @@ -1620,7 +1620,7 @@ int MakeRecept::make(CharData *ch) { } } if (make_fail) { - // . + // Считаем крит фейл или нет. int crit_fail = number(0, 100); if (crit_fail > 2) { const ObjData obj(*tobj); @@ -1631,16 +1631,16 @@ int MakeRecept::make(CharData *ch) { act(chardam.c_str(), false, ch, &obj, 0, kToChar); act(roomdam.c_str(), false, ch, &obj, 0, kToRoom); dam = number(0, dam); - // . + // Наносим дамаг. if (GetRealLevel(ch) >= kLvlImmortal && dam > 0) { - SendMsgToChar(" , ...", ch); + SendMsgToChar("Будучи бессмертным, вы избежали повреждения...", ch); return (false); } ch->set_hit(ch->get_hit() - dam); update_pos(ch); char_dam_message(dam, ch, ch, 0); if (ch->GetPosition() == EPosition::kDead) { - // . + // Убился веником. if (!ch->IsNpc()) { sprintf(tmpbuf, "%s killed by a crafting at %s", GET_NAME(ch), @@ -1659,17 +1659,17 @@ int MakeRecept::make(CharData *ch) { record_craft_failure(obj_proto, skill); return (false); } - // + // Лоадим предмет игроку const auto obj = world_objects.create_from_prototype_by_vnum(obj_proto); act(charsucc.c_str(), false, ch, obj.get(), 0, kToChar); act(roomsucc.c_str(), false, ch, obj.get(), 0, kToRoom); - // 6. - // - // -: -+(skill - random(100))/20; - // ???: random(200) - skill > 0 . - // .. . - // . - // . + // 6. Считаем базовые статсы предмета и таймер + // формула для каждого умения отдельная + // Для числовых х-к: х-ка+(skill - random(100))/20; + // Для флагов ???: random(200) - skill > 0 то флаг переноситься. + // Т.к. сделать мы можем практически любой предмет. + // Модифицируем вес предмета и его таймер. + // Для маг предметов надо в сторону облегчения. // i = obj->get_weight(); switch (skill) { case ESkill::kMakeBow:; @@ -1687,29 +1687,29 @@ int MakeRecept::make(CharData *ch) { i = obj->get_timer(); obj->set_timer(stat_modify(ch, obj->get_timer(), 1)); - // -. - // (!20!) - // 20 100 +5 - // . - // . + // Модифицируем уникальные для предметов х-ки. + // Балансировка варьируется изменением делителя (!20!) + // при делителе 20 и умении 100 и макс везении будет +5 к параметру + // Можно посчитать бонусы от времени суток. + // Считаем среднюю силу ингров . switch (obj->get_type()) { case EObjType::kLightSource: - // . + // Считаем длительность свечения. if (GET_OBJ_VAL(obj, 2) != -1) { obj->set_val(2, stat_modify(ch, GET_OBJ_VAL(obj, 2), 1)); } break; case EObjType::kWand: case EObjType::kStaff: - // + // Считаем уровень закла // issue.magic-items: the maker's competence (make-staff skill + Int) becomes the casting // potency (kSpellItem* keys), replacing the legacy val[0] = caster level. obj->SetPotionValueKey(ObjVal::EValueKey::kMakerSkill, std::max(1, GetSkill(ch, ESkill::kMakeStaff))); obj->SetPotionValueKey(ObjVal::EValueKey::kMakerStat, GetRealBaseStat(ch, EBaseStat::kInt)); break; case EObjType::kWeapon: - // xdy - // XdY + // Считаем число xdy + // модифицируем XdY if (GET_OBJ_VAL(obj, 1) > GET_OBJ_VAL(obj, 2)) { obj->set_val(1, stat_modify(ch, GET_OBJ_VAL(obj, 1), 1)); } else { @@ -1720,79 +1720,79 @@ int MakeRecept::make(CharData *ch) { case EObjType::kLightArmor: case EObjType::kMediumArmor: case EObjType::kHeavyArmor: - // + + // Считаем + АС obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); - // . + // Считаем поглощение. obj->set_val(1, stat_modify(ch, GET_OBJ_VAL(obj, 1), 1)); break; case EObjType::kPotion: - // + // Считаем уровень итоговый напитка obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kContainer: - // . + // Считаем объем контейнера. obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kLiquidContainer: - // . + // Считаем объем контейнера. obj->set_val(0, stat_modify(ch, GET_OBJ_VAL(obj, 0), 1)); break; case EObjType::kMagicIngredient: - // ... . :) + // Для ингров ничего не трогаем ... ибо опасно. :) break; default: break; } - // 7. . . - // - + - // (random(100) - ) < 1 . - // 1 25 1/2 - // 25 50 1/3 - // 0 - // ...+ + .. + // 7. Считаем доп. статсы предмета. + // х-ка прототипа + + // если (random(100) - сила ингра ) < 1 то переноситься весь параметр. + // если от 1 до 25 то переноситься 1/2 + // если от 25 до 50 то переноситься 1/3 + // больше переноситься 0 + // переносим доп аффекты ...+мудра +ловка и т.п. if (skill == ESkill::kMakeWear) { make_object(ch, obj.get(), ingrs, ingr_cnt); make_value_wear(ch, obj.get(), ingrs); - } else // + } else // если не шитье то никаких махинаций с падежами и копированием рандом аффекта { for (j = 0; j < ingr_cnt; j++) { ingr_pow = get_ingr_pow(ingrs[j]); if (ingr_pow < 0) { ingr_pow = 20; } - // ... c . + // переносим аффекты ... c ингров на прототип. auto temp_flags = obj->get_affect_flags(); add_flags(ch, &temp_flags, &ingrs[j]->get_affect_flags(), ingr_pow); obj->SetEquipmentAffectFlags(temp_flags); - // ... . + // перносим эффекты ... с ингров на прототип. merge_extra_flags(ch, obj.get(), ingrs[j], ingr_pow); auto temp_affected = obj->get_all_affected(); add_affects(ch, temp_affected, ingrs[j]->get_all_affected(), ingr_pow); obj->set_all_affected(temp_affected); } } - // . + // Мочим истраченные ингры. for (i = 0; i < ingr_cnt; i++) { if (ingrs[i]->get_weight() <= 0) { ExtractObjFromWorld(ingrs[i]); } } - // 8. . . - // (31 - . ) * 5 - - // 30 5 - // .. . - // - // ( ). - // . + // 8. Проверяем мах. инворлд. + // Считаем по формуле (31 - ср. уровень предмета) * 5 - + // овер шмота в мире не 30 лева не больше 5 штук + // Т.к. ср. уровень ингров будет определять + // число шмоток в мире то шмотки по хуже будут вытеснять + // шмотки по лучше (в целом это не так страшно). + // Ставим метку если все хорошо. if ((obj->get_type() != EObjType::kMagicIngredient && obj->get_type() != EObjType::kMagicComponent) && (number(1, 100) - CalcCurrentSkill(ch, skill, 0) < 0)) { act(tagging.c_str(), false, ch, obj.get(), 0, kToChar); - // . + // Прибавляем в экстра описание строчку. char *tagchar = format_act(itemtag.c_str(), ch, obj.get(), 0); obj->set_tag(tagchar); free(tagchar); }; - // + // простановка мортов при шитье auto total_weight = count_mort_requred(obj.get()) * 7 / 10; if (total_weight > 35) { @@ -1809,14 +1809,14 @@ int MakeRecept::make(CharData *ch) { obj->set_minimum_remorts(0); } - // . + // Пишем производителя в поле. obj->set_crafter_uid(ch->get_uid()); - // 9. 2 + // 9. Проверяем минимум 2 if (ch->GetCarryingQuantity() >= CAN_CARRY_N(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не сможете унести столько предметов.\r\n", ch); PlaceObjToRoom(obj.get(), ch->in_room); } else if (ch->GetCarryingWeight() + obj->get_weight() > CAN_CARRY_W(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не сможете унести такой вес.\r\n", ch); PlaceObjToRoom(obj.get(), ch->in_room); } else { PlaceObjToInventory(obj.get(), ch); @@ -1825,11 +1825,11 @@ int MakeRecept::make(CharData *ch) { record_craft_success(obj_proto, skill); return (true); } -// . -// . +// вытащить рецепт из строки. +// Модификатор базовых значений. int MakeRecept::stat_modify(CharData *ch, int value, float devider) { - // -: -+(skill - random(100))/20; - // ???: random(200) - skill > 0 . + // Для числовых х-к: х-ка+(skill - random(100))/20; + // Для флагов ???: random(200) - skill > 0 то флаг переноситься. int res = value; float delta = 0; int skill_prc = 0; @@ -1844,7 +1844,7 @@ int MakeRecept::stat_modify(CharData *ch, int value, float devider) { delta = (value / 4) * delta / MUD::Skill(skill).difficulty / devider; } res += (int) delta; - // 1; + // Если параметр завалили то возвращаем 1; if (res < 0) { return 1; } @@ -1857,14 +1857,14 @@ void MakeRecept::add_rnd_skills(CharData * /*ch*/, ObjData *obj_from, ObjData *o int percent; CObjectPrototype::skills_t skills; obj_from->get_skills(skills); - int i = static_cast(skills.size()); // - rskill = number(0, i); // + int i = static_cast(skills.size()); // сколько добавлено умелок + rskill = number(0, i); // берем рандом for (const auto &it : skills) { - if (z == rskill) // + if (z == rskill) // ставим рандомную умелку { auto skill_num = it.first; percent = it.second; - // TODO: ? + // TODO: такого не должно быть? if (percent == 0) { continue; } @@ -1876,7 +1876,7 @@ void MakeRecept::add_rnd_skills(CharData * /*ch*/, ObjData *obj_from, ObjData *o } template int MakeRecept::add_flags(CharData *ch, F *base_flag, const F *add_flag, int/* delta*/) { -// :( +// БЕз вариантов вообще :( int tmpprob, skl = CalcCurrentSkill(ch, skill, 0); for (int i = 0; i < 3; i++) { for (int j = 0; j < 32; j++) { @@ -1891,9 +1891,9 @@ int MakeRecept::add_flags(CharData *ch, F *base_flag, const F *add_flag, int/* d template int MakeRecept::add_flags(CharData *, BitsetFlags *, const BitsetFlags *, int); template int MakeRecept::add_flags(CharData *, BitsetFlags *, const BitsetFlags *, int); -// extra- ( add_flags), -// // (issue #3459: "!" ..). -// -- . +// Переносит extra-флаги ингредиента на предмет (как add_flags), но НЕ даёт ингредиенту +// навешивать служебные/привязочные/распадные флаги (issue #3459: крафт плодил "!бросить" и т.п.). +// Свои такие флаги предмет сохраняет -- блокируется только добавление их с ингредиента. void MakeRecept::merge_extra_flags(CharData *ch, ObjData *obj, ObjData *ingr, int pow) { static const EObjFlag kNotTransferable[] = { EObjFlag::kNodrop, EObjFlag::kNorent, EObjFlag::kNodonate, EObjFlag::kNosell, @@ -1910,7 +1910,7 @@ void MakeRecept::merge_extra_flags(CharData *ch, ObjData *obj, ObjData *ingr, in auto temp = obj->get_extra_flags(); add_flags(ch, &temp, &ingr->get_extra_flags(), pow); obj->set_extra_flags(temp); - // , (.. ) + // откатываем только те служебные флаги, которых у предмета не было (т.е. их добавил ингредиент) for (std::size_t i = 0; i < std::size(kNotTransferable); ++i) { if (!had[i] && obj->has_flag(kNotTransferable[i])) { obj->unset_extraflag(kNotTransferable[i]); @@ -1932,7 +1932,7 @@ int MakeRecept::add_affects(CharData *ch, if (base[j].location == EApply::kNone) continue; if (add[i].location == base[j].location) { - // . + // Аффекты совпали. found = true; if (number(0, 100) > delta) break; @@ -1941,7 +1941,7 @@ int MakeRecept::add_affects(CharData *ch, } } if (!found) { - // . + // Ищем первый свободный аффект и втыкаем туда новый. for (int j = 0; j < kMaxObjAffect; j++) { if (base[j].location == EApply::kNone) { if (number(0, 100) > delta) @@ -1957,7 +1957,7 @@ int MakeRecept::add_affects(CharData *ch, return (true); } -// act-a +// Форматирование вывода в соответствии с форматом act-a // output act format// char *format_act(const char *orig, CharData *ch, ObjData *obj, const void *vict_obj) { const char *i = nullptr; diff --git a/src/gameplay/mechanics/groups.cpp b/src/gameplay/mechanics/groups.cpp index b7e38c2d6..eac31300c 100644 --- a/src/gameplay/mechanics/groups.cpp +++ b/src/gameplay/mechanics/groups.cpp @@ -2,8 +2,8 @@ \file groups.h - a part of the Bylins engine. \authors Created by Sventovit. \date 15.09.2024. -\brief . -\detail , , , - . +\brief Код групп игроков и мобов. +\detail Группы, вступление, покидание, дележка опыта - должно быть тут. */ #include "utils/native_text.h" @@ -34,7 +34,7 @@ bool group::same_group(CharData *ch, CharData *tch) { if (!ch || !tch) return false; - // () + // Добавлены проверки чтобы не любой заследовавшийся моб считался согруппником (Купала) if (ch->IsNpc() && ch->has_master() && !ch->get_master()->IsNpc() @@ -97,19 +97,19 @@ int group::perform_group(CharData *ch, CharData *vict) { AFF_FLAGS(vict).set(EAffect::kGroup); if (ch != vict) { - act("$N $A (-, :).", false, ch, nullptr, vict, kToChar); - act(" $n1.", false, ch, nullptr, vict, kToVict); - act("$N $A $n1.", false, ch, nullptr, vict, kToNotVict | kToArenaListen); + act("$N принят$A в члены вашего кружка (тьфу-ты, группы :).", false, ch, nullptr, vict, kToChar); + act("Вы приняты в группу $n1.", false, ch, nullptr, vict, kToVict); + act("$N принят$A в группу $n1.", false, ch, nullptr, vict, kToNotVict | kToArenaListen); } return (true); } /** -* . -* , . -* \param vict - , ' ', -* , , , -* . +* Смена лидера группы на персонажа с макс лидеркой. +* Сам лидер при этом остается в группе, если он живой. +* \param vict - новый лидер, если смена происходит по команде 'гр лидер имя', +* старый лидер соответственно группится обратно, если нулевой, то считаем, что +* произошла смерть старого лидера и новый выбирается по наибольшей лидерке. */ void change_leader(CharData *ch, CharData *vict) { if (ch->IsNpc() @@ -120,7 +120,7 @@ void change_leader(CharData *ch, CharData *vict) { CharData *leader = vict; if (!leader) { - // , + // лидер умер, ищем согрупника с максимальным скиллом лидерки for (auto *l : ch->followers) { if (!is_group_member(ch, l)) continue; @@ -135,7 +135,7 @@ void change_leader(CharData *ch, CharData *vict) { return; } - // + // для реследования используем стандартные функции std::vector temp_list; auto followers_copy = ch->followers; for (auto *l : followers_copy) { @@ -159,9 +159,9 @@ void change_leader(CharData *ch, CharData *vict) { } } - // , + // бывшего лидера последним закидываем обратно в группу, если он живой if (vict) { - // , + // флаг группы надо снять, иначе при регрупе не будет писаться о старом лидере //AFF_FLAGS(ch).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(ch); follow::AddFollowerSilently(leader, ch); @@ -179,64 +179,64 @@ void change_leader(CharData *ch, CharData *vict) { if (group::perform_group(leader, f)) ++num_followers; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); return; } } } void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { - const char *WORD_STATE[] = {" ", - "..", - "..", - " .", - " .", - " ", - " ", - " ", - ". ", - ". ", - ".", - " " + const char *WORD_STATE[] = {"При смерти", + "Оч.тяж.ран", + "Оч.тяж.ран", + " Тяж.ранен", + " Тяж.ранен", + " Ранен ", + " Ранен ", + " Ранен ", + "Лег.ранен ", + "Лег.ранен ", + "Слег.ранен", + " Невредим " }; - const char *MOVE_STATE[] = {"", - "", - ".", - " ", - " ", - ".", - ".", - " ", - " ", - " ", - ".", - " " + const char *MOVE_STATE[] = {"Истощен", + "Истощен", + "О.устал", + " Устал ", + " Устал ", + "Л.устал", + "Л.устал", + "Хорошо ", + "Хорошо ", + "Хорошо ", + "Отдохн.", + " Полон " }; auto generate_affects_string = [](CharData *k) -> std::string { std::string affects; - affects += AFF_FLAGGED(k, EAffect::kSanctuary) ? "&R" : (AFF_FLAGGED(k, EAffect::kPrismaticAura) ? "&R" : " "); - affects += AFF_FLAGGED(k, EAffect::kWaterBreath) ? "&g" : " "; - affects += AFF_FLAGGED(k, EAffect::kInvisible) ? "&C" : " "; + affects += AFF_FLAGGED(k, EAffect::kSanctuary) ? "&RО" : (AFF_FLAGGED(k, EAffect::kPrismaticAura) ? "&RП" : " "); + affects += AFF_FLAGGED(k, EAffect::kWaterBreath) ? "&gД" : " "; + affects += AFF_FLAGGED(k, EAffect::kInvisible) ? "&CН" : " "; affects += (AFF_FLAGGED(k, EAffect::kSingleLight) || AFF_FLAGGED(k, EAffect::kHolyLight) || (GET_EQ(k, EEquipPos::kLight) - && GET_OBJ_VAL(GET_EQ(k, EEquipPos::kLight), 2))) ? "&Y" : " "; - affects += AFF_FLAGGED(k, EAffect::kFly) ? "&B" : " "; - affects += mount::IsOnHorse(k) ? "&Y" : " "; + && GET_OBJ_VAL(GET_EQ(k, EEquipPos::kLight), 2))) ? "&YС" : " "; + affects += AFF_FLAGGED(k, EAffect::kFly) ? "&BЛ" : " "; + affects += mount::IsOnHorse(k) ? "&YВ" : " "; return affects; }; auto generate_debuf_string = [](CharData *k) -> std::string { std::string debuf; - debuf += AFF_FLAGGED(k, EAffect::kHold) ? "&R" : " "; - debuf += AFF_FLAGGED(k, EAffect::kSleep) ? "&B" : " "; - debuf += AFF_FLAGGED(k, EAffect::kSilence) ? "&y" : " "; - debuf += AFF_FLAGGED(k, EAffect::kDeafness) ? "&g" : " "; - debuf += AFF_FLAGGED(k, EAffect::kBlind) ? "&Y" : " "; - debuf += AFF_FLAGGED(k, EAffect::kCurse) ? "&m" : " "; - debuf += IsAffected(k, EAffect::kFever) ? "&c" : " "; + debuf += AFF_FLAGGED(k, EAffect::kHold) ? "&RХ" : " "; + debuf += AFF_FLAGGED(k, EAffect::kSleep) ? "&BС" : " "; + debuf += AFF_FLAGGED(k, EAffect::kSilence) ? "&yМ" : " "; + debuf += AFF_FLAGGED(k, EAffect::kDeafness) ? "&gГ" : " "; + debuf += AFF_FLAGGED(k, EAffect::kBlind) ? "&YБ" : " "; + debuf += AFF_FLAGGED(k, EAffect::kCurse) ? "&mП" : " "; + debuf += IsAffected(k, EAffect::kFever) ? "&cЛ" : " "; return debuf; }; @@ -249,10 +249,10 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { if (div > 0) { if (!IS_MANA_CASTER(k)) { ok2 = std::max(0, 1 + k->mem_queue.total - k->mem_queue.stored); - ok2 = ok2 * 60 / div; // + ok2 = ok2 * 60 / div; // время мема в сек } else { ok2 = std::max(0, 1 + Mana(GetRealWis(k)) - k->mem_queue.stored); - ok2 = ok2 / div; // + ok2 = ok2 / div; // время восстановления в секундах } ok = ok2 / 60; ok2 %= 60; @@ -271,19 +271,19 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { if (k->IsNpc()) { std::ostringstream buffer; if (!header) - buffer << " | | | | | \r\n"; + buffer << "Персонаж | Здоровье | Рядом | Аффект | Дебаф | Положение\r\n"; buffer << fmt::format("&B{:<20}&n|", k->get_name().substr(0, native_text::char_offset(k->get_name(), 20))); buffer << fmt::format("{}", GetWarmValueColor(k->get_hit(), k->get_real_max_hit())); buffer << fmt::format("{:<10}&n|", WORD_STATE[posi_value(k->get_hit(), k->get_real_max_hit()) + 1]); - buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&g" : "&r"); + buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&gДа" : "&rНет"); - // + // АФФЕКТЫ buffer << fmt::format(" {:<5}", generate_affects_string(k)); - buffer << fmt::format("{:<1} &n|", IsCharmExpiring(k) ? "&n" : " "); + buffer << fmt::format("{:<1} &n|", IsCharmExpiring(k) ? "&nТ" : " "); - // + // ДЕБАФЫ buffer << fmt::format(" {:<7} &n|", generate_debuf_string(k)); buffer << fmt::format(" {:<10}\r\n", position_types[(int) k->GetPosition()]); @@ -293,7 +293,7 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { } else { std::ostringstream buffer; if (!header) - buffer << " | | | | | | | | | \r\n"; + buffer << "Персонаж | Здоровье | Энергия | Рядом | Учить | Аффект | Дебаф | Кто | Строй | Положение \r\n"; std::string health_color = GetWarmValueColor(k->get_hit(), k->get_real_max_hit()); std::string move_color = GetWarmValueColor(k->get_move(), k->get_real_max_move()); @@ -306,15 +306,15 @@ void group::print_one_line(CharData *ch, CharData *k, int leader, int header) { buffer << fmt::format("{}", move_color); buffer << fmt::format("{:^9}&n|", MOVE_STATE[posi_value(k->get_move(), k->get_real_max_move()) + 1]); - buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&g" : "&r"); + buffer << fmt::format(" {:^7} &n|", ch->in_room == k->in_room ? "&gДа" : "&rНет"); buffer << fmt::format(" {:^5} &n|", generate_mem_string(k)); buffer << fmt::format(" {:<5} &n|", generate_affects_string(k)); buffer << fmt::format(" {:<7} &n|", generate_debuf_string(k)); - buffer << fmt::format(" {:^5} &n|", leader ? "" : ""); - buffer << fmt::format(" {:^5} &n|", k->IsFlagged(EPrf::kSkirmisher) ? " &g " : ""); - buffer << fmt::format(" {:<10}\r\n", k->GetEnemy() ? "" : mount::IsOnHorse(k) ? "" : position_types[(int) k->GetPosition()]); + buffer << fmt::format(" {:^5} &n|", leader ? "Лидер" : ""); + buffer << fmt::format(" {:^5} &n|", k->IsFlagged(EPrf::kSkirmisher) ? " &gДа " : "Нет"); + buffer << fmt::format(" {:<10}\r\n", k->GetEnemy() ? "Сражается" : mount::IsOnHorse(k) ? "Верхом" : position_types[(int) k->GetPosition()]); SendMsgToChar(buffer.str().c_str(), ch); } @@ -325,9 +325,9 @@ void group::print_list_group(CharData *ch) { int count = 1; k = (ch->has_master() ? ch->get_master() : ch); if (AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Ваша группа состоит из:\r\n", ch); if (AFF_FLAGGED(k, EAffect::kGroup)) { - sprintf(buf1, ": %s\r\n", GET_NAME(k)); + sprintf(buf1, "Лидер: %s\r\n", GET_NAME(k)); SendMsgToChar(buf1, ch); } @@ -335,12 +335,12 @@ void group::print_list_group(CharData *ch) { if (!AFF_FLAGGED(f, EAffect::kGroup)) { continue; } - sprintf(buf1, "%d. : %s\r\n", count, GET_NAME(f)); + sprintf(buf1, "%d. Согруппник: %s\r\n", count, GET_NAME(f)); SendMsgToChar(buf1, ch); count++; } } else { - SendMsgToChar(" ( ) !\r\n", ch); + SendMsgToChar("Но вы же не член (в лучшем смысле этого слова) группы!\r\n", ch); } } @@ -353,7 +353,7 @@ void group::print_group(CharData *ch) { ch->desc->msdp_report(msdp::constants::GROUP); if (AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Ваша группа состоит из:\r\n", ch); if (AFF_FLAGGED(k, EAffect::kGroup)) { group::print_one_line(ch, k, true, gfound++); } @@ -371,11 +371,11 @@ void group::print_group(CharData *ch) { continue; } if (!cfound) - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Ваши последователи:\r\n", ch); print_one_line(ch, f, false, cfound++); } if (!gfound && !cfound) { - SendMsgToChar(" ( ) !\r\n", ch); + SendMsgToChar("Но вы же не член (в лучшем смысле этого слова) группы!\r\n", ch); return; } if (ch->IsFlagged(EPrf::kShowGroup)) { @@ -400,7 +400,7 @@ void group::print_group(CharData *ch) { } if (!cfound) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Последователи членов вашей группы:\r\n", ch); } group::print_one_line(ch, f, false, cfound++); } @@ -419,7 +419,7 @@ void group::print_group(CharData *ch) { } if (!cfound) { - SendMsgToChar(" :\r\n", ch); + SendMsgToChar("Последователи членов вашей группы:\r\n", ch); } group::print_one_line(ch, g, false, cfound++); } @@ -437,11 +437,11 @@ void group::GoGroup(CharData *ch, char *argument) { CharData *vict; if (!str_cmp(buf, "all") - || !str_cmp(buf, "")) { + || !str_cmp(buf, "все")) { int found = 0; for (auto *f : ch->followers) { if ((f_number + found) >= group::max_group_size(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); break; } if (IsAffected(f, EAffect::kFrenzy)) { @@ -451,13 +451,13 @@ void group::GoGroup(CharData *ch, char *argument) { } if (!found) { - SendMsgToChar(", , .\r\n", ch); + SendMsgToChar("Все, кто за вами следуют, уже включены в вашу группу.\r\n", ch); } else { group::perform_group(ch, ch); } return; - } else if (!str_cmp(buf, "leader") || !str_cmp(buf, "")) { + } else if (!str_cmp(buf, "leader") || !str_cmp(buf, "лидер")) { vict = target_resolver::FindPlayerVis(ch, argument); if (vict && vict->IsNpc() @@ -473,14 +473,14 @@ void group::GoGroup(CharData *ch, char *argument) { } if (!vict) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такого персонажа.\r\n", ch); return; } else if (vict == ch) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("Вы и так лидер группы...\r\n", ch); return; } else if (!AFF_FLAGGED(vict, EAffect::kGroup) || vict->get_master() != ch) { - SendMsgToChar(ch, "%s .\r\n", GET_NAME(vict)); + SendMsgToChar(ch, "%s не является членом вашей группы.\r\n", GET_NAME(vict)); return; } change_leader(ch, vict); @@ -492,25 +492,25 @@ void group::GoGroup(CharData *ch, char *argument) { if (!vict) { SendMsgToChar(CommonMsg(ECommonMsg::kNoPerson) + "\r\n", ch); } else if ((vict->get_master() != ch) && (vict != ch)) { - act("$N2 , .", false, ch, nullptr, vict, kToChar); + act("$N2 нужно следовать за вами, чтобы стать членом вашей группы.", false, ch, nullptr, vict, kToChar); } else if (IsAffected(vict, EAffect::kFrenzy)) { - act("$N $W $W! $s!", false, ch, nullptr, vict, kToChar); + act("$N слишком агрессивн$W и непредсказуем$W! Нельзя брать $s!", false, ch, nullptr, vict, kToChar); } else { if (!AFF_FLAGGED(vict, EAffect::kGroup)) { if (vict->IsFlagged(EMobFlag::kCompanion) || mount::IsHorse(vict)) { - SendMsgToChar(" .\r\n", ch); - SendMsgToChar(" .\r\n", vict); + SendMsgToChar("Только равноправные персонажи могут быть включены в группу.\r\n", ch); + SendMsgToChar("Только равноправные персонажи могут быть включены в группу.\r\n", vict); }; if (f_number >= group::max_group_size(ch)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы больше никого не можете принять в группу.\r\n", ch); return; } group::perform_group(ch, ch); group::perform_group(ch, vict); } else if (ch != vict) { - act("$N $A .", false, ch, nullptr, vict, kToChar); - act(" $n1!", false, ch, nullptr, vict, kToVict); - act("$N $G $A $n1!", false, ch, nullptr, vict, kToNotVict | kToArenaListen); + act("$N исключен$A из состава вашей группы.", false, ch, nullptr, vict, kToChar); + act("Вы исключены из группы $n1!", false, ch, nullptr, vict, kToVict); + act("$N был$G исключен$A из группы $n1!", false, ch, nullptr, vict, kToNotVict | kToArenaListen); //AFF_FLAGS(vict).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(vict); } @@ -520,7 +520,7 @@ void group::GoGroup(CharData *ch, char *argument) { void group::GoUngroup(CharData *ch, char *argument) { CharData *tch; if (!*argument) { - sprintf(buf2, " %s.\r\n", GET_PAD(ch, 1)); + sprintf(buf2, "Вы исключены из группы %s.\r\n", GET_PAD(ch, 1)); auto copy = ch->followers; for (auto *f : copy) { if (AFF_FLAGGED(f, EAffect::kGroup)) { @@ -536,7 +536,7 @@ void group::GoUngroup(CharData *ch, char *argument) { } AFF_FLAGS(ch).unset(EAffect::kGroup); group::RemoveGroupFlags(ch); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы распустили группу.\r\n", ch); return; } auto copy2 = ch->followers; @@ -547,14 +547,14 @@ void group::GoUngroup(CharData *ch, char *argument) { && !mount::IsHorse(tch)) { //AFF_FLAGS(tch).unset(EAffectFlag::AFF_GROUP); group::RemoveGroupFlags(tch); - act("$N .", false, ch, nullptr, tch, kToChar); - act(" $n1!", false, ch, nullptr, tch, kToVict); - act("$N $G $A $n1!", false, ch, nullptr, tch, kToNotVict | kToArenaListen); + act("$N более не член вашей группы.", false, ch, nullptr, tch, kToChar); + act("Вы исключены из группы $n1!", false, ch, nullptr, tch, kToVict); + act("$N был$G изгнан$A из группы $n1!", false, ch, nullptr, tch, kToNotVict | kToArenaListen); follow::StopFollower(tch, follow::kSfEmpty); return; } } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Этот игрок не входит в состав вашей группы.\r\n", ch); } void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -562,11 +562,11 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!*argument) { if (!AFF_FLAGGED(ch, EAffect::kGroup) && !AFF_FLAGGED(ch, EAffect::kCharmed)) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("И перед кем вы отчитываетесь?\r\n", ch); return; } if (IS_MANA_CASTER(ch)) { - sprintf(buf, "%s %s : %d(%d)H, %d(%d)V, %d(%d)M\r\n", + sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V, %d(%d)M\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move(), @@ -580,13 +580,13 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; } } - sprintf(buf, "%s %s : %d(%d)H, %d(%d)V, %dL\r\n", + sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V, %dL\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move(), loyalty); } else { - sprintf(buf, "%s %s : %d(%d)H, %d(%d)V\r\n", + sprintf(buf, "%s доложил%s : %d(%d)H, %d(%d)V\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), ch->get_hit(), ch->get_real_max_hit(), ch->get_move(), ch->get_real_max_move()); @@ -603,18 +603,18 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (k != ch && !AFF_FLAGGED(k, EAffect::kDeafness)) { SendMsgToChar(buf, k); } - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы доложили о состоянии всем членам вашей группы.\r\n", ch); } else { - if (CompareParam(argument, "")) { + if (CompareParam(argument, "умения")) { if (ch->followers.empty()) { - SendMsgToChar(ch, " ."); + SendMsgToChar(ch, "За вами никто не следует."); return; } for (auto *f : ch->followers) { if (IsCharmice(f)) { std::vector skills; - SendMsgToChar(ch, "%s %s :", utils::CAP(f->get_name()).c_str(), grammar::SexEnding((f)->get_sex(), 1)); + SendMsgToChar(ch, "%s доложил%s свои умения:", utils::CAP(f->get_name()).c_str(), grammar::SexEnding((f)->get_sex(), 1)); for (const auto &skill : MUD::Skills()) { if (skill.IsValid() && GetSkill(f, skill.GetId())) { skills.emplace_back(skill.GetName()); @@ -623,11 +623,11 @@ void do_report(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { SendMsgToChar(ch, "&C %s&n\r\n", utils::OutWordsList(skills, ch->player_specials->saved.stringLength).c_str()); } else { - SendMsgToChar(ch, "%s , .\r\n", utils::CAP(f->get_name()).c_str()); + SendMsgToChar(ch, "%s не подчиняется вам, и не хочет ничего докладывать.\r\n", utils::CAP(f->get_name()).c_str()); } } } else { - SendMsgToChar(ch, " ?\r\n"); + SendMsgToChar(ch, "Кому чего доложить?\r\n"); } } } @@ -649,19 +649,19 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, if (is_number(buf)) { amount = atoi(buf); if (amount <= 0) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("И как вы это планируете сделать?\r\n", ch); return; } if (amount > currencies::GetHand(*ch, currency_vnum)) { - SendMsgToChar(" ?.\r\n", ch); + SendMsgToChar("И где бы взять вам столько денег?.\r\n", ch); return; } k = ch->has_master() ? ch->get_master() : ch; - // issue #3669: - -- - // ( !k->IsNpc()), - // . + // issue #3669: лидера-моба считать нельзя -- долю получают только игроки + // (ниже стоит !k->IsNpc()), иначе его доля списывалась бы у делящего и + // не доставалась никому. if (AFF_FLAGGED(k, EAffect::kGroup) && !k->IsNpc() && (k->in_room == ch->in_room)) { @@ -670,7 +670,7 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, num = 0; } - int absent = 0; // issue #3669: -- + int absent = 0; // issue #3669: члены группы вне комнаты -- долю не получают if (AFF_FLAGGED(k, EAffect::kGroup) && !k->IsNpc() && k->in_room != ch->in_room) { @@ -692,14 +692,14 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, share = amount / num; rest = amount % num; } else { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("С кем вы хотите разделить это добро?\r\n", ch); return; } - // issue #3669: -- . - // ӣ , " 0". + // issue #3669: делить нечего -- каждому вышло бы по нулю. Молча оставляем + // всё делящему, а не рассылаем группе "вам досталось 0". if (share <= 0) { - SendMsgToChar(ch, "%d %s %d -- .\r\n", + SendMsgToChar(ch, "%d %s на %d не разделить -- оставили себе.\r\n", amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), num); return; } @@ -707,7 +707,7 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, currencies::RemoveHand(*ch, currency_vnum, share * (num - 1)); - sprintf(buf, "%s %s %d %s; %d.\r\n", + sprintf(buf, "%s разделил%s %d %s; вам досталось %d.\r\n", GET_NAME(ch), grammar::SexEnding((ch)->get_sex(), 1), amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), share); if (AFF_FLAGGED(k, EAffect::kGroup) && k->in_room == ch->in_room && !k->IsNpc() && k != ch) { SendMsgToChar(buf, k); @@ -722,29 +722,29 @@ void group::do_split(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/, currencies::AddHand(*f, currency_vnum, share - (currency_vnum == currencies::kGoldVnum ? ClanSystem::do_gold_tax(f, share) : 0), false, true); } } - sprintf(buf, " %d %s %d - %d .\r\n", + sprintf(buf, "Вы разделили %d %s на %d - по %d каждому.\r\n", amount, MUD::Currency(currency_vnum).GetNameWithAmount(amount, grammar::ECase::kAcc).c_str(), num, share); if (rest) { sprintf(buf + strlen(buf), - " %d %s ( ) .\r\n", + "Как истинный еврей вы оставили %d %s (которые не смогли разделить нацело) себе.\r\n", rest, MUD::Currency(currency_vnum).GetNameWithAmount(rest, grammar::ECase::kAcc).c_str()); } - // issue #3669: , . , - // , . + // issue #3669: делится только на тех, кто рядом. Иначе непонятно, почему + // в группе народу больше, чем долей. if (absent > 0) { sprintf(buf + strlen(buf), - " : %d -- .\r\n", absent); + "Членов группы вне комнаты: %d -- доля им не досталась.\r\n", absent); } SendMsgToChar(buf, ch); - // - , + // клан-налог лутера с той части, которая пошла каждому в группе if (currency_vnum == currencies::kGoldVnum) { const long clan_tax = ClanSystem::do_gold_tax(ch, share); currencies::RemoveHand(*ch, currencies::kGold, clan_tax); } } else { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Сколько и чего вы хотите разделить?\r\n", ch); return; } } diff --git a/src/gameplay/mechanics/obj_sets_olc.cpp b/src/gameplay/mechanics/obj_sets_olc.cpp index c1d1e2d9d..1e09f69ff 100644 --- a/src/gameplay/mechanics/obj_sets_olc.cpp +++ b/src/gameplay/mechanics/obj_sets_olc.cpp @@ -38,20 +38,20 @@ using namespace obj_sets; namespace obj_sets_olc { -/// , -/// ( CON_SEDIT) +/// класс для редактирования сета в олц, содержит состояние редактирования +/// (наружу торчит только CON_SEDIT) и всю временную инфу под это дело class sedit { public: sedit() : state(-1), new_entry(false), obj_edit(-1), activ_edit(-1), apply_edit(-1) {}; - // ( ) + // стадия редактирования (в какой менюшке) int state; - // + // правка глобальных сообщений obj_sets::SetMsgNode msg_edit; - // + // редактируемый сет obj_sets::SetNode olc_set; - // + // идет создание нового сета bool new_entry; void parse_global_msg(CharData *ch, const char *arg); @@ -85,11 +85,11 @@ class sedit { void show_global_msg(CharData *ch); private: - // - + // если правится предмет - его внум int obj_edit; - // - + // если правится активатор - его размер int activ_edit; - // - + // если правится аффект - его индекс в списке size_t apply_edit; void show_activ_ench_vnum(CharData *ch); @@ -99,10 +99,10 @@ class sedit { void show_activ_affects(CharData *ch); void show_activ_edit(CharData *ch); void show_obj_edit(CharData *ch); - // - , - // + // проверка поменялось ли что-то в сете, + // чтобы не спамить про сохранения при выходе bool changed(); - // changed() + // для сравнения в changed() bool operator!=(const sedit &r) const { return olc_set != r.olc_set; } @@ -154,7 +154,7 @@ enum { STATE_GLBMSG_ROOM_OFF }; -// + MAIN_TOTAL +// распечатка главного меню + смещение для следующих пунктов через MAIN_TOTAL enum { MAIN_SET_REMOVE = 1, MAIN_ENABLED, @@ -169,11 +169,11 @@ enum { }; namespace { -const auto MISSING_OBJECT_NAME = "&R< VNUM >&n"; +const auto MISSING_OBJECT_NAME = "&R<объект с таким VNUM не существует>&n"; } -/// , -/// , , +/// распечатка форматированного списка шмоток сета, форматирование идет как +/// как по столбцам, так и по длине имени и внума шмоток, вобщем чтоб красиво std::string main_menu_objlist(const SetNode &set, int menu) { std::string out; char buf_[128]; @@ -234,37 +234,37 @@ std::string main_menu_objlist(const SetNode &set, int menu) { const char *main_menu_str(SetNode &olc_set, int num) { static char buf_[1024]; switch (num) { - case MAIN_SET_REMOVE: return " "; + case MAIN_SET_REMOVE: return "Удалить набор"; case MAIN_ENABLED: - snprintf(buf_, sizeof(buf_), " : %s%s%s", kColorCyn, - olc_set.enabled ? "" : "", kColorNrm); + snprintf(buf_, sizeof(buf_), "Статус : %s%s%s", kColorCyn, + olc_set.enabled ? "активен" : "неактивен", kColorNrm); break; case MAIN_NAME: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Имя : %s", olc_set.name.c_str()); break; case MAIN_ALIAS: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Алиасы : %s", olc_set.alias.c_str()); break; case MAIN_COMMENT: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Комментарий : %s", olc_set.comment.c_str()); break; case MAIN_MSG_CHAR_ON: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Активатор персонажу : %s", olc_set.messages.char_on_msg.c_str()); break; case MAIN_MSG_CHAR_OFF: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Деактиватор персонажу : %s", olc_set.messages.char_off_msg.c_str()); break; case MAIN_MSG_ROOM_ON: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Активатор в комнату : %s", olc_set.messages.room_on_msg.c_str()); break; case MAIN_MSG_ROOM_OFF: - snprintf(buf_, sizeof(buf_), " : %s", + snprintf(buf_, sizeof(buf_), "Деактиватор в комнату : %s", olc_set.messages.room_off_msg.c_str()); break; default: return ""; @@ -279,17 +279,17 @@ void sedit::show_main(CharData *ch) { std::string out("\r\n"); if (new_entry) { - out += " \r\n"; + out += "Создание нового набора предметов\r\n"; } else { size_t idx = setidx_by_uid(olc_set.uid); if (idx >= sets_list.size()) { - SendMsgToChar(" : .\r\n", ch); + SendMsgToChar("Редактирование прервано: набор был удален.\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; return; } snprintf(buf_, sizeof(buf_), - " #%llu\r\n", static_cast(idx + 1)); + "Редактирование набора предметов #%llu\r\n", static_cast(idx + 1)); out += buf_; } int i = 1; @@ -299,18 +299,18 @@ void sedit::show_main(CharData *ch) { main_menu_str(olc_set, i)); out += buf_; } - // - snprintf(buf_, sizeof(buf_), "\r\n%s%2d%s) ()\r\n", + // предметы + snprintf(buf_, sizeof(buf_), "\r\n%s%2d%s) Добавить предмет(ы)\r\n", kColorGrn, i++, kColorNrm); out += buf_; out += main_menu_objlist(olc_set, i); i += static_cast(olc_set.obj_list.size()); - // + // активаторы snprintf(buf_, sizeof(buf_), - "\r\n%s%2d%s) \r\n", + "\r\n%s%2d%s) Распечатать сумму активаторов\r\n", kColorGrn, i++, kColorNrm); out += buf_; - snprintf(buf_, sizeof(buf_), "%s%2d%s) \r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) Добавить активатор\r\n", kColorGrn, i++, kColorNrm); out += buf_; for (auto & k : olc_set.activ_list) { @@ -318,13 +318,13 @@ void sedit::show_main(CharData *ch) { std::string prof; PrinSetClasses(k.second.prof, prof); snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s%d (%s)%s\r\n", + "%s%2d%s) Редактировать активатор: %s%d (%s)%s\r\n", kColorGrn, i++, kColorNrm, kColorCyn, k.first, prof.c_str(), kColorNrm); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s%d%s\r\n", + "%s%2d%s) Редактировать активатор: %s%d%s\r\n", kColorGrn, i++, kColorNrm, kColorCyn, k.first, kColorNrm); } @@ -332,8 +332,8 @@ void sedit::show_main(CharData *ch) { } snprintf(buf_, sizeof(buf_), - "\r\n%s%2d%s) Q()\r\n" - " : ", + "\r\n%s%2d%s) Выйти Q(В)\r\n" + "Ваш выбор : ", kColorGrn, i++, kColorNrm); out += buf_; @@ -346,7 +346,7 @@ void sedit::show_obj_edit(CharData *ch) { auto obj = olc_set.obj_list.find(obj_edit); if (obj == olc_set.obj_list.end()) { SendMsgToChar(ch, - ": %s:%d (%s).\r\n", + "Ошибка: предмет не в наборе %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; @@ -359,15 +359,15 @@ void sedit::show_obj_edit(CharData *ch) { : obj_proto[rnum]->get_short_description().c_str(); char buf_[2048]; snprintf(buf_, sizeof(buf_), - "\r\n '%s'\r\n" - "%s 1%s) \r\n" - "%s 2%s) (vnum) : %s%d%s\r\n" - "%s 3%s) : %s\r\n" - "%s 4%s) : %s\r\n" - "%s 5%s) : %s\r\n" - "%s 6%s) : %s\r\n" - "%s 7%s) Q()\r\n" - " : ", + "\r\nРедактирование предмета '%s'\r\n" + "%s 1%s) Удалить из набора\r\n" + "%s 2%s) Виртуальный номер (vnum) : %s%d%s\r\n" + "%s 3%s) Активатор персонажу : %s\r\n" + "%s 4%s) Деактиватор персонажу : %s\r\n" + "%s 5%s) Активатор в комнату : %s\r\n" + "%s 6%s) Деактиватор в комнату : %s\r\n" + "%s 7%s) В главное меню Q(В)\r\n" + "Ваш выбор : ", name, kColorGrn, kColorNrm, kColorGrn, kColorNrm, @@ -385,7 +385,7 @@ void sedit::show_activ_edit(CharData *ch) { auto i = olc_set.activ_list.find(activ_edit); if (i == olc_set.activ_list.end()) { - SendMsgToChar(ch, ": %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); + SendMsgToChar(ch, "Ошибка: активатор не найден %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; } @@ -398,17 +398,17 @@ void sedit::show_activ_edit(CharData *ch) { if (!activ.prof.all()) { PrinSetClasses(activ.prof, prof_str); } else { - prof_str = ""; + prof_str = "все"; } std::string out; char buf_[2048]; snprintf(buf_, sizeof(buf_), - "\r\n \r\n" - "%s 1%s) \r\n" - "%s 2%s) : %s%d%s\r\n" - "%s 3%s) : %s%s%s\r\n" - "%s 4%s) : %s%s%s\r\n", + "\r\nРедактирование активатора\r\n" + "%s 1%s) Удалить из набора\r\n" + "%s 2%s) Количество предметов : %s%d%s\r\n" + "%s 3%s) Профессии : %s%s%s\r\n" + "%s 4%s) Аффекты : %s%s%s\r\n", kColorGrn, kColorNrm, kColorGrn, kColorNrm, kColorCyn, activ_edit, kColorNrm, @@ -421,26 +421,26 @@ void sedit::show_activ_edit(CharData *ch) { int cnt = 5; for (auto apply : activ.apply) { if (apply.location > 0) { - snprintf(buf_, sizeof(buf_), "%s%2d%s) : %s", + snprintf(buf_, sizeof(buf_), "%s%2d%s) Наводимый аффект : %s", kColorGrn, cnt++, kColorNrm, print_obj_affects(apply).c_str()); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s%s%s\r\n", + "%s%2d%s) Наводимый аффект : %s%s%s\r\n", kColorGrn, cnt++, kColorNrm, - kColorCyn, "", kColorNrm); + kColorCyn, "ничего", kColorNrm); } out += buf_; } if (MUD::Skills().IsValid(activ.skill.first)) { snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s%+d to %s%s\r\n", + "%s%2d%s) Изменяемое умение : %s%+d to %s%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.skill.second, MUD::Skill(activ.skill.first).GetName(), kColorNrm); } else { - snprintf(buf_, sizeof(buf_), "%s%2d%s) : \r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) Изменяемое умение : нет\r\n", kColorGrn, cnt++, kColorNrm); } out += buf_; @@ -451,45 +451,45 @@ void sedit::show_activ_edit(CharData *ch) { (rnum >= 0 ? obj_proto[rnum]->get_short_description().c_str() : ""); if (obj_proto[rnum]->get_type() == EObjType::kWeapon) { snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s[%d] %s %+d, %+dD%+d%s\r\n", + "%s%2d%s) Зачарование предмета : %s[%d] %s вес %+d, кубики %+dD%+d%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.enchant.first, name, activ.enchant.second.weight, activ.enchant.second.ndice, activ.enchant.second.sdice, kColorNrm); } else { snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s[%d] %s %+d%s\r\n", + "%s%2d%s) Зачарование предмета : %s[%d] %s вес %+d%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.enchant.first, name, activ.enchant.second.weight, kColorNrm); } } else { - snprintf(buf_, sizeof(buf_), "%s%2d%s) : \r\n", + snprintf(buf_, sizeof(buf_), "%s%2d%s) Зачарование предмета: нет\r\n", kColorGrn, cnt++, kColorNrm); } out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) . : %s%+d%%%s\r\n", + "%s%2d%s) Увеличение физ. урона : %s%+d%%%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.bonus.phys_dmg, kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) . : %s%+d%%%s\r\n", + "%s%2d%s) Увеличение маг. урона : %s%+d%%%s\r\n", kColorGrn, cnt++, kColorNrm, kColorCyn, activ.bonus.mage_dmg, kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) : %s%s%s\r\n", + "%s%2d%s) Актив на мобах : %s%s%s\r\n", kColorGrn, cnt++, kColorNrm, - kColorCyn, (activ.npc ? "" : ""), kColorNrm); + kColorCyn, (activ.npc ? "Да" : "Нет"), kColorNrm); out += buf_; snprintf(buf_, sizeof(buf_), - "%s%2d%s) Q()\r\n" - " : ", + "%s%2d%s) В главное меню Q(В)\r\n" + "Ваш выбор : ", kColorGrn, cnt++, kColorNrm); out += buf_; @@ -521,7 +521,7 @@ void sedit::save_olc(CharData *ch) { obj_sets::save(); init_obj_index(); } else { - SendMsgToChar(" : .", ch); + SendMsgToChar("Ошибка сохранения: набор был удален.", ch); } } @@ -534,19 +534,19 @@ void parse_main_exit(CharData *ch, const char *arg) { case rus::kDeUpper: ch->desc->state = EConState::kPlaying; ch->desc->sedit->save_olc(ch); ch->desc->sedit.reset(); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Изменения сохранены.\r\n", ch); break; case 'n': case 'N': case rus::kEn: case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); break; default: SendMsgToChar( - " !\r\n" - " ? Y()/N() : ", ch); + "Неверный выбор!\r\n" + "Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); break; } } @@ -563,7 +563,7 @@ void parse_set_remove(CharData *ch, const char *arg) { sets_list.erase(i); obj_sets::save(); init_obj_index(); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Набор удален.\r\n", ch); break; } } @@ -574,13 +574,13 @@ void parse_set_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); ch->desc->sedit->show_main(ch); break; default: SendMsgToChar( - " !\r\n" - " Y()/N() : ", ch); + "Неверный выбор!\r\n" + "Подтвердите удаление набора Y(Д)/N(Н) : ", ch); break; } } @@ -595,7 +595,7 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { auto i = olc_set.obj_list.find(obj_edit); if (i != olc_set.obj_list.end()) { olc_set.obj_list.erase(i); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Предмет удален из набора.\r\n", ch); } obj_edit = -1; show_main(ch); @@ -604,11 +604,11 @@ void sedit::parse_obj_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); show_obj_edit(ch); break; - default: SendMsgToChar(" !\r\n", ch); - SendMsgToChar(" Y()/N() :", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar("Подтвердите удаление предмета Y(Д)/N(Н) :", ch); break; } } @@ -623,7 +623,7 @@ void sedit::parse_activ_remove(CharData *ch, const char *arg) { auto i = olc_set.activ_list.find(activ_edit); if (i != olc_set.activ_list.end()) { olc_set.activ_list.erase(i); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Активатор удален из набора.\r\n", ch); } activ_edit = -1; show_main(ch); @@ -632,11 +632,11 @@ void sedit::parse_activ_remove(CharData *ch, const char *arg) { case 'n': case 'N': case rus::kEn: - case rus::kEnUpper: SendMsgToChar(" .\r\n", ch); + case rus::kEnUpper: SendMsgToChar("Удаление отменено.\r\n", ch); show_activ_edit(ch); break; - default: SendMsgToChar(" !\r\n", ch); - SendMsgToChar(" Y()/N() :", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); + SendMsgToChar("Подтвердите удаление активатора Y(Д)/N(Н) :", ch); break; } } @@ -646,13 +646,13 @@ void sedit::show_global_msg(CharData *ch) { char buf_[1024]; snprintf(buf_, sizeof(buf_), - "\r\n \r\n" - "%s 1%s) : %s\r\n" - "%s 2%s) : %s\r\n" - "%s 3%s) : %s\r\n" - "%s 4%s) : %s\r\n" - "%s 5%s) Q()\r\n" - " : ", + "\r\nРедактирование глобальных сообщений\r\n" + "%s 1%s) Активатор персонажу : %s\r\n" + "%s 2%s) Деактиватор персонажу : %s\r\n" + "%s 3%s) Активатор в комнату : %s\r\n" + "%s 4%s) Деактиватор в комнату : %s\r\n" + "%s 5%s) Выйти Q(В)\r\n" + "Ваш выбор : ", kColorGrn, kColorNrm, msg_edit.char_on_msg.c_str(), kColorGrn, kColorNrm, msg_edit.char_off_msg.c_str(), kColorGrn, kColorNrm, msg_edit.room_on_msg.c_str(), @@ -664,7 +664,7 @@ void sedit::show_global_msg(CharData *ch) { void sedit::parse_global_msg(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Неверный выбор!\r\n", ch); show_global_msg(ch); return; } @@ -675,15 +675,15 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: if (msg_edit != global_msg) { - SendMsgToChar(" ? Y()/N() : ", ch); + SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_GLOBAL_MSG_EXIT; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_global_msg(ch); break; } @@ -693,29 +693,29 @@ void sedit::parse_global_msg(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case 1: SendMsgToChar(" : ", ch); + case 1: SendMsgToChar("Сообщение персонажу при активации : ", ch); state = STATE_GLBMSG_CHAR_ON; break; - case 2: SendMsgToChar(" : ", ch); + case 2: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); state = STATE_GLBMSG_CHAR_OFF; break; - case 3: SendMsgToChar(" : ", ch); + case 3: SendMsgToChar("Сообщение в комнату при активации : ", ch); state = STATE_GLBMSG_ROOM_ON; break; - case 4: SendMsgToChar(" : ", ch); + case 4: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); state = STATE_GLBMSG_ROOM_OFF; break; case 5: if (msg_edit != global_msg) { - SendMsgToChar(" ? Y()/N() : ", ch); + SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_GLOBAL_MSG_EXIT; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_global_msg(ch); break; } @@ -731,19 +731,19 @@ void parse_global_msg_exit(CharData *ch, const char *arg) { global_msg = ch->desc->sedit->msg_edit; obj_sets::save(); ch->desc->sedit.reset(); - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Изменения сохранены.\r\n", ch); break; case 'n': case 'N': case rus::kEn: case rus::kEnUpper: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); break; default: SendMsgToChar( - " !\r\n" - " ? Y()/N() : ", ch); + "Неверный выбор!\r\n" + "Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); break; } } @@ -751,7 +751,7 @@ void parse_global_msg_exit(CharData *ch, const char *arg) { void sedit::parse_main(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Неверный выбор!\r\n", ch); show_main(ch); return; } @@ -762,15 +762,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: if (new_entry || changed()) { - SendMsgToChar(" ? Y()/N() : ", ch); + SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_MAIN_EXIT; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_main(ch); break; } @@ -780,33 +780,33 @@ void sedit::parse_main(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case MAIN_SET_REMOVE: SendMsgToChar(" Y()/N() : ", ch); + case MAIN_SET_REMOVE: SendMsgToChar("Подтвердите удаление набора Y(Д)/N(Н) : ", ch); state = STATE_SET_REMOVE; return; case MAIN_ENABLED: olc_set.enabled = !olc_set.enabled; show_main(ch); return; - case MAIN_NAME: SendMsgToChar(" (, ) : ", ch); + case MAIN_NAME: SendMsgToChar("Имя набора (опознание, справка сеты) : ", ch); state = STATE_NAME; return; case MAIN_ALIAS: SendMsgToChar( - " ( / ) : ", ch); + "Алиасы набора для справки (через запятую и/или пробел) : ", ch); state = STATE_ALIAS; return; - case MAIN_COMMENT: SendMsgToChar(" 40 ( slist) : ", ch); + case MAIN_COMMENT: SendMsgToChar("Ваш комментарий до 40 символов (виден по slist) : ", ch); state = STATE_COMMENT; return; - case MAIN_MSG_CHAR_ON: SendMsgToChar(" : ", ch); + case MAIN_MSG_CHAR_ON: SendMsgToChar("Сообщение персонажу при активации : ", ch); state = STATE_SETMSG_CHAR_ON; return; - case MAIN_MSG_CHAR_OFF: SendMsgToChar(" : ", ch); + case MAIN_MSG_CHAR_OFF: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); state = STATE_SETMSG_CHAR_OFF; return; - case MAIN_MSG_ROOM_ON: SendMsgToChar(" : ", ch); + case MAIN_MSG_ROOM_ON: SendMsgToChar("Сообщение в комнату при активации : ", ch); state = STATE_SETMSG_ROOM_ON; return; - case MAIN_MSG_ROOM_OFF: SendMsgToChar(" : ", ch); + case MAIN_MSG_ROOM_OFF: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); state = STATE_SETMSG_ROOM_OFF; return; default: break; @@ -817,15 +817,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { const unsigned NUM_ADD_ACTIV = NUM_TOTAL_ACTIV + 1; const unsigned NUM_QUIT = NUM_ADD_ACTIV + static_cast(olc_set.activ_list.size()) + 1; - // , + // после статичного меню идут предметы, за ними активаторы if (num == NUM_ADD_OBJ) { SendMsgToChar( - "Vnum ( ) : ", ch); + "Vnum добавляемого предмета (несколько через пробел) : ", ch); state = STATE_OBJ_ADD; } else if (!olc_set.obj_list.empty() && num > NUM_ADD_OBJ && num <= NUM_ADD_OBJ + olc_set.obj_list.size()) { - // + // редактирование предмета const unsigned offset = NUM_ADD_OBJ + 1; auto i = olc_set.obj_list.begin(); if (num > offset) { @@ -836,16 +836,16 @@ void sedit::parse_main(CharData *ch, const char *arg) { } else if (num == NUM_TOTAL_ACTIV) { state = STATE_TOTAL_ACTIV; SendMsgToChar(print_total_activ(olc_set), ch); - SendMsgToChar(" : ", ch); + SendMsgToChar("Введите любую команду для продолжения : ", ch); } else if (num == NUM_ADD_ACTIV) { SendMsgToChar(ch, - " - (%u-%u) : ", + "Укажите кол-во предметов для активации (%u-%u) : ", MIN_ACTIVE_SIZE, MAX_ACTIVE_SIZE); state = STATE_ACTIV_ADD; } else if (!olc_set.activ_list.empty() && num > NUM_ADD_ACTIV && num <= NUM_ADD_ACTIV + olc_set.activ_list.size()) { - // + // редактирование активатора const unsigned offset = NUM_ADD_ACTIV + 1; auto i = olc_set.activ_list.begin(); if (num > offset) { @@ -855,15 +855,15 @@ void sedit::parse_main(CharData *ch, const char *arg) { show_activ_edit(ch); } else if (num == NUM_QUIT) { if (new_entry || changed()) { - SendMsgToChar(" ? Y()/N() : ", ch); + SendMsgToChar("Вы хотите сохранить изменения? Y(Д)/N(Н) : ", ch); state = STATE_MAIN_EXIT; } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Редактирование отменено.\r\n", ch); ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; } } else { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Неверный выбор!\r\n", ch); show_main(ch); } } @@ -871,7 +871,7 @@ void sedit::parse_main(CharData *ch, const char *arg) { void sedit::parse_setname(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Имя набора удалено.\r\n", ch); olc_set.name.clear(); } else { olc_set.name = arg; @@ -882,7 +882,7 @@ void sedit::parse_setname(CharData *ch, const char *arg) { void sedit::parse_setalias(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Алиас набора удален.\r\n", ch); olc_set.alias.clear(); } else { olc_set.alias = arg; @@ -893,7 +893,7 @@ void sedit::parse_setalias(CharData *ch, const char *arg) { void sedit::parse_setcomment(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Комментарий набора удален.\r\n", ch); olc_set.comment.clear(); } else { olc_set.comment = arg; @@ -902,7 +902,7 @@ void sedit::parse_setcomment(CharData *ch, const char *arg) { show_main(ch); } -/// +/// чтобы в одном месте срау обработать все три вида сообщений enum { PARSE_GLB_MSG, PARSE_SET_MSG, PARSE_OBJ_MSG }; void sedit::parse_setmsg(CharData *ch, const char *arg) { @@ -932,25 +932,25 @@ void sedit::parse_setmsg(CharData *ch, const char *arg) { case STATE_OBJMSG_CHAR_ON: case STATE_GLBMSG_CHAR_ON: if ((msg.char_on_msg = arg).empty()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Сообщение активатора персонажу удалено.\r\n", ch); break; case STATE_SETMSG_CHAR_OFF: case STATE_OBJMSG_CHAR_OFF: case STATE_GLBMSG_CHAR_OFF: if ((msg.char_off_msg = arg).empty()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Сообщение деактиватора персонажу удалено.\r\n", ch); break; case STATE_SETMSG_ROOM_ON: case STATE_OBJMSG_ROOM_ON: case STATE_GLBMSG_ROOM_ON: if ((msg.room_on_msg = arg).empty()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Сообщение активатора в комнату удалено.\r\n", ch); break; case STATE_SETMSG_ROOM_OFF: case STATE_OBJMSG_ROOM_OFF: case STATE_GLBMSG_ROOM_OFF: if ((msg.room_off_msg = arg).empty()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Сообщение деактиватора в комнату удалено.\r\n", ch); break; default: break; } @@ -970,16 +970,16 @@ void sedit::parse_activ_add(CharData *ch, const char *arg) { if (!*arg || !a_isdigit(*arg) || (num = atoi(arg)) < MIN_ACTIVE_SIZE || num > MAX_ACTIVE_SIZE) { - SendMsgToChar(" - .\r\n", ch); + SendMsgToChar("Некорректное кол-во предметов для активации.\r\n", ch); show_main(ch); return; } auto i = olc_set.activ_list.find(num); if (i != olc_set.activ_list.end()) { - SendMsgToChar(ch, " %d %s.\r\n", + SendMsgToChar(ch, "В наборе уже есть активатор на %d %s.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } else { - SendMsgToChar(ch, " %d %s .\r\n", + SendMsgToChar(ch, "Активатор на %d %s добавлен в набор.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); ActivNode node; // GCC 4.4 @@ -992,7 +992,7 @@ void sedit::parse_activ_add(CharData *ch, const char *arg) { void sedit::show_activ_ench_vnum(CharData *ch) { state = STATE_ACTIV_ENCH_VNUM; std::string out = main_menu_objlist(olc_set, 1); - out += " vnum (0 - , - ) :"; + out += "Укажите vnum предмета (0 - удалить и выйти, пустой ввод - выход) :"; SendMsgToChar(out, ch); } @@ -1028,12 +1028,12 @@ void sedit::parse_activ_ench_vnum(CharData *ch, const char *arg) { } else { rnum = GetObjRnum(vnum); if (rnum < 0) { - SendMsgToChar(ch, " vnum %d .\r\n", vnum); + SendMsgToChar(ch, "Предметов с vnum %d не существует.\r\n", vnum); show_activ_ench_vnum(ch); return; } else if (olc_set.obj_list.find(vnum) == olc_set.obj_list.end()) { SendMsgToChar(ch, - " vnum %d.\r\n", vnum); + "В данном наборе нет предмета с vnum %d.\r\n", vnum); show_activ_ench_vnum(ch); return; } @@ -1044,12 +1044,12 @@ void sedit::parse_activ_ench_vnum(CharData *ch, const char *arg) { if (rnum >= 0 && obj_proto[rnum]->get_type() == EObjType::kWeapon) { state = STATE_ACTIV_ENCH_NDICE; - SendMsgToChar(" (0 - ) :", ch); + SendMsgToChar("Укажите изменение бросков кубика (0 - без изменений) :", ch); } else { olc_set.activ_list.at(activ_edit).enchant.second.ndice = 0; olc_set.activ_list.at(activ_edit).enchant.second.sdice = 0; state = STATE_ACTIV_ENCH_WEIGHT; - SendMsgToChar(" (0 - ) :", ch); + SendMsgToChar("Укажите прибавляемый вес (0 - без изменений) :", ch); } } @@ -1071,7 +1071,7 @@ void sedit::parse_activ_ench_ndice(CharData *ch, const char *arg) { std::max(std::min(ndice, 100), -100); state = STATE_ACTIV_ENCH_SDICE; - SendMsgToChar(" (0 - ) :", ch); + SendMsgToChar("Укажите изменение граней кубиков (0 - без изменений) :", ch); } void sedit::parse_activ_ench_sdice(CharData *ch, const char *arg) { @@ -1082,14 +1082,14 @@ void sedit::parse_activ_ench_sdice(CharData *ch, const char *arg) { std::max(std::min(sdice, 100), -100); state = STATE_ACTIV_ENCH_WEIGHT; - SendMsgToChar(" (0 - ) :", ch); + SendMsgToChar("Укажите прибавляемый вес (0 - без изменений) :", ch); } void sedit::parse_obj_add(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный виртуальный номер предмета.\r\n", ch); show_main(ch); return; } @@ -1101,16 +1101,16 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { const int rnum = GetObjRnum(vnum); if (olc_set.obj_list.size() >= MAX_OBJ_LIST) { SendMsgToChar( - " - .\r\n", ch); + "Набор уже содержит максимальное кол-во предметов.\r\n", ch); } else if (rnum < 0) { - SendMsgToChar(ch, " vnum %d .\r\n", vnum); + SendMsgToChar(ch, "Предметов с vnum %d не существует.\r\n", vnum); } else if (is_duplicate(olc_set.uid, vnum)) { SendMsgToChar(ch, - " '%s' .\r\n", + "Предмет '%s' уже является частью другого набора.\r\n", obj_proto[rnum]->get_short_description().c_str()); } else if (!verify_wear_flag(obj_proto[rnum])) { SendMsgToChar(ch, - " '%s' .\r\n", + "Предмет '%s' имеет запрещенный слот для надевания.\r\n", obj_proto[rnum]->get_short_description().c_str()); } else { SetMsgNode empty_msg; @@ -1118,7 +1118,7 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { //olc_set.obj_list.emplace(vnum, empty_msg); olc_set.obj_list.insert(std::make_pair(vnum, empty_msg)); SendMsgToChar(ch, - " '%s' .\r\n", + "Предмет '%s' добавлен в набор.\r\n", obj_proto[rnum]->get_short_description().c_str()); } } @@ -1129,7 +1129,7 @@ void sedit::parse_obj_add(CharData *ch, const char *arg) { void sedit::parse_obj_edit(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Неверный выбор!\r\n", ch); show_obj_edit(ch); return; } @@ -1140,7 +1140,7 @@ void sedit::parse_obj_edit(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: show_main(ch); break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_obj_edit(ch); break; } @@ -1149,27 +1149,27 @@ void sedit::parse_obj_edit(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { - case 1: SendMsgToChar(" Y()/N() :", ch); + case 1: SendMsgToChar("Подтвердите удаление предмета Y(Д)/N(Н) :", ch); state = STATE_OBJ_REMOVE; break; - case 2: SendMsgToChar(" : ", ch); + case 2: SendMsgToChar("Виртуальный номер нового предмета : ", ch); state = STATE_OBJ_CHANGE; break; - case 3: SendMsgToChar(" : ", ch); + case 3: SendMsgToChar("Сообщение персонажу при активации : ", ch); state = STATE_OBJMSG_CHAR_ON; break; - case 4: SendMsgToChar(" : ", ch); + case 4: SendMsgToChar("Сообщение персонажу при деактивации : ", ch); state = STATE_OBJMSG_CHAR_OFF; break; - case 5: SendMsgToChar(" : ", ch); + case 5: SendMsgToChar("Сообщение в комнату при активации : ", ch); state = STATE_OBJMSG_ROOM_ON; break; - case 6: SendMsgToChar(" : ", ch); + case 6: SendMsgToChar("Сообщение в комнату при деактивации : ", ch); state = STATE_OBJMSG_ROOM_OFF; break; case 7: show_main(ch); break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_obj_edit(ch); break; } @@ -1204,7 +1204,7 @@ void sedit::show_activ_skill(CharData *ch) { } SendMsgToChar(out, ch); SendMsgToChar( - "\r\n (0 - ) : ", ch); + "\r\nУкажите номер и уровень владения умением (0 - конец) : ", ch); } void sedit::show_activ_prof(CharData *ch) { @@ -1221,17 +1221,17 @@ void sedit::show_activ_prof(CharData *ch) { } snprintf(buf_, sizeof(buf_), - "%s%2llu%s) \r\n" - "%s%2llu%s) \r\n", + "%s%2llu%s) Сбросить все\r\n" + "%s%2llu%s) Установить все\r\n", kColorGrn, static_cast(bits.size() + 1), kColorNrm, kColorGrn, static_cast(bits.size() + 2), kColorNrm); out += buf_; - snprintf(buf_, sizeof(buf_), " : %s", kColorCyn); + snprintf(buf_, sizeof(buf_), "Текущие профессии : %s", kColorCyn); out += buf_; PrinSetClasses(bits, out, true); out += kColorNrm; - out += "\r\n (0 - ) : "; + out += "\r\nВыберите профессии для данного активатора (0 - выход) : "; SendMsgToChar(out, ch); } @@ -1239,7 +1239,7 @@ void sedit::show_activ_prof(CharData *ch) { void sedit::parse_activ_prof(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный ввод.\r\n", ch); show_activ_prof(ch); return; } @@ -1258,7 +1258,7 @@ void sedit::parse_activ_prof(CharData *ch, const char *arg) { } else if (num == bits.size() + 2) { bits.set(); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный ввод.\r\n", ch); } show_activ_prof(ch); } @@ -1276,13 +1276,13 @@ void sedit::parse_activ_skill(CharData *ch, const char *arg) { } if (sscanf(arg, "%d %d", &ssnum, &ssval) < 2) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Не указан уровень владения умением.\r\n", ch); show_activ_skill(ch); return; } auto skill_id = static_cast(ssnum); if (MUD::Skills().IsInvalid(skill_id)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Неизвестное умение.\r\n", ch); show_activ_skill(ch); } else if (ssval == 0) { skill.first = ESkill::kUndefined; @@ -1325,7 +1325,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { auto i = olc_set.activ_list.find(activ_edit); if (i == olc_set.activ_list.end()) { SendMsgToChar(ch, - ": %s:%d (%s).\r\n", + "Ошибка: активатор не найден %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); show_main(ch); return; @@ -1334,7 +1334,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Неверный выбор!\r\n", ch); show_activ_edit(ch); return; } @@ -1345,7 +1345,7 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { case rus::kVeUpper: case rus::kVe: show_main(ch); break; - default: SendMsgToChar(" !\r\n", ch); + default: SendMsgToChar("Неверный выбор!\r\n", ch); show_activ_edit(ch); break; } @@ -1355,10 +1355,10 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { unsigned num = atoi(arg); switch (num) { case 1: state = STATE_ACTIV_REMOVE; - SendMsgToChar(" Y()/N() :", ch); + SendMsgToChar("Подтвердите удаление активатора Y(Д)/N(Н) :", ch); return; case 2: state = STATE_ACTIV_CHANGE; - SendMsgToChar(" - :", ch); + SendMsgToChar("Введите новое кол-во предметов для активации :", ch); return; case 3: show_activ_prof(ch); return; @@ -1377,23 +1377,23 @@ void sedit::parse_activ_edit(CharData *ch, const char *arg) { show_activ_ench_vnum(ch); } else if (num == offset + activ.apply.size() + 3) { state = STATE_ACTIV_PHYS_DMG; - SendMsgToChar(" . :", ch); + SendMsgToChar("Укажите процент прибавляемого физ. урона :", ch); } else if (num == offset + activ.apply.size() + 4) { state = STATE_ACTIV_MAGE_DMG; - SendMsgToChar(" . :", ch); + SendMsgToChar("Укажите процент прибавляемого маг. урона :", ch); } else if (num == offset + activ.apply.size() + 5) { if (activ.npc) { - SendMsgToChar(" .", ch); + SendMsgToChar("Теперь НЕ активится на мобах.", ch); i->second.npc = false; } else { - SendMsgToChar(" .", ch); + SendMsgToChar("Теперь активится на мобах.", ch); i->second.npc = true; } show_activ_edit(ch); } else if (num == offset + activ.apply.size() + 6) { show_main(ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный ввод.\r\n", ch); show_activ_edit(ch); } } @@ -1426,12 +1426,12 @@ void sedit::parse_activ_apply_loc(CharData *ch, const char *arg) { apply.modifier = 0; show_activ_edit(ch); } else if (num < 0 || num >= EApply::kNumberApplies) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный ввод.\r\n", ch); show_apply_olc(ch->desc); } else { apply.location = static_cast(num); state = STATE_ACTIV_APPLY_MOD; - SendMsgToChar(" : ", ch); + SendMsgToChar("Введите модификатор : ", ch); } } @@ -1455,14 +1455,14 @@ void sedit::parse_activ_apply_mod(CharData *ch, const char *arg) { void sedit::parse_obj_change(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Некорректный виртуальный номер предмета.\r\n", ch); show_obj_edit(ch); return; } const int vnum = atoi(arg); if (vnum == obj_edit) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы и так редактируете данный предмет.\r\n", ch); show_obj_edit(ch); return; } @@ -1472,9 +1472,9 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { ? MISSING_OBJECT_NAME : obj_proto[rnum]->get_short_description().c_str(); if (is_duplicate(olc_set.uid, vnum)) { - SendMsgToChar(ch, " '%s' .\r\n", name); + SendMsgToChar(ch, "Предмет '%s' уже является частью другого набора.\r\n", name); } else if (olc_set.obj_list.find(vnum) != olc_set.obj_list.end()) { - SendMsgToChar(ch, " '%s' .\r\n", name); + SendMsgToChar(ch, "Предмет '%s' уже является частью данного набора.\r\n", name); } else { SetMsgNode msg; auto i = olc_set.obj_list.find(obj_edit); @@ -1485,7 +1485,7 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { obj_edit = vnum; olc_set.obj_list.insert(std::make_pair(vnum, msg)); - SendMsgToChar(ch, " '%s' .\r\n", name); + SendMsgToChar(ch, "Предмет '%s' добавлен в набор.\r\n", name); } show_obj_edit(ch); @@ -1494,17 +1494,17 @@ void sedit::parse_obj_change(CharData *ch, const char *arg) { void sedit::parse_activ_change(CharData *ch, const char *arg) { skip_spaces(&arg); if (!*arg || !a_isdigit(*arg)) { - SendMsgToChar(" - .\r\n", ch); + SendMsgToChar("Некорректное кол-во предметов для активации.\r\n", ch); show_activ_edit(ch); return; } const int num = atoi(arg); if (num == activ_edit) { - SendMsgToChar("- .\r\n", ch); + SendMsgToChar("Кол-во предметов активации не изменилось.\r\n", ch); } else if (olc_set.activ_list.find(num) != olc_set.activ_list.end()) { SendMsgToChar(ch, - " %d %s.\r\n", + "Набор уже содержит активатор на %d %s.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } else { ActivNode activ; @@ -1517,15 +1517,15 @@ void sedit::parse_activ_change(CharData *ch, const char *arg) { // GCC 4.4 //olc_set.activ_list.emplace(num, activ); olc_set.activ_list.insert(std::make_pair(num, activ)); - SendMsgToChar(ch, " %d %s .\r\n", + SendMsgToChar(ch, "Активатор на %d %s добавлен в набор.\r\n", num, grammar::GetDeclensionInNumber(num, grammar::EWhat::kObject)); } show_activ_edit(ch); } -/// -/// try/catch out_of_range, .. -/// .at +/// наружу при обработке команд в олц торчит только эта функция +/// плюс она обернута в try/catch для ловли возможных out_of_range, т.к. внутри +/// все через .at и местами код на это закладывается вместо ручных сравнений void parse_input(CharData *ch, const char *arg) { sedit &olc = *(ch->desc->sedit); @@ -1602,7 +1602,7 @@ void parse_input(CharData *ch, const char *arg) { default: ch->desc->sedit.reset(); ch->desc->state = EConState::kPlaying; SendMsgToChar(ch, - ": STATE, %s:%d (%s).\r\n", + "Ошибка: не найден STATE, редактирование отменено %s:%d (%s).\r\n", __FILE__, __LINE__, __func__); break; } // switch @@ -1626,15 +1626,15 @@ void start_sedit(CharData *ch, size_t idx) { } const char *SEDIT_HELP = - " :\r\n" - " sedit - \r\n" - " sedit < slist> - \r\n" - " sedit - \r\n" - " sedit - \r\n"; + "Формат команды:\r\n" + " sedit - создание нового сета\r\n" + " sedit <прядковый номер сета из slist> - редактирование существующего сета\r\n" + " sedit - редактирование существующего сета\r\n" + " sedit - редактирование глобальных сообщений сетов\r\n"; } // namespace -/// sedit, . SEDIT_HELP +/// иммский sedit, см. SEDIT_HELP void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) { return; @@ -1642,20 +1642,20 @@ void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { skip_spaces(&argument); if (!argument || !*argument) { - // + // создание нового сета start_sedit(ch, -1); } else if (a_isdigit(*argument)) { - // + // редактирование существующего unsigned num = atoi(argument); if (num > 0 && num <= sets_list.size()) { - // slist + // по номеру сета из slist --num; start_sedit(ch, num); } else { - // + // по внуму предмета if (GetObjRnum(num) < 0) { SendMsgToChar(SEDIT_HELP, ch); - SendMsgToChar(ch, " vnum %s .\r\n", + SendMsgToChar(ch, "Предметов с vnum %s не существует.\r\n", argument); return; } @@ -1665,11 +1665,11 @@ void do_sedit(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } else { SendMsgToChar(SEDIT_HELP, ch); SendMsgToChar(ch, - " vnum %s .\r\n", argument); + "В сетах предметов с vnum %s не найдено.\r\n", argument); } } } else if (!str_cmp(argument, "msg_set") || !str_cmp(argument, "messages")) { - // + // редактирование глобальных сообщений ch->desc->state = EConState::kSedit; ch->desc->sedit = std::make_shared(); ch->desc->sedit->msg_edit = global_msg; diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 11c9547fa..8140a442d 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -59,22 +59,22 @@ namespace sight { #define TAG_AUTUMNDAY "" #define MAX_FIRES 6 -const char *Fires[MAX_FIRES] = {" ", - " ", - "- ", - " ", - " ", - " " +const char *Fires[MAX_FIRES] = {"тлеет небольшая кучка угольков", + "тлеет небольшая кучка угольков", + "еле-еле теплится огонек", + "догорает небольшой костер", + "весело трещит костер", + "ярко пылает костер" }; -const char *ObjState[8][2] = {{"", ""}, - {"", " "}, - {"", " "}, - {"", " "}, - {"", " "}, - {"", " "}, - {" ", " "}, - {"", " "} +const char *ObjState[8][2] = {{"рассыпается", "рассыпается"}, + {"плачевно", "в плачевном состоянии"}, + {"плохо", "в плохом состоянии"}, + {"неплохо", "в неплохом состоянии"}, + {"средне", "в рабочем состоянии"}, + {"хорошо", "в хорошем состоянии"}, + {"очень хорошо", "в очень хорошем состоянии"}, + {"великолепно", "в великолепном состоянии"} }; void list_obj_to_char(const ObjData::obj_list_t &list, CharData *ch, int mode, int show); @@ -114,25 +114,25 @@ std::string BuildCompactShieldSuffix(const CharData *viewer, const CharData *tar }; if (AFF_FLAGGED(target, EAffect::kFireShield)) { - append("&R&R"); + append("&RОЩ&R"); } if (AFF_FLAGGED(target, EAffect::kAirShield)) { - append("&W&R"); + append("&WВЩ&R"); } if (AFF_FLAGGED(target, EAffect::kIceShield)) { - append("&C&R"); + append("&CЛЩ&R"); } if (AFF_FLAGGED(viewer, EAffect::kDetectMagic) && AFF_FLAGGED(target, EAffect::kMagicGlass)) { - append("&w&R"); + append("&wМЗ&R"); } if (mode == EBriefShieldsMode::kCompressed) { if (AFF_FLAGGED(target, EAffect::kSanctuary)) { - append("&Y&R"); + append("&YО&R"); } else if (AFF_FLAGGED(target, EAffect::kPrismaticAura)) { - append("&M&R"); + append("&MП&R"); } if (AFF_FLAGGED(viewer, EAffect::kDetectMagic) && AFF_FLAGGED(target, EAffect::kBrokenChains)) { - append("&C&R"); + append("&CРО&R"); } } @@ -160,11 +160,11 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { if (!ch->desc) return; if (is_dark(ch->in_room) && !CanSeeInDark(ch) && !CanUseFeat(ch, EFeat::kDarkReading)) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("Слишком темно...\r\n", ch); show_glow_objs(ch); return; } else if (AFF_FLAGGED(ch, EAffect::kBlind)) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("Вы все еще слепы...\r\n", ch); return; } else if (ch->GetPosition() < EPosition::kSleep) { return; @@ -183,7 +183,7 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { SendMsgToChar(kColorBoldCyn, ch); if (!ch->IsNpc() && (ch->IsFlagged(EPrf::kRoomFlags) || InTestZone(ch))) { - // * + // иммам рандомная * во флагах ломает мапер грят const bool has_flag = ROOM_FLAGGED(ch->in_room, ERoomFlag::kBfsMark) ? true : false; world[ch->in_room]->unset_flag(ERoomFlag::kBfsMark); @@ -213,7 +213,7 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { SendMsgToChar("\r\n", ch); if (is_dark(ch->in_room) && !ch->IsFlagged(EPrf::kHolylight)) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar("Слишком темно...\r\n", ch); } else if ((!ch->IsNpc() && !ch->IsFlagged(EPrf::kBrief)) || ignore_brief || ROOM_FLAGGED(ch->in_room, ERoomFlag::kDeathTrap)) { show_extend_room(GlobalObjects::descriptions().get(world[ch->in_room]->description_num).c_str(), ch); } @@ -222,14 +222,14 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { do_auto_exits(ch); } - // . . - // spell_msg.xml kRoomAffect{Visible,Invisible,SelfInvisible} - // (issue.sight-fmt); show_room_affects ch. + // Отображаем аффекты комнаты. После автовыходов чтобы не ломать популярный маппер. + // Описания живут в spell_msg.xml под ключами kRoomAffect{Visible,Invisible,SelfInvisible} + // (issue.sight-fmt); show_room_affects выбирает нужный по флагам ch. show_room_affects(ch); // now list characters & objects if (world[ch->in_room]->fires) { - sprintf(buf, "%s %s.%s\r\n", kColorRed, Fires[MIN(world[ch->in_room]->fires, MAX_FIRES - 1)], kColorNrm); + sprintf(buf, "%sВ центре %s.%s\r\n", kColorRed, Fires[MIN(world[ch->in_room]->fires, MAX_FIRES - 1)], kColorNrm); SendMsgToChar(buf, ch); } // kPortalTimer rendering (regular + PK variant + immortal/tester timer suffix) @@ -238,8 +238,8 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { if (world[ch->in_room]->holes) { const int ar = round_up(world[ch->in_room]->holes / kHolesTime); - sprintf(buf, "%s %i %s.%s\r\n", - kColorYel, ar, (ar == 1 ? "" : (ar < 5 ? "" : "")), (kColorNrm)); + sprintf(buf, "%sЗдесь выкопана ямка глубиной примерно в %i аршин%s.%s\r\n", + kColorYel, ar, (ar == 1 ? "" : (ar < 5 ? "а" : "ов")), (kColorNrm)); SendMsgToChar(buf, ch); } @@ -251,29 +251,29 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { case ESector::kForestSnow: case ESector::kHillsSnow: case ESector::kMountainSnow: - sprintf(buf, "%s .%s\r\n", + sprintf(buf, "%sСнежный ковер лежит у вас под ногами.%s\r\n", kColorWht, kColorNrm); break; case ESector::kFieldRain: case ESector::kForestRain: case ESector::kHillsRain: sprintf(buf, - "%s .%s\r\n", + "%sВы просто увязаете в грязи.%s\r\n", kColorBoldBlk, kColorNrm); break; case ESector::kThickIce: sprintf(buf, - "%s .%s\r\n", + "%sУ вас под ногами толстый лед.%s\r\n", kColorBoldBlu, kColorNrm); break; case ESector::kNormalIce: - sprintf(buf, "%s .%s\r\n", + sprintf(buf, "%sУ вас под ногами достаточно толстый лед.%s\r\n", kColorBoldBlu, kColorNrm); break; case ESector::kThinIce: - sprintf(buf, "%s - .%s\r\n", + sprintf(buf, "%sТоненький ледок вот-вот проломится под вами.%s\r\n", kColorBoldCyn, kColorNrm); break; }; @@ -285,12 +285,12 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { // issue.runestones phase 3: the runestone now shows itself as a physical object in the room // contents below (no registry-driven room-presence hack needed). list_obj_to_char(world[ch->in_room]->contents, ch, 0, false); - list_char_to_char_thing(world[ch->in_room]->people, ch); // + list_char_to_char_thing(world[ch->in_room]->people, ch); //добавим отдельный вызов если моб типа предмет выводим желтым SendMsgToChar("&R&q", ch); list_char_to_char(world[ch->in_room]->people, ch); SendMsgToChar("&Q&n", ch); - // + // вход в новую зону if (!ch->IsNpc()) { ZoneRnum inroom = world[ch->in_room]->zone_rn; if (zone_table[world[ch->get_from_room()]->zone_rn].vnum != zone_table[inroom].vnum) { @@ -303,8 +303,8 @@ void look_at_room(CharData *ch, int ignore_brief, bool msdp_mode) { } if (zone_table[world[ch->get_from_room()]->zone_rn].vnum >= dungeons::kZoneStartDungeons && zone_table[inroom].vnum < dungeons::kZoneStartDungeons) { - SendMsgToChar("&G .\r\n&n", ch); -// dungeons::SwapObjectDungeon(ch); + SendMsgToChar("&GВы покинули зазеркалье.\r\n&n", ch); +// dungeons::SwapObjectDungeon(ch); перенесено на закрытие данжа } } } @@ -323,8 +323,8 @@ void show_glow_objs(CharData *ch) { if (!cnt) return; const char *str = cnt > 1 ? - " - .\r\n" : - " - .\r\n"; + "Вы видите очертания каких-то блестящих предметов.\r\n" : + "Вы видите очертания какого-то блестящего предмета.\r\n"; SendMsgToChar(str, ch); } @@ -394,7 +394,7 @@ void show_extend_room(const char *const description, CharData *ch) { * * Thanks to Angus Mezick for the * suggested fix to this problem. - * \return -, look_in_obj + * \return флаг если смотрим в клан-сундук, чтобы после осмотра не смотреть второй раз по look_in_obj */ bool look_at_target(CharData *ch, char *arg, int subcmd) { int bits, found = false, fnum, i = 0; @@ -409,18 +409,18 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { } if (!*arg) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("На что вы так мечтаете посмотреть?\r\n", ch); return false; } half_chop(arg, whatp, where); what = whatp; - if (isname(where, " room ground")) + if (isname(where, "земля комната room ground")) where_bits = EFind::kObjRoom | EFind::kCharInRoom; - else if (isname(where, " inventory")) + else if (isname(where, "инвентарь inventory")) where_bits = EFind::kObjInventory; - else if (isname(where, " equipment")) + else if (isname(where, "экипировка equipment")) where_bits = EFind::kObjEquip; bits = generic_find(what, where_bits, ch, &found_char, &found_obj); @@ -440,27 +440,27 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return false; } if (CanSee(found_char, ch)) - act("$n $g .", true, ch, nullptr, found_char, kToVict); - act("$n $g $N3.", true, ch, nullptr, found_char, kToNotVict); + act("$n оглядел$g вас с головы до пят.", true, ch, nullptr, found_char, kToVict); + act("$n посмотрел$g на $N3.", true, ch, nullptr, found_char, kToNotVict); } return false; } // Strip off "number." from 2.foo and friends. if (!(fnum = get_number(&what))) { - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Что осматриваем?\r\n", ch); return false; } - // + // заглянуть в пентаграмму i = 0; - if (isname(whatp, "") && IS_SET(where_bits, EFind::kObjRoom)) { + if (isname(whatp, "пентаграмма") && IS_SET(where_bits, EFind::kObjRoom)) { RoomRnum to_room = kNowhere; bool one_way = false; for (const auto &aff : world[ch->in_room]->affected) { if (room_spells::IsPortalAffect(aff->affect_type)) { if (aff->affect_type == room_spells::ERoomAffect::kNoPortalExit) { - SendMsgToChar(", !\r\n", ch); + SendMsgToChar("Похоже, этого здесь нет!\r\n", ch); return false; } if (++i == fnum) { @@ -477,10 +477,10 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { } if (to_room != kNowhere) { const auto r = ch->in_room; - SendMsgToChar(" , .\r\n\r\n", ch); - act("$n0 $g .\r\n", true, ch, nullptr, nullptr, kToRoom); + SendMsgToChar("Приблизившись к пентаграмме, вы осторожно заглянули в нее.\r\n\r\n", ch); + act("$n0 осторожно заглянул$g в пентаграмму.\r\n", true, ch, nullptr, nullptr, kToRoom); if (!one_way) { - SendMsgToChar(" , , .\r\n\r\n", ch); + SendMsgToChar("Яркий свет, идущий с противоположного конца прохода, застилает вам глаза.\r\n\r\n", ch); return false; } ch->in_room = to_room; @@ -496,8 +496,8 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return false; } - // - ? - // (#3209). + // Совпадает ли аргумент с ключевыми словами какой-нибудь двери в комнате? + // Тогда покажем описание этого направления (#3209). for (int d = 0; d < EDirection::kMaxDirNum; ++d) { const auto &exit = world[ch->in_room]->dir_option[d]; if (!exit || !exit->keyword || !*exit->keyword) { @@ -509,7 +509,7 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { if (!exit->general_description.empty()) { SendMsgToChar(exit->general_description.c_str(), ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не видите ничего особенного.\r\n", ch); } return false; } @@ -528,8 +528,8 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { return true; } - // . "if (!found)" - // , "generic_find" + // Собственно изменение. Вместо проверки "if (!found)" юзается проверка + // наличия описания у объекта, найденного функцией "generic_find" if (!(desc = find_exdesc(what, found_obj->get_ex_description())) || ((found_obj->get_type() == EObjType::kNote) && !found_obj->get_action_description().empty())) { show_obj_to_char(found_obj, ch, 5, true, 1); // Show no-description @@ -542,7 +542,7 @@ bool look_at_target(CharData *ch, char *arg, int subcmd) { obj_info(ch, found_obj, buf); SendMsgToChar(buf, ch); } else - SendMsgToChar(", !\r\n", ch); + SendMsgToChar("Похоже, этого здесь нет!\r\n", ch); return false; } @@ -560,100 +560,100 @@ void look_at_char(CharData *i, CharData *ch) { else SendMsgToChar(ch, "*\r\n%s*\r\n", AddLeadingStringSpace(i->player_data.description).c_str()); } else if (!i->IsNpc()) { - strcpy(buf, "\r\n"); + strcpy(buf, "\r\nЭто"); if (IsFemale(i)) { if (GET_HEIGHT(i) <= 151) { if (GET_WEIGHT(i) >= 140) - strcat(buf, " .\r\n"); + strcat(buf, " маленькая плотная дамочка.\r\n"); else if (GET_WEIGHT(i) >= 125) - strcat(buf, " .\r\n"); + strcat(buf, " маленькая женщина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " миниатюрная дамочка.\r\n"); } else if (GET_HEIGHT(i) <= 159) { if (GET_WEIGHT(i) >= 145) - strcat(buf, " .\r\n"); + strcat(buf, " невысокая плотная мадам.\r\n"); else if (GET_WEIGHT(i) >= 130) - strcat(buf, " .\r\n"); + strcat(buf, " невысокая женщина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " изящная леди.\r\n"); } else if (GET_HEIGHT(i) <= 165) { if (GET_WEIGHT(i) >= 145) - strcat(buf, " .\r\n"); + strcat(buf, " среднего роста женщина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " среднего роста изящная красавица.\r\n"); } else if (GET_HEIGHT(i) <= 175) { if (GET_WEIGHT(i) >= 150) - strcat(buf, " .\r\n"); + strcat(buf, " высокая дородная баба.\r\n"); else if (GET_WEIGHT(i) >= 135) - strcat(buf, " .\r\n"); + strcat(buf, " высокая стройная женщина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " высокая изящная женщина.\r\n"); } else { if (GET_WEIGHT(i) >= 155) - strcat(buf, " .\r\n"); + strcat(buf, " очень высокая крупная дама.\r\n"); else if (GET_WEIGHT(i) >= 140) - strcat(buf, " .\r\n"); + strcat(buf, " очень высокая стройная женщина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " очень высокая худощавая женщина.\r\n"); } } else { if (GET_HEIGHT(i) <= 165) { if (GET_WEIGHT(i) >= 170) - strcat(buf, " , , .\r\n"); + strcat(buf, " маленький, похожий на колобок, мужчина.\r\n"); else if (GET_WEIGHT(i) >= 150) - strcat(buf, " .\r\n"); + strcat(buf, " маленький плотный мужчина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " маленький плюгавенький мужичонка.\r\n"); } else if (GET_HEIGHT(i) <= 175) { if (GET_WEIGHT(i) >= 175) - strcat(buf, " .\r\n"); + strcat(buf, " невысокий коренастый крепыш.\r\n"); else if (GET_WEIGHT(i) >= 160) - strcat(buf, " .\r\n"); + strcat(buf, " невысокий крепкий мужчина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " невысокий худощавый мужчина.\r\n"); } else if (GET_HEIGHT(i) <= 185) { if (GET_WEIGHT(i) >= 180) - strcat(buf, " .\r\n"); + strcat(buf, " среднего роста коренастый мужчина.\r\n"); else if (GET_WEIGHT(i) >= 165) - strcat(buf, " .\r\n"); + strcat(buf, " среднего роста крепкий мужчина.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, " среднего роста худощавый мужчина.\r\n"); } else if (GET_HEIGHT(i) <= 195) { if (GET_WEIGHT(i) >= 185) - strcat(buf, " .\r\n"); + strcat(buf, " высокий крупный мужчина.\r\n"); else if (GET_WEIGHT(i) >= 170) - strcat(buf, " .\r\n"); + strcat(buf, " высокий стройный мужчина.\r\n"); else - strcat(buf, " , .\r\n"); + strcat(buf, " длинный, худощавый мужчина.\r\n"); } else { if (GET_WEIGHT(i) >= 190) - strcat(buf, " .\r\n"); + strcat(buf, " огромный мужик.\r\n"); else if (GET_WEIGHT(i) >= 180) - strcat(buf, " , .\r\n"); + strcat(buf, " очень высокий, крупный амбал.\r\n"); else - strcat(buf, " , .\r\n"); + strcat(buf, " длиннющий, похожий на жердь мужчина.\r\n"); } } SendMsgToChar(buf, ch); } else - act("\r\n $n5 .", false, i, nullptr, ch, kToVict); + act("\r\nНичего необычного в $n5 вы не заметили.", false, i, nullptr, ch, kToVict); if (AFF_FLAGGED(i, EAffect::kHaemorrhage)) { - act("$n $a !", false, i, nullptr, ch, kToVict); + act("$n покрыт$a кровоточащими ранами!", false, i, nullptr, ch, kToVict); } if (AFF_FLAGGED(i, EAffect::kLacerations)) { - act("$n $a !", false, i, nullptr, ch, kToVict); + act("$n покрыт$a рваными ранами!", false, i, nullptr, ch, kToVict); } if (AFF_FLAGGED(i, EAffect::kCharmed) && i->get_master() == ch) { if (IsCharmExpiring(i)) { - act("$n .", false, i, nullptr, ch, kToVict); + act("$n скоро перестанет следовать за вами.", false, i, nullptr, ch, kToVict); } else { for (const auto &aff : i->affected) { if (IS_SET(aff->battleflag, kAfCharmBond)) { sprintf(buf, - IsPoly(i) ? "$n %d %s." : "$n %d %s.", + IsPoly(i) ? "$n будут слушаться вас еще %d %s." : "$n будет слушаться вас еще %d %s.", aff->duration/2, grammar::GetDeclensionInNumber(aff->duration/2, grammar::EWhat::kHour)); act(buf, false, i, nullptr, ch, kToVict); @@ -665,15 +665,15 @@ void look_at_char(CharData *i, CharData *ch) { if (mount::IsHorse(i) && i->get_master() == ch) { - strcpy(buf, "\r\n . "); + strcpy(buf, "\r\nЭто ваш скакун. Он "); if (GET_HORSESTATE(i) <= 0) - strcat(buf, ".\r\n"); + strcat(buf, "загнан.\r\n"); else if (GET_HORSESTATE(i) <= 20) - strcat(buf, " .\r\n"); + strcat(buf, "весь в мыле.\r\n"); else if (GET_HORSESTATE(i) <= 80) - strcat(buf, " .\r\n"); + strcat(buf, "в хорошем состоянии.\r\n"); else - strcat(buf, " .\r\n"); + strcat(buf, "выглядит совсем свежим.\r\n"); SendMsgToChar(buf, ch); }; @@ -686,7 +686,7 @@ void look_at_char(CharData *i, CharData *ch) { } if (found) { SendMsgToChar("\r\n", ch); - act("$n $a :", false, i, nullptr, ch, kToVict); + act("$n одет$a :", false, i, nullptr, ch, kToVict); for (j = 0; j < EEquipPos::kNumEquipPos; j++) { if (GET_EQ(i, j) && CanSeeObj(ch, GET_EQ(i, j))) { SendMsgToChar(where[j], ch); @@ -702,7 +702,7 @@ void look_at_char(CharData *i, CharData *ch) { if (ch != i && (GetSkill(ch, ESkill::kPry) || privilege::IsImmortal(ch))) { found = false; - act("\r\n $s :", false, i, nullptr, ch, kToVict); + act("\r\nВы попытались заглянуть в $s ношу:", false, i, nullptr, ch, kToVict); for (tmp_obj = i->carrying; tmp_obj; tmp_obj = tmp_obj->get_next_content()) { if (CanSeeObj(ch, tmp_obj) && (number(0, 30) < GetRealLevel(ch))) { if (!push) { @@ -721,7 +721,7 @@ void look_at_char(CharData *i, CharData *ch) { if (push && push_count) show_obj_to_char(push, ch, 1, ch == i, push_count); if (!found) - SendMsgToChar("... .\r\n", ch); + SendMsgToChar("...и ничего не обнаружили.\r\n", ch); } } @@ -777,7 +777,7 @@ void list_char_to_char_thing(const RoomData::people_t &list, CharData *ch) { // // kPortalTimer adds one extra debug line for immortals/testers: // "[timer: into: ]", echoing the immortals-only -// "(: %d, : %d)" suffix that used to live in look_at_room. +// "(время: %d, куда: %d)" suffix that used to live in look_at_room. void show_room_affects(CharData *ch) { std::ostringstream buffer; @@ -848,7 +848,7 @@ void show_room_affects(CharData *ch) { } buffer << "\r\n"; // kPortalTimer debug suffix: timer (room-tick pulses) + destination vnum. - // Replaces the immortals-only "(: %d, : %d)" suffix that used to + // Replaces the immortals-only "(время: %d, куда: %d)" suffix that used to // be inlined into the main description in look_at_room. if (room_spells::IsPortalAffect(af->affect_type) && is_immortal_or_tester) { char debug_buf[128]; @@ -910,11 +910,11 @@ void do_auto_exits(CharData *ch) { *buf = '\0'; for (door = 0; door < EDirection::kMaxDirNum; door++) { - // - + // Наконец-то добавлена отрисовка в автовыходах закрытых дверей if (EXIT(ch, door) && EXIT(ch, door)->to_room() != kNowhere) { if (EXIT_FLAGGED(EXIT(ch, door), EExitFlag::kClosed)) { - // : UTF-8 , %c - // (issue #3681). + // Печатаем первую БУКВУ направления: под UTF-8 у русской буквы два байта, и %c + // выводил половину (issue #3681). slen += sprintf(buf + slen, "("); slen += static_cast(native_text::copy_lower_char(dirs[door], buf + slen)); slen += sprintf(buf + slen, ") "); @@ -977,11 +977,11 @@ void list_obj_to_char(ObjData *list, CharData *ch, int mode, int show) { } if (!found && show) { if (show == 1) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar(" Внутри ничего нет.\r\n", ch); else if (show == 2) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar(" Вы ничего не несете.\r\n", ch); else if (show == 3) { - SendMsgToChar(" ...\r\n", ch); + SendMsgToChar(" Пусто...\r\n", ch); return; } } @@ -1048,7 +1048,7 @@ void list_char_to_char(const RoomData::people_t &list, CharData *ch) { && i->in_room == ch->in_room && !CanSeeInDark(ch) && AFF_FLAGGED(i, EAffect::kInfravision)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Пара светящихся глаз смотрит на вас.\r\n", ch); } } } @@ -1108,20 +1108,20 @@ void look_in_direction(CharData *ch, int dir, int info_is) { count += sprintf(buf, "%s%s:%s ", kColorYel, dirs_rus[dir], kColorNrm); if (EXIT_FLAGGED(rdata, EExitFlag::kClosed)) { if (rdata->keyword) { - count += sprintf(buf + count, " (%s).\r\n", rdata->keyword); + count += sprintf(buf + count, " закрыто (%s).\r\n", rdata->keyword); } else { - count += sprintf(buf + count, " ( ).\r\n"); + count += sprintf(buf + count, " закрыто (вероятно дверь).\r\n"); } const int skill_pick = GetSkill(ch, ESkill::kPickLock); if (EXIT_FLAGGED(rdata, EExitFlag::kLocked) && skill_pick) { if (EXIT_FLAGGED(rdata, EExitFlag::kPickroof)) { count += sprintf(buf + count - 2, - "%s !%s\r\n", + "%s вы никогда не сможете ЭТО взломать!%s\r\n", kColorBoldCyn, kColorNrm); } else if (EXIT_FLAGGED(rdata, EExitFlag::kBrokenLock)) { - count += sprintf(buf + count - 2, "%s ... %s\r\n", kColorRed, kColorNrm); + count += sprintf(buf + count - 2, "%s Замок сломан... %s\r\n", kColorRed, kColorNrm); } else { const PickProbabilityInformation &pbi = get_pick_probability(ch, rdata->lock_complexity); count += sprintf(buf + count - 2, "%s\r\n", pbi.text.c_str()); @@ -1135,7 +1135,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { } if (is_dark(rdata->to_room())) { - count += sprintf(buf + count, " .\r\n"); + count += sprintf(buf + count, " слишком темно.\r\n"); SendMsgToChar(buf, ch); if (info_is & EXIT_SHOW_LOOKING) { SendMsgToChar("&R&q", ch); @@ -1146,7 +1146,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { TrainSkill(ch, ESkill::kLooking, probe >= percent, tch); if (HERE(tch) && InvisOk(ch, tch) && probe >= percent && (percent < 100 || privilege::IsImmortal(ch))) { - // + // Если моб не вещь и смотрящий не им if (GET_RACE(tch) != ENpcRace::kConstruct || privilege::IsImmortal(ch)) { ListOneChar(tch, ch, ESkill::kLooking); count++; @@ -1155,7 +1155,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { } if (!count) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Вы ничего не смогли разглядеть!\r\n", ch); } SendMsgToChar("&Q&n", ch); } @@ -1173,7 +1173,7 @@ void look_in_direction(CharData *ch, int dir, int info_is) { SendMsgToChar("&Q&n", ch); } } else if (info_is & EXIT_SHOW_WALL) - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("И что вы там мечтаете увидеть?\r\n", ch); } bool quest_item(ObjData *obj) { @@ -1191,26 +1191,26 @@ void look_in_obj(CharData *ch, char *arg) { int where_bits = EFind::kObjInventory | EFind::kObjRoom | EFind::kObjEquip; if (!*arg) - SendMsgToChar(" ?\r\n", ch); + SendMsgToChar("Смотреть во что?\r\n", ch); else half_chop(arg, whatp, where); - if (isname(where, " room ground")) + if (isname(where, "земля комната room ground")) where_bits = EFind::kObjRoom; - else if (isname(where, " inventory")) + else if (isname(where, "инвентарь inventory")) where_bits = EFind::kObjInventory; - else if (isname(where, " equipment")) + else if (isname(where, "экипировка equipment")) where_bits = EFind::kObjEquip; bits = generic_find(arg, where_bits, ch, &dummy, &obj); if ((obj == nullptr) || !bits) { - sprintf(buf, " '%s'.\r\n", arg); + sprintf(buf, "Вы не видите здесь '%s'.\r\n", arg); SendMsgToChar(buf, ch); } else if (obj->get_type() != EObjType::kLiquidContainer && obj->get_type() != EObjType::kFountain && obj->get_type() != EObjType::kContainer) { - SendMsgToChar(" !\r\n", ch); + SendMsgToChar("Ничего в нем нет!\r\n", ch); } else { if (Clan::ChestShow(obj, ch)) { return; @@ -1225,17 +1225,17 @@ void look_in_obj(CharData *ch, char *arg) { if (obj->get_type() == EObjType::kContainer) { if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kShutted))) { - act("$A.", false, ch, obj, nullptr, kToChar); + act("Закрыт$A.", false, ch, obj, nullptr, kToChar); const int skill_pick = GetSkill(ch, ESkill::kPickLock); - int count = sprintf(buf, "%s.", grammar::ObjSexEnding((obj)->get_sex(), 6)); + int count = sprintf(buf, "Заперт%s.", grammar::ObjSexEnding((obj)->get_sex(), 6)); if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kLockedUp)) && skill_pick) { if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kUncrackable))) count += sprintf(buf + count, - "%s !%s\r\n", + "%s Вы никогда не сможете ЭТО взломать!%s\r\n", kColorBoldCyn, kColorNrm); else if (IS_SET(GET_OBJ_VAL((obj), 1), (EContainerFlag::kLockIsBroken))) - count += sprintf(buf + count, "%s ... %s\r\n", kColorRed, kColorNrm); + count += sprintf(buf + count, "%s Замок сломан... %s\r\n", kColorRed, kColorNrm); else { const PickProbabilityInformation &pbi = get_pick_probability(ch, GET_OBJ_VAL(obj, 3)); count += sprintf(buf + count, "%s\r\n", pbi.text.c_str()); @@ -1245,25 +1245,25 @@ void look_in_obj(CharData *ch, char *arg) { } else { SendMsgToChar(OBJN(obj, ch, grammar::ECase::kNom), ch); switch (bits) { - case EFind::kObjInventory: SendMsgToChar("( )\r\n", ch); + case EFind::kObjInventory: SendMsgToChar("(в руках)\r\n", ch); break; - case EFind::kObjRoom: SendMsgToChar("( )\r\n", ch); + case EFind::kObjRoom: SendMsgToChar("(на земле)\r\n", ch); break; - case EFind::kObjEquip: SendMsgToChar("( )\r\n", ch); + case EFind::kObjEquip: SendMsgToChar("(в амуниции)\r\n", ch); break; - default: SendMsgToChar("( )\r\n", ch); + default: SendMsgToChar("(неведомо где)\r\n", ch); break; } if (!obj->get_contains()) - SendMsgToChar(" .\r\n", ch); + SendMsgToChar(" Внутри ничего нет.\r\n", ch); else { if (GET_OBJ_VAL(obj, 0) > 0 && bits != EFind::kObjRoom) { - /* amt - 6 (0..5) - . , - 0 5. ( 5 ) + /* amt - индекс массива из 6 элементов (0..5) с описанием наполненности + с помощью нехитрых мат. преобразований мы получаем соотношение веса и максимального объема контейнера, + выраженные числами от 0 до 5. (причем 5 будет лишь при полностью полном контейнере) */ amt = std::clamp((obj->get_weight() * 100) / (GET_OBJ_VAL(obj, 0) * 20), 0, 5); - sprintf(buf, "%s %s:\r\n", grammar::ObjSexEnding((obj)->get_sex(), 6), fullness[amt]); + sprintf(buf, "Заполнен%s содержимым %s:\r\n", grammar::ObjSexEnding((obj)->get_sex(), 6), fullness[amt]); SendMsgToChar(buf, ch); } list_obj_to_char(obj->get_contains(), ch, 1, bits != EFind::kObjRoom); @@ -1287,7 +1287,7 @@ void skip_hide_on_look(CharData *ch) { } } -// mode 1 show_state 3 (4 - ) +// mode 1 show_state 3 для хранилище (4 - хранилище ингров) const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_state, int how) { *buf = '\0'; if ((mode < 5) && (ch->IsFlagged(EPrf::kRoomFlags) || InTestZone(ch))) @@ -1303,24 +1303,24 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s } else if (mode == 5) { if (object->get_type() == EObjType::kNote) { if (!object->get_action_description().empty()) { - strcpy(buf, " :\r\n\r\n"); + strcpy(buf, "Вы прочитали следующее :\r\n\r\n"); strcat(buf, AddLeadingStringSpace(object->get_action_description()).c_str()); strcat(buf, "\r\n"); page_string(ch->desc, buf, 1); } else { - SendMsgToChar(".\r\n", ch); + SendMsgToChar("Чисто.\r\n", ch); } return nullptr; } else if (object->get_type() == EObjType::kBandage) { - strcpy(buf, " ('').\r\n"); - snprintf(buf2, kMaxStringLength, " : %d, : %d", + strcpy(buf, "Бинты для перевязки ран ('перевязать').\r\n"); + snprintf(buf2, kMaxStringLength, "Осталось применений: %d, восстановление: %d", object->get_weight(), GET_OBJ_VAL(object, 0) * 10); strcat(buf, buf2); } else if (object->get_type() != EObjType::kLiquidContainer) { - strcpy(buf, " ."); + strcpy(buf, "Вы не видите ничего необычного."); } else // ITEM_TYPE == kLiquidContainer||FOUNTAIN { - strcpy(buf, " ."); + strcpy(buf, "Это емкость для жидкости."); } } @@ -1329,9 +1329,9 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s if (mode == 1 && how <= 1) { if (object->get_type() == EObjType::kLightSource) { if (GET_OBJ_VAL(object, 2) == -1) - strcpy(buf2, " ( )"); + strcpy(buf2, " (вечный свет)"); else if (GET_OBJ_VAL(object, 2) == 0) - sprintf(buf2, " (%s)", grammar::ObjSexEnding((object)->get_sex(), 4)); + sprintf(buf2, " (погас%s)", grammar::ObjSexEnding((object)->get_sex(), 4)); else sprintf(buf2, " (%d %s)", GET_OBJ_VAL(object, 2), grammar::GetDeclensionInNumber(GET_OBJ_VAL(object, 2), grammar::EWhat::kHour)); @@ -1351,28 +1351,28 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s } } if ((object->get_type() == EObjType::kContainer) - && !IS_SET(GET_OBJ_VAL((object), 1), (EContainerFlag::kShutted))) // , + && !IS_SET(GET_OBJ_VAL((object), 1), (EContainerFlag::kShutted))) // если закрыто, содержимое не показываем { if (object->get_contains()) { - strcat(buf2, " ( )"); + strcat(buf2, " (есть содержимое)"); } else { - if (GET_OBJ_VAL(object, 3) < 1) // , 2 - sprintf(buf2 + strlen(buf2), " (%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); + if (GET_OBJ_VAL(object, 3) < 1) // есть ключ для открытия, пустоту не показываем2 + sprintf(buf2 + strlen(buf2), " (пуст%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); } } if ((object->get_type() == EObjType::kNote) && !object->get_action_description().empty()) { - strcat(buf2, " (- )"); + strcat(buf2, " (что-то накарябано)"); } } else if (mode >= 2 && how <= 1) { std::string obj_name = OBJN(object, ch, grammar::ECase::kNom); native_text::capitalize_first(obj_name); if (object->get_type() == EObjType::kLightSource) { if (GET_OBJ_VAL(object, 2) == -1) { - sprintf(buf2, "\r\n%s .", obj_name.c_str()); + sprintf(buf2, "\r\n%s дает вечный свет.", obj_name.c_str()); } else if (GET_OBJ_VAL(object, 2) == 0) { - sprintf(buf2, "\r\n%s %s.", obj_name.c_str(), grammar::ObjSexEnding((object)->get_sex(), 4)); + sprintf(buf2, "\r\n%s погас%s.", obj_name.c_str(), grammar::ObjSexEnding((object)->get_sex(), 4)); } else { - sprintf(buf2, "\r\n%s %d %s.", obj_name.c_str(), GET_OBJ_VAL(object, 2), + sprintf(buf2, "\r\n%s будет светить %d %s.", obj_name.c_str(), GET_OBJ_VAL(object, 2), grammar::GetDeclensionInNumber(GET_OBJ_VAL(object, 2), grammar::EWhat::kHour)); } } else if (object->get_current_durability() < object->get_maximum_durability()) { @@ -1387,45 +1387,45 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s // issue.obj-suppressor-affect: brief marker when a worn item's conferred magic is currently // suppressed (dispelled + auto-returning); the full list with timers shows on examine/identify. if (mode != 3 && how <= 1 && object->has_suppressed_affects()) { - strcat(buf, " < >"); + strcat(buf, " <магия подавлена>"); } if (mode != 3 && how <= 1) { if (object->has_flag(EObjFlag::kInvisible)) { - sprintf(buf2, " (%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); + sprintf(buf2, " (невидим%s)", grammar::ObjSexEnding((object)->get_sex(), 6)); strcat(buf, buf2); } if (object->has_flag(EObjFlag::kBless) && AFF_FLAGGED(ch, EAffect::kDetectAlign)) - strcat(buf, " .. !"); + strcat(buf, " ..голубая аура!"); if (object->has_flag(EObjFlag::kMagic) && AFF_FLAGGED(ch, EAffect::kDetectMagic)) - strcat(buf, " .. !"); + strcat(buf, " ..желтая аура!"); if (object->has_flag(EObjFlag::kPoisoned) && AFF_FLAGGED(ch, EAffect::kDetectPoison)) { - sprintf(buf2, "..%s!", grammar::ObjSexEnding((object)->get_sex(), 6)); + sprintf(buf2, "..отравлен%s!", grammar::ObjSexEnding((object)->get_sex(), 6)); strcat(buf, buf2); } if (object->has_flag(EObjFlag::kGlow)) - strcat(buf, " ..!"); + strcat(buf, " ..блестит!"); if (object->has_flag(EObjFlag::kHum) && !AFF_FLAGGED(ch, EAffect::kDeafness)) - strcat(buf, " ..!"); + strcat(buf, " ..шумит!"); if (object->has_flag(EObjFlag::kFire)) - strcat(buf, " ..!"); + strcat(buf, " ..горит!"); if (object->has_flag(EObjFlag::kBloody)) { - sprintf(buf2, " %s..%s !%s", kColorBoldRed, grammar::ObjSexEnding((object)->get_sex(), 6), kColorNrm); + sprintf(buf2, " %s..покрыт%s кровью!%s", kColorBoldRed, grammar::ObjSexEnding((object)->get_sex(), 6), kColorNrm); strcat(buf, buf2); } } if (mode == 1) { - // -, + // клан-сундук, выводим список разом постранично if (show_state == 3) { sprintf(buf + strlen(buf), " [%d %s]\r\n", object->get_rent_on() * kClanStorehouseCoeff / 100, MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(object->get_rent_on() * kClanStorehouseCoeff / 100, grammar::ECase::kNom).c_str()); return buf; } - // + // ингры else if (show_state == 4) { sprintf(buf + strlen(buf), " [%d %s]\r\n", object->get_rent_off(), MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(object->get_rent_off(), grammar::ECase::kNom).c_str()); @@ -1439,7 +1439,7 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s strcat(buf, diag_armor_type_to_char(object).c_str()); strcat(buf, diag_timer_to_char(object)); strcat(buf, "\r\n"); - //strcat(buf, diag_uses_to_char(object, ch)); // commented by WorM obj_info / + //strcat(buf, diag_uses_to_char(object, ch)); // commented by WorM перенес в obj_info чтобы заряды рун было видно на базаре/ауке strcat(buf, obj_affects::Diag(object, ch).c_str()); } page_string(ch->desc, buf, true); @@ -1454,11 +1454,11 @@ void print_zone_info(CharData *ch) { bool delim = false; if (!zone->is_town) { delim = put_delim(out, delim); - out << " : " << zone->mob_level; + out << "средний уровень: " << zone->mob_level; } if (zone->group > 1) { delim = put_delim(out, delim); - out << " " << zone->group + out << "групповая на " << zone->group << " " << grammar::GetDeclensionInNumber(zone->group, grammar::EWhat::kPeople); } if (delim) { @@ -1489,8 +1489,8 @@ const char *find_exdesc(const char *word, const std::vector &l } /** -* -* ( ), ! +* При чтении писем и осмотре чара в его описании подставляем в начало каждой строки пробел +* (для дурных тригов), пользуясь случаем передаю привет проне! */ std::string AddLeadingStringSpace(const std::string& text) { if (!text.empty()) { @@ -1556,33 +1556,33 @@ void diag_char_to_char(CharData *i, CharData *ch) { strcpy(buf, PersonName(i, ch, 0)); utils::CAP(buf); - // , -- - // (iosystem.cpp) (groups.cpp). - // . , . + // Состояние жизни красим тем же градиентом, каким показаны хиты в бою -- в промпте + // (iosystem.cpp) и в списке группы (groups.cpp). Без цвета строка при осмотре сливается + // с описанием персонажа. Красится только само состояние, имя и поза остаются как были. strcat(buf, GetWarmValueColor(i->get_hit(), i->get_real_max_hit())); if (percent >= 100) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " невредим%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 90) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " слегка поцарапан%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 75) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " легко ранен%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 50) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " ранен%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 30) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " тяжело ранен%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 15) { - sprintf(buf2, " %s", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " смертельно ранен%s", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } else if (percent >= 0) - strcat(buf, " "); + strcat(buf, " в ужасном состоянии"); else - strcat(buf, " "); + strcat(buf, " умирает"); strcat(buf, kColorNrm); @@ -1590,33 +1590,33 @@ void diag_char_to_char(CharData *i, CharData *ch) { switch (i->GetPosition()) { case EPosition::kPerish: strcat(buf, "."); break; - case EPosition::kIncap: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kIncap: strcat(buf, IsPoly(i) ? ", лежат без сознания." : ", лежит без сознания."); break; - case EPosition::kStun: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kStun: strcat(buf, IsPoly(i) ? ", лежат в обмороке." : ", лежит в обмороке."); break; - case EPosition::kSleep: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kSleep: strcat(buf, IsPoly(i) ? ", спят." : ", спит."); break; - case EPosition::kRest: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kRest: strcat(buf, IsPoly(i) ? ", отдыхают." : ", отдыхает."); break; - case EPosition::kSit: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kSit: strcat(buf, IsPoly(i) ? ", сидят." : ", сидит."); break; - case EPosition::kStand: strcat(buf, IsPoly(i) ? ", ." : ", ."); + case EPosition::kStand: strcat(buf, IsPoly(i) ? ", стоят." : ", стоит."); break; case EPosition::kFight: if (i->GetEnemy()) - strcat(buf, IsPoly(i) ? ", ." : ", ."); + strcat(buf, IsPoly(i) ? ", сражаются." : ", сражается."); else - strcat(buf, IsPoly(i) ? ", ." : ", ."); + strcat(buf, IsPoly(i) ? ", махают кулаками." : ", махает кулаками."); break; default: return; break; } else - strcat(buf, IsPoly(i) ? ", ." : ", ."); + strcat(buf, IsPoly(i) ? ", сидят верхом." : ", сидит верхом."); if (AFF_FLAGGED(ch, EAffect::kDetectPoison)) if (AFF_FLAGGED(i, EAffect::kPoisoned)) { - sprintf(buf2, " (%s)", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf2, " (отравлен%s)", grammar::SexEnding((i)->get_sex(), 6)); strcat(buf, buf2); } @@ -1625,12 +1625,12 @@ void diag_char_to_char(CharData *i, CharData *ch) { } -//- -//buf , - *buf='\0' +//ф-ция вывода доп инфы об объекте +//buf это буфер в который дописывать инфу, в нем уже может быть что-то иначе надо перед вызовом присвоить *buf='\0' void obj_info(CharData *ch, ObjData *obj, char buf[kMaxStringLength]) { int j; if (CanUseFeat(ch, EFeat::kSkilledTrader) || ch->IsFlagged(EPrf::kHolylight) || GetSkill(ch, ESkill::kJewelry)) { - sprintf(buf + strlen(buf), " : %s", kColorCyn); + sprintf(buf + strlen(buf), "Материал : %s", kColorCyn); sprinttype(obj->get_material(), material_name, buf + strlen(buf)); sprintf(buf + strlen(buf), "\r\n%s", kColorNrm); } @@ -1641,59 +1641,59 @@ void obj_info(CharData *ch, ObjData *obj, char buf[kMaxStringLength]) { for (j = 0; imtypes[j].id != GET_OBJ_VAL(obj, IM_TYPE_SLOT) && j <= top_imtypes;) { j++; } - sprintf(buf + strlen(buf), " '%s'.\r\n", imtypes[j].name); + sprintf(buf + strlen(buf), "Это ингредиент вида '%s'.\r\n", imtypes[j].name); const int imquality = GET_OBJ_VAL(obj, IM_POWER_SLOT); if (GetRealLevel(ch) >= imquality) { - sprintf(buf + strlen(buf), " "); + sprintf(buf + strlen(buf), "Качество ингредиента "); if (imquality > 25) - strcat(buf + strlen(buf), ".\r\n"); + strcat(buf + strlen(buf), "наилучшее.\r\n"); else if (imquality > 20) - strcat(buf + strlen(buf), ".\r\n"); + strcat(buf + strlen(buf), "отличное.\r\n"); else if (imquality > 15) - strcat(buf + strlen(buf), " .\r\n"); + strcat(buf + strlen(buf), "очень хорошее.\r\n"); else if (imquality > 10) - strcat(buf + strlen(buf), " .\r\n"); + strcat(buf + strlen(buf), "выше среднего.\r\n"); else if (imquality > 5) - strcat(buf + strlen(buf), " .\r\n"); + strcat(buf + strlen(buf), "весьма посредственное.\r\n"); else - strcat(buf + strlen(buf), " .\r\n"); + strcat(buf + strlen(buf), "хуже не бывает.\r\n"); } else { - strcat(buf + strlen(buf), " .\r\n"); + strcat(buf + strlen(buf), "Вы не в состоянии определить качество этого ингредиента.\r\n"); } } //|| EPrf::FLAGGED(ch, EPrf::HOLYLIGHT) if (CanUseFeat(ch, EFeat::kJeweller)) { - sprintf(buf + strlen(buf), " : %s", kColorCyn); + sprintf(buf + strlen(buf), "Слоты : %s", kColorCyn); if (obj->has_flag(EObjFlag::kHasThreeSlots)) { - strcat(buf, " 3 \r\n"); + strcat(buf, "доступно 3 слота\r\n"); } else if (obj->has_flag(EObjFlag::kHasTwoSlots)) { - strcat(buf, " 2 \r\n"); + strcat(buf, "доступно 2 слота\r\n"); } else if (obj->has_flag(EObjFlag::kHasOneSlot)) { - strcat(buf, " 1 \r\n"); + strcat(buf, "доступен 1 слот\r\n"); } else { - strcat(buf, " \r\n"); + strcat(buf, "нет слотов\r\n"); } sprintf(buf + strlen(buf), "\r\n%s", kColorNrm); } if (AUTH_CUSTOM_LABEL(obj, ch) && !obj->get_custom_label()->text_label.empty()) { if (!obj->get_custom_label()->clan_abbrev.empty()) { - strcat(buf, " : "); + strcat(buf, "Метки дружины: "); } else { - strcat(buf, " : "); + strcat(buf, "Ваши метки: "); } sprintf(buf + strlen(buf), "%s\r\n", obj->get_custom_label()->text_label.c_str()); } sprintf(buf + strlen(buf), "%s", diag_uses_to_char(obj, ch)); sprintf(buf + strlen(buf), "%s", diag_shot_to_char(obj, ch)); if (GET_OBJ_VNUM(obj) >= DUPLICATE_MINI_SET_VNUM) { - sprintf(buf + strlen(buf), " .\r\n"); + sprintf(buf + strlen(buf), "Светится белым сиянием.\r\n"); } if (((obj->get_type() == EObjType::kLiquidContainer) && (GET_OBJ_VAL(obj, 1) > 0)) || (obj->get_type() == EObjType::kFood)) { - sprintf(buf1, ": %s\r\n", drinkcon::diag_liquid_timer(obj)); + sprintf(buf1, "Качество: %s\r\n", drinkcon::diag_liquid_timer(obj)); strcat(buf, buf1); } } @@ -1784,37 +1784,37 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { char aura_txt[200]; const char *positions[] = { - " , . ", - " , . ", - " , . ", - " , . ", - " . ", - " . ", - " . ", - "! ", - " . " + "лежит здесь, мертвый. ", + "лежит здесь, при смерти. ", + "лежит здесь, без сознания. ", + "лежит здесь, в обмороке. ", + "спит здесь. ", + "отдыхает здесь. ", + "сидит здесь. ", + "СРАЖАЕТСЯ! ", + "стоит здесь. " }; - // IsPoly() - "" -- + // Здесь и далее при использовании IsPoly() - патч для отображения позиций мобов типа "они" -- Ковшегуб const char *poly_positions[] = { - " , . ", - " , . ", - " , . ", - " , . ", - " . ", - " . ", - " . ", - "! ", - " . " + "лежат здесь, мертвые. ", + "лежат здесь, при смерти. ", + "лежат здесь, без сознания. ", + "лежат здесь, в обмороке. ", + "спят здесь. ", + "отдыхают здесь. ", + "сидят здесь. ", + "СРАЖАЮТСЯ! ", + "стоят здесь. " }; if (mount::IsHorse(i) && mount::IsOnHorse(i->get_master())) { if (ch == i->get_master()) { if (!IsPoly(i)) { - act("$N .", false, ch, nullptr, i, kToChar); + act("$N несет вас на своей спине.", false, ch, nullptr, i, kToChar); } else { - act("$N .", false, ch, nullptr, i, kToChar); + act("$N несут вас на своей спине.", false, ch, nullptr, i, kToChar); } } @@ -1824,9 +1824,9 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (mode == ESkill::kLooking) { if (HERE(i) && InvisOk(ch, i) && GetRealLevel(ch) >= (i->IsNpc() ? 0 : GET_INVIS_LEV(i))) { if (GET_RACE(i) == ENpcRace::kConstruct && privilege::IsImmortal(ch)) { - sprintf(buf, " %s.()\r\n", GET_PAD(i, 3)); + sprintf(buf, "Вы разглядели %s.(предмет)\r\n", GET_PAD(i, 3)); } else { - sprintf(buf, " %s.\r\n", GET_PAD(i, 3)); + sprintf(buf, "Вы разглядели %s.\r\n", GET_PAD(i, 3)); } SendMsgToChar(buf, ch); } @@ -1840,34 +1840,34 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { *buf = 0; if (IS_SET(mode_flags, kAcheckAffects)) { REMOVE_BIT(mode_flags, kAcheckAffects); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "магический ореол%s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckLight)) { REMOVE_BIT(mode_flags, kAcheckLight); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "яркий свет%s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckGlowing) && IS_SET(mode_flags, kAcheckHumming) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckGlowing); REMOVE_BIT(mode_flags, kAcheckHumming); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "шум и блеск экипировки%s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckGlowing)) { REMOVE_BIT(mode_flags, kAcheckGlowing); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "блеск экипировки%s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckHumming) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckHumming); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "шум экипировки%s", mode_flags ? ", " : " "); } if (IS_SET(mode_flags, kAcheckWeight) && !AFF_FLAGGED(ch, EAffect::kDeafness)) { REMOVE_BIT(mode_flags, kAcheckWeight); - sprintf(buf + strlen(buf), " %s", mode_flags ? ", " : " "); + sprintf(buf + strlen(buf), "бряцание металла%s", mode_flags ? ", " : " "); } - strcat(buf, " - .\r\n"); + strcat(buf, "выдает чье-то присутствие.\r\n"); SendMsgToChar(utils::CAP(buf), ch); return; } @@ -1886,35 +1886,35 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && !AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (AFF_FLAGGED(i, EAffect::kForcesOfEvil)) { - strcat(buf, "( ) "); + strcat(buf, "(черная аура) "); } } if (AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (i->IsNpc()) { if (NPC_FLAGGED(i, ENpcFlag::kAirCreature)) - sprintf(buf + strlen(buf), "%s( )%s ", + sprintf(buf + strlen(buf), "%s(аура воздуха)%s ", kColorBoldBlu, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kWaterCreature)) - sprintf(buf + strlen(buf), "%s( )%s ", + sprintf(buf + strlen(buf), "%s(аура воды)%s ", kColorBoldCyn, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kFireCreature)) - sprintf(buf + strlen(buf), "%s( )%s ", + sprintf(buf + strlen(buf), "%s(аура огня)%s ", kColorBoldMag, kColorBoldRed); else if (NPC_FLAGGED(i, ENpcFlag::kEarthCreature)) - sprintf(buf + strlen(buf), "%s( )%s ", + sprintf(buf + strlen(buf), "%s(аура земли)%s ", kColorBoldGrn, kColorBoldRed); } } if (AFF_FLAGGED(i, EAffect::kInvisible)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(невидим%s) ", grammar::SexEnding((i)->get_sex(), 6)); if (AFF_FLAGGED(i, EAffect::kHide)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(спрятал%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kDisguise)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(замаскировал%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kFly)) - strcat(buf, IsPoly(i) ? "() " : "() "); + strcat(buf, IsPoly(i) ? "(летят) " : "(летит) "); if (AFF_FLAGGED(i, EAffect::kHorse)) - strcat(buf, "( ) "); + strcat(buf, "(под седлом) "); std::string line = buf; line += i->player_data.long_descr; @@ -1930,66 +1930,66 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { strcpy(buf1, i->get_npc_name().c_str()); strcat(buf1, " "); if (AFF_FLAGGED(i, EAffect::kHorse)) - strcat(buf1, "( ) "); + strcat(buf1, "(под седлом) "); utils::CAP(buf1); } else { - sprintf(buf1, "%s%s ", i->race_or_title().c_str(), i->IsFlagged(EPlrFlag::kKiller) ? " <>" : ""); + sprintf(buf1, "%s%s ", i->race_or_title().c_str(), i->IsFlagged(EPlrFlag::kKiller) ? " <ДУШЕГУБ>" : ""); } snprintf(buf, kMaxStringLength, "%s%s", AFF_FLAGGED(i, EAffect::kCharmed) ? "*" : "", buf1); if (AFF_FLAGGED(i, EAffect::kInvisible)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(невидим%s) ", grammar::SexEnding((i)->get_sex(), 6)); if (AFF_FLAGGED(i, EAffect::kHide)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(спрятал%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (AFF_FLAGGED(i, EAffect::kDisguise)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 2)); + sprintf(buf + strlen(buf), "(замаскировал%s) ", grammar::SexEnding((i)->get_sex(), 2)); if (!i->IsNpc() && !i->desc) - sprintf(buf + strlen(buf), "(%s ) ", grammar::SexEnding((i)->get_sex(), 1)); + sprintf(buf + strlen(buf), "(потерял%s связь) ", grammar::SexEnding((i)->get_sex(), 1)); if (!i->IsNpc() && i->IsFlagged(EPlrFlag::kWriting)) - strcat(buf, "() "); + strcat(buf, "(пишет) "); if (i->GetPosition() != EPosition::kFight) { if (mount::IsOnHorse(i)) { CharData *horse = mount::GetHorse(i); if (horse) { const char *msg = - AFF_FLAGGED(horse, EAffect::kFly) ? "" : ""; - sprintf(buf + strlen(buf), "%s %s. ", + AFF_FLAGGED(horse, EAffect::kFly) ? "летает" : "сидит"; + sprintf(buf + strlen(buf), "%s здесь верхом на %s. ", msg, PersonName(horse, ch, 5)); } } else if (mount::IsHorse(i) && AFF_FLAGGED(i, EAffect::kTethered)) - sprintf(buf + strlen(buf), "%s . ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "привязан%s здесь. ", grammar::SexEnding((i)->get_sex(), 6)); else if ((sector = real_sector(i->in_room)) == ESector::kOnlyFlying) - strcat(buf, IsPoly(i) ? " . " : " . "); + strcat(buf, IsPoly(i) ? "летают здесь. " : "летает здесь. "); else if (sector == ESector::kUnderwater) - strcat(buf, IsPoly(i) ? " . " : " . "); + strcat(buf, IsPoly(i) ? "плавают здесь. " : "плавает здесь. "); else if (i->GetPosition() > EPosition::kSleep && AFF_FLAGGED(i, EAffect::kFly)) - strcat(buf, IsPoly(i) ? " . " : " . "); + strcat(buf, IsPoly(i) ? "летают здесь. " : "летает здесь. "); else if (sector == ESector::kWaterSwim || sector == ESector::kWaterNoswim) - strcat(buf, IsPoly(i) ? " . " : " . "); + strcat(buf, IsPoly(i) ? "плавают здесь. " : "плавает здесь. "); else strcat(buf, IsPoly(i) ? poly_positions[static_cast(i->GetPosition())] : positions[static_cast(i->GetPosition())]); if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && i->IsNpc() && IsAffected(i, EAffect::kCapable)) - sprintf(buf + strlen(buf), "( ) "); + sprintf(buf + strlen(buf), "(аура магии) "); } else { if (i->GetEnemy()) { - strcat(buf, IsPoly(i) ? " " : " "); + strcat(buf, IsPoly(i) ? "сражаются с " : "сражается с "); if (i->in_room != i->GetEnemy()->in_room) - strcat(buf, "- "); + strcat(buf, "чьей-то тенью"); else if (i->GetEnemy() == ch) - strcat(buf, ""); + strcat(buf, "ВАМИ"); else strcat(buf, GET_PAD(i->GetEnemy(), 4)); if (mount::IsOnHorse(i)) - sprintf(buf + strlen(buf), ", %s! ", PersonName(mount::GetHorse(i), ch, 5)); + sprintf(buf + strlen(buf), ", сидя верхом на %s! ", PersonName(mount::GetHorse(i), ch, 5)); else strcat(buf, "! "); } else // NIL fighting pointer { - strcat(buf, IsPoly(i) ? " " : " "); + strcat(buf, IsPoly(i) ? "колотят по воздуху" : "колотит по воздуху"); if (mount::IsOnHorse(i)) - sprintf(buf + strlen(buf), ", %s. ", PersonName(mount::GetHorse(i), ch, 5)); + sprintf(buf + strlen(buf), ", сидя верхом на %s. ", PersonName(mount::GetHorse(i), ch, 5)); else strcat(buf, ". "); } @@ -1998,26 +1998,26 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && !AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "( ) "); + strcat(buf, "(черная аура) "); } if (AFF_FLAGGED(ch, EAffect::kDetectAlign)) { if (i->IsNpc()) { if (alignment::IsEvil(i)) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "(- ) "); + strcat(buf, "(иссиня-черная аура) "); else - strcat(buf, "( ) "); + strcat(buf, "(темная аура) "); } else if (alignment::IsGood(i)) { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "( ) "); + strcat(buf, "(серая аура) "); else - strcat(buf, "( ) "); + strcat(buf, "(светлая аура) "); } else { if (AFF_FLAGGED(ch, EAffect::kDetectMagic) && AFF_FLAGGED(i, EAffect::kForcesOfEvil)) - strcat(buf, "( ) "); + strcat(buf, "(черная аура) "); } } else { AddPkAuraDescription(i, aura_txt); @@ -2029,7 +2029,7 @@ void ListOneChar(CharData *i, CharData *ch, ESkill mode) { // affect/spell -- every poison affect_type carries the kAfPoison flag (affects.xml; aconitum via poison.cpp). if (AFF_FLAGGED(ch, EAffect::kDetectPoison)) if (IsAffectedWithFlag(i, kAfPoison)) - sprintf(buf + strlen(buf), "(%s) ", grammar::SexEnding((i)->get_sex(), 6)); + sprintf(buf + strlen(buf), "(отравлен%s) ", grammar::SexEnding((i)->get_sex(), 6)); std::string line = buf; AppendCompactShieldSuffix(line, ch, i); @@ -2047,89 +2047,89 @@ char *diag_weapon_to_char(const CObjectPrototype *obj, int show_wear) { if (obj->get_type() == EObjType::kWeapon) { const auto skill = static_cast(obj->get_spec_param()); if (skill != ESkill::kUndefined) { - sprintf(out_str, " \"%s\".\r\n", MUD::Skill(skill).name.c_str()); + sprintf(out_str, "Принадлежит к классу \"%s\".\r\n", MUD::Skill(skill).name.c_str()); } else { - sprintf(out_str, "!! - (skill=%d) !!\r\n", obj->get_spec_param()); + sprintf(out_str, "!! Не принадлежит к известным типам оружия - сообщите Богам (skill=%d) !!\r\n", obj->get_spec_param()); } } if (show_wear) { if (CAN_WEAR(obj, EWearFlag::kFinger)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на палец.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kNeck)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на шею.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kBody)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на туловище.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHead)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на голову.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kLegs)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на ноги.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kFeet)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно обуть.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHands)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на кисти.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kArms)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на руки.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kShoulders)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на плечи.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWaist)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на пояс.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kQuiver)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно использовать как колчан.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWrist)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно надеть на запястья.\r\n"); } if (show_wear > 1) { if (CAN_WEAR(obj, EWearFlag::kShield)) { need_str = std::max(0, calc_str_req((obj->get_weight() + 1) / 2, STR_HOLD_W)); sprintf(out_str + strlen(out_str), - " ( %d %s).\r\n", + "Можно использовать как щит (требуется %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kWield)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_WIELD_W)); sprintf(out_str + strlen(out_str), - " ( %d %s).\r\n", + "Можно взять в правую руку (требуется %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kHold)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_HOLD_W)); sprintf(out_str + strlen(out_str), - " ( %d %s).\r\n", + "Можно взять в левую руку (требуется %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } if (CAN_WEAR(obj, EWearFlag::kBoth)) { need_str = std::max(0, calc_str_req(obj->get_weight(), STR_BOTH_W)); sprintf(out_str + strlen(out_str), - " ( %d %s).\r\n", + "Можно взять в обе руки (требуется %d %s).\r\n", need_str, grammar::GetDeclensionInNumber(need_str, grammar::EWhat::kStr)); } } else { if (CAN_WEAR(obj, EWearFlag::kShield)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно использовать как щит.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kWield)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно взять в правую руку.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kHold)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно взять в левую руку.\r\n"); } if (CAN_WEAR(obj, EWearFlag::kBoth)) { - sprintf(out_str + strlen(out_str), " .\r\n"); + sprintf(out_str + strlen(out_str), "Можно взять в обе руки.\r\n"); } } } @@ -2138,13 +2138,13 @@ char *diag_weapon_to_char(const CObjectPrototype *obj, int show_wear) { std::string diag_armor_type_to_char(const ObjData *obj) { if (obj->get_type() == EObjType::kLightArmor) { - return " .\r\n"; + return "Легкий тип доспехов.\r\n"; } if (obj->get_type() == EObjType::kMediumArmor) { - return " .\r\n"; + return "Средний тип доспехов.\r\n"; } if (obj->get_type() == EObjType::kHeavyArmor) { - return " .\r\n"; + return "Тяжелый тип доспехов.\r\n"; } return ""; } @@ -2152,7 +2152,7 @@ std::string diag_armor_type_to_char(const ObjData *obj) { char *diag_timer_to_char(const ObjData *obj) { static char out_str[kMaxStringLength]; *out_str = 0; - sprintf(out_str, ": %s.", diag_obj_timer(obj)); + sprintf(out_str, "Состояние: %s.", diag_obj_timer(obj)); return (out_str); } @@ -2165,10 +2165,10 @@ char *diag_uses_to_char(ObjData *obj, CharData *ch) { && IS_MANA_CASTER(ch)) { int i = -1; if ((i = GetObjRnum(GET_OBJ_VAL(obj, 1))) >= 0) { - sprintf(out_str, ": %s%s%s.\r\n", + sprintf(out_str, "Прототип: %s%s%s.\r\n", kColorBoldCyn, obj_proto[i]->get_PName(grammar::ECase::kNom).c_str(), kColorNrm); } - sprintf(out_str + strlen(out_str), " : %s%d&n.\r\n", + sprintf(out_str + strlen(out_str), "Осталось применений: %s%d&n.\r\n", GET_OBJ_VAL(obj, 2) > 100 ? "&G" : "&R", GET_OBJ_VAL(obj, 2)); } return (out_str); @@ -2180,31 +2180,31 @@ char *diag_shot_to_char(ObjData *obj, CharData *ch) { *out_str = 0; if (obj->get_type() == EObjType::kMagicContaner && (ch->GetClass() == ECharClass::kRanger || ch->GetClass() == ECharClass::kCharmer || IS_MANA_CASTER(ch))) { - sprintf(out_str + strlen(out_str), " : %s%d&n.\r\n", + sprintf(out_str + strlen(out_str), "Осталось стрел: %s%d&n.\r\n", GET_OBJ_VAL(obj, 2) > 3 ? "&G" : "&R", GET_OBJ_VAL(obj, 2)); } return (out_str); } -// +// Чтобы можно было получить только строку состяния const char *diag_obj_timer(const ObjData *obj) { int prot_timer; if (obj->get_rnum() != kNothing) { if (stable_objs::IsTimerUnlimited(obj)) { - return ""; + return "нерушимо"; } if (obj->get_craft_timer() > 0) { - prot_timer = obj->get_craft_timer();// , + prot_timer = obj->get_craft_timer();// если вещь скрафчена, смотрим ее таймер а не у прототипа } else { prot_timer = obj_proto[obj->get_rnum()]->get_timer(); } if (!prot_timer) { - return " !\r\n"; + return "Прототип предмета имеет нулевой таймер!\r\n"; } - const int tm = (obj->get_timer() * 100 / prot_timer); // , + const int tm = (obj->get_timer() * 100 / prot_timer); // если вещь скрафчена, смотрим ее таймер а не у прототипа return print_obj_state(tm); } return ""; @@ -2212,22 +2212,22 @@ const char *diag_obj_timer(const ObjData *obj) { const char *print_obj_state(int tm_pct) { if (tm_pct < 20) - return ""; + return "ужасно"; else if (tm_pct < 40) - return " "; + return "скоро сломается"; else if (tm_pct < 60) - return ""; + return "плоховато"; else if (tm_pct < 80) - return ""; - //else if (tm_pct <=100) // 100% , <= - // return ""; - else if (tm_pct < 1000) // , - return ""; - else return ""; + return "средне"; + //else if (tm_pct <=100) // у только что созданной шмотки значение 100% первый тик, потому <= + // return "идеально"; + else if (tm_pct < 1000) // проблема крафта, на хаймортах таймер больще прототипа + return "идеально"; + else return "нерушимо"; } -// , " ()" -// , handler, - +// Это не совсем механика зрения, нужен отдельный модуль на тему "кто кого (не)видит" +// Но пока эти функции распиханы по всему коду, начиная с handler, по этому временно - тут void Appear(CharData *ch) { const bool appear_msg = AFF_FLAGGED(ch, EAffect::kInvisible) || AFF_FLAGGED(ch, EAffect::kDisguise) @@ -2249,9 +2249,9 @@ void Appear(CharData *ch) { if (appear_msg) { if (ch->IsNpc() || GetRealLevel(ch) < kLvlImmortal) { - act("$n $u .", false, ch, nullptr, nullptr, kToRoom); + act("$n медленно появил$u из пустоты.", false, ch, nullptr, nullptr, kToRoom); } else { - act(" $n1.", + act("Вы почувствовали странное присутствие $n1.", false, ch, nullptr, nullptr, kToRoom); } } diff --git a/src/gameplay/statistics/dps.cpp b/src/gameplay/statistics/dps.cpp index 7f41ab78c..fd5c59f27 100644 --- a/src/gameplay/statistics/dps.cpp +++ b/src/gameplay/statistics/dps.cpp @@ -13,10 +13,10 @@ namespace DpsSystem { -// - +// кол-во учитываемых чармисов с каждого чара const unsigned MAX_DPS_CHARMICE = 5; -// * -. +// * Добавление эффективной дамаги и овер-дамаги. void DpsNode::add_dmg(int dmg, int over_dmg) { if (dmg >= 0 && over_dmg >= 0) { dmg_ += dmg; @@ -26,8 +26,8 @@ void DpsNode::add_dmg(int dmg, int over_dmg) { } /** -* . -* . +* Имя для вывода в статистике при отсутствии онлайн. +* Пока осталось актуальным только для чармисов. */ void DpsNode::set_name(const char *name) { if (name && *name) { @@ -38,7 +38,7 @@ void DpsNode::set_name(const char *name) { } } -// * . +// * Расчет дамаги в раунд. int DpsNode::get_stat() const { if (rounds_) { return dmg_ / rounds_; @@ -58,7 +58,7 @@ const std::string &DpsNode::get_name() const { return name_; } -// * , . +// * Используется для идентификации в списках как чармисов, так и чаров. long DpsNode::get_id() const { return id_; } @@ -91,7 +91,7 @@ void Dps::AddDmg(int type, CharData *ch, int dmg, int over_dmg) { AddGroupCharmDmg(ch, dmg, over_dmg); } break; - default: log("SYSERROR: , func: %s", __func__); + default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); return; } } @@ -106,12 +106,12 @@ void Dps::EndRound(int type, CharData *ch) { break; case GROUP_CHARM_DPS: EndGroupCharmRound(ch); break; - default: log("SYSERROR: , func: %s", __func__); + default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); return; } } -// * . +// * Очистка себя или группы. void Dps::Clear(int type) { switch (type) { case PERS_DPS: { @@ -126,12 +126,12 @@ void Dps::Clear(int type) { case GROUP_DPS: group_dps_.clear(); break; case GROUP_CHARM_DPS: break; - default: log("SYSERROR: , func: %s", __func__); + default: log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); return; } } -// * . +// * Для сортировки вывода по нанесенной дамаге. struct sort_node { sort_node(const std::string &in_name, int in_dps, unsigned in_round_dmg, unsigned in_over_dmg) : dps(in_dps), round_dmg(in_round_dmg), over_dmg(in_over_dmg) { @@ -146,7 +146,7 @@ struct sort_node { typedef std::multimap SortGroupType; SortGroupType tmp_group_list; -// +// суммарный дамаг группы при распечатке статистики unsigned tmp_total_dmg = 0; void Dps::AddTmpGroupList(CharData *ch) { @@ -161,9 +161,9 @@ void Dps::AddTmpGroupList(CharData *ch) { } /** -* . -* \param ch - , . -* \param coder - , = 0 +* Распечатка персональной статистики игрока и его чармисов. +* \param ch - игрок, которому идет распечатка. +* \param coder - случай вывода статистики другому персонажу, по умолчанию = 0 */ void Dps::PrintStats(CharData *ch, CharData *coder) { if (!coder) { @@ -185,15 +185,15 @@ void Dps::PrintStats(CharData *ch, CharData *coder) { } /** - * . - * @param ch - , . - * @param out - . + * Вывести в поток персональную статистику по урону. + * @param ch - персонаж, для которого формируется статистика. + * @param out - поток для вывода. */ void Dps::PrintPersonalDpsStat(CharData *ch, std::ostringstream &out) { - out << " :" << "\r\n"; + out << " Персональная статистика урона:" << "\r\n"; table_wrapper::Table table; table << table_wrapper::kHeader << - "" << "" << " " << "." << " " << table_wrapper::kEndRow; + "Имя" << "Урон" << "В раунд" << "Макс." << "Лишний урон" << table_wrapper::kEndRow; table << ch->get_name() << pers_dps_.get_dmg() << pers_dps_.get_stat() @@ -206,29 +206,29 @@ void Dps::PrintPersonalDpsStat(CharData *ch, std::ostringstream &out) { } /** - * . - * @param out - . + * Вывести в поток персональную статистику по полученному и потерянному опыту. + * @param out - выводной поток. */ void Dps::PrintPersonalExpStat(std::ostringstream &out) const { - out << " :" << "\r\n"; + out << " Персональная статистика по опыту:" << "\r\n"; double percent = exp_ ? battle_exp_ * 100.0 / exp_ : 0.0; int balance = exp_ + lost_exp_; - out << kColorBoldBlu << " *" << kColorNrm << " : " << PrintNumberByDigits(exp_) - << " : " << PrintNumberByDigits(battle_exp_) + out << kColorBoldBlu << " *" << kColorNrm << " Всего получено опыта: " << PrintNumberByDigits(exp_) + << " Из него за удары: " << PrintNumberByDigits(battle_exp_) << " (" << std::setprecision(2) << percent << "%)" << "\r\n" - << kColorBoldBlu << " *" << kColorNrm << " : " << PrintNumberByDigits(abs(lost_exp_)); + << kColorBoldBlu << " *" << kColorNrm << " Потеряно опыта: " << PrintNumberByDigits(abs(lost_exp_)); if (balance != 0) { - out << " : " << (balance > 0 ? "+" : "-") << PrintNumberByDigits(abs(balance)) << "\r\n"; + out << " Баланс: " << (balance > 0 ? "+" : "-") << PrintNumberByDigits(abs(balance)) << "\r\n"; } } /** -* , . -* \param ch - , . -* \param coder - , = nullptr +* Распечатка групповой статистики, находящейся у лидера группы. +* \param ch - игрок, которому идет распечатка. +* \param coder - случай вывода статистики другому персонажу, по умолчанию = nullptr */ void Dps::PrintGroupStats(CharData *ch, CharData *coder) { if (!coder) { @@ -250,11 +250,11 @@ void Dps::PrintGroupStats(CharData *ch, CharData *coder) { } std::ostringstream out; - out << " :" << "\r\n"; + out << " Статистика урона вашей группы:" << "\r\n"; table_wrapper::Table table; table << table_wrapper::kHeader << - "" << "" << "(%)" << " " << "." << " " << table_wrapper::kEndRow; + "Имя" << "Урон" << "(%)" << "В раунд" << "Макс." << "Лишний урон" << table_wrapper::kEndRow; for (auto it = tmp_group_list.rbegin(); it != tmp_group_list.rend(); ++it) { double percent = tmp_total_dmg ? it->first * 100.0 / tmp_total_dmg : 0.0; table << it->second.name @@ -319,7 +319,7 @@ void Dps::EndGroupCharmRound(CharData *ch) { } } -// * dps_copy, .. +// * Чтобы не морочить голову в dps_copy, заменяем только груп.статистику. Dps &Dps::operator=(const Dps ©) { if (this != ©) { group_dps_ = copy.group_dps_; @@ -368,7 +368,7 @@ void PlayerDpsNode::add_charm_dmg(CharData *ch, int dmg, int over_dmg) { if (i != charm_list_.end()) { i->add_dmg(dmg, over_dmg); } else { - log("SYSERROR: , func: %s", __func__); + log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); } } @@ -377,11 +377,11 @@ void PlayerDpsNode::end_charm_round(CharData *ch) { if (it != charm_list_.end()) { it->end_round(); } else { - log("SYSERROR: , func: %s", __func__); + log("SYSERROR: мы не должны были сюда попасть, func: %s", __func__); } } -// * . +// * Чармисы в персональной статистике выводятся без сортировки. void PlayerDpsNode::print_charm_stats(table_wrapper::Table &table) const { for (const auto & it : charm_list_) { if (it.get_dmg() > 0) { @@ -395,7 +395,7 @@ void PlayerDpsNode::print_charm_stats(table_wrapper::Table &table) const { } } -// * . +// * Распечатка групповой статистики живых чармисов по данному игроку. void PlayerDpsNode::print_group_charm_stats(CharData *ch) const { for (auto *f : ch->followers) { if (!IsCharmice(f)) { @@ -417,7 +417,7 @@ void PlayerDpsNode::print_group_charm_stats(CharData *ch) const { // PlayerDpsNode //////////////////////////////////////////////////////////////////////////////// -// * , . +// * Подсчет дамаги за предыдущий раунд, дергается в начале раунда и по окончанию боя. void check_round(CharData *ch) { if (!ch->IsNpc()) { ch->dps_end_round(DpsSystem::PERS_DPS); @@ -470,17 +470,17 @@ void UpdateDpsStatistics(CharData *ch, int real_dam, int over_dam) { namespace { const char *DMETR_FORMAT = - " :\r\n" - " - \r\n" - " - \r\n" - " - ( )\r\n"; + "Формат команды:\r\n" + "дметр - вывод всей статистики\r\n" + "дметр очистить - очистка персональной статистики\r\n" + "дметр очистить группа - очистка групповой статистики (только лидер)\r\n"; } // namespace /** -* '' - , . -* ' ' - . -* ' ' - ( ). +* 'дметр' - вывод своей статистики и групповой, если в группе. +* 'дметр очистить' - очистка своей статистики. +* 'дметр очистить группа' - очистка групповой статистики (только лидер). */ void do_dmeter(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (ch->IsNpc()) { @@ -492,28 +492,28 @@ void do_dmeter(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { if (!*arg) { ch->dps_print_stats(); - } else if (isname(arg, "")) { + } else if (isname(arg, "очистить")) { if (!*name) { ch->dps_clear(DpsSystem::PERS_DPS); - SendMsgToChar(" .\r\n", ch); - } else if (isname(name, "")) { + SendMsgToChar("Персональная статистика очищена.\r\n", ch); + } else if (isname(name, "группа")) { if (!AFF_FLAGGED(ch, EAffect::kGroup)) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не состоите в группе.\r\n", ch); return; } if (ch->has_master()) { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Вы не являетесь лидером группы.\r\n", ch); return; } ch->dps_clear(DpsSystem::GROUP_DPS); } } else if (ch->IsFlagged(EPrf::kCoderinfo)) { - // + // распечатка статистики указанного персонажа CharData *vict = target_resolver::FindPlayerVis(ch, arg); if (vict) { vict->dps_print_stats(ch); } else { - SendMsgToChar(" .\r\n", ch); + SendMsgToChar("Нет такого персонажа.\r\n", ch); } } else { SendMsgToChar(DMETR_FORMAT, ch); diff --git a/src/gameplay/statistics/mob_stat.cpp b/src/gameplay/statistics/mob_stat.cpp index d2a4d5cb3..5c51b8c4d 100644 --- a/src/gameplay/statistics/mob_stat.cpp +++ b/src/gameplay/statistics/mob_stat.cpp @@ -22,22 +22,22 @@ namespace char_stat { -/// - '' +/// мобов убито - для 'статистика' int mobs_killed = 0; -/// - '' +/// игроков убито - для 'статистика' int players_killed = 0; using ClassExpStat = std::pair; using ExpStatArray = std::array; using ExpStatArrayPtr = std::unique_ptr; -/// - '' +/// Опыт за ребут с делением по профам - для 'статистика' std::unordered_map classes_exp_stat; /* - * : , - - * . - * \todo global_objects - * , , . ABYRVALG + * Подобная инициализация небезопасна: всегда существует возможность, что кто-то сунется в структуру + * статистики до ее инициализации. + * \todo Нужно сделать глобальное хранилище статистики в global_objects и засунуть это все туда + * инициализацию, соответственно, разместить в процедуре загрузки мада. ABYRVALG */ void InitClassesExpStat() { for ( const auto &it : MUD::Classes()) { @@ -89,11 +89,11 @@ void PrintClassesExpStat(std::ostringstream &out) { auto tmp_array = BuildExpStatArray(); const unsigned long long top_exp = (*tmp_array)[0].second; - out << " :" << "\r\n" << "\r\n"; + out << " Соотношения набранного с перезагрузки опыта:" << "\r\n" << "\r\n"; /* - * 2 . - * : - * , . + * При более чем 2 столбцах будет работать неправильно. + * Но возиться сейчас с написанием универсальной функции не вижу смысла: + * нужен полноценный модуль форматирования таблиц, а не такие костыли по всему коду. * \todo table format */ const int columns{2}; @@ -130,12 +130,12 @@ namespace mob_stat { // mob_stat_new.xml (legacy XML fallback) and mob_stat.bin (current binary store) live // under state/statistics/; their paths come from StateManager (kMobStat / kMobStatBin). -/// (+ ) +/// за сколько месяцев хранится статистика (+ текущий месяц) const int kMobStatHistorySize = 6; -/// - show stats +/// выборка кол-ва мобов для show stats при старте мада std::map count_stats; std::map kill_stats; -/// +/// список мобов по внуму и месяцам const time_t MobKillStat::default_date = 0; @@ -150,14 +150,14 @@ std::pair GetDate() { namespace { -// mob_stat. , , -// XML ( DOM), e. -// : . +// Бинарный формат mob_stat. Файл машинный, руками не правится, поэтому +// XML тут только тормозил и запись (построение DOM), и чтение при бутe. +// Порядок байт нативный: файл не шарится между архитектурами. // -// kills ( + , ) -- -// - kills[kMaxGroupSize+1] -// XML ( n#). -constexpr uint32_t kMobStatBinMagic = 0x4253544D; // 'MSTB' little-endian +// Записи kills хранятся разреженно (индекс + значение, только ненулевые) -- +// иначе фикс-массив kills[kMaxGroupSize+1] на каждый стат раздувал файл +// больше XML (там тоже писались только ненулевые n#). +constexpr uint32_t kMobStatBinMagic = 0x4253544D; // 'MSTB' в little-endian constexpr uint32_t kMobStatBinVersion = 2; template @@ -172,8 +172,8 @@ bool BinRead(std::istream &in, T &value) { } // namespace -// + count_stats/kill_stats -// . legacy-XML . +// Валидация + накопление count_stats/kill_stats для одного разобранного +// стата. Общая для бинарного и legacy-XML путей загрузки. static bool ApplyParsedStat(MobKillStat &tmp_time, MobMonthKillStat tmp_mob) { if (tmp_mob.month <= 0 || tmp_mob.month > 12 || tmp_mob.year <= 0) { return false; @@ -193,8 +193,8 @@ static bool ApplyParsedStat(MobKillStat &tmp_time, MobMonthKillStat tmp_mob) { return true; } -// . true -- ( -// : XML), false -- ( ). +// Чтение бинарного файла. true -- файл существует (даже если оказался +// битым: тогда просто не падаем в XML), false -- файла нет (нужна миграция). static bool LoadBinary() { std::ifstream in(MUD::StateManager().Path(state::EStateFile::kMobStatBin), std::ios::binary); if (!in.is_open()) { @@ -209,8 +209,8 @@ static bool LoadBinary() { return false; } if (!BinRead(in, version) || version != kMobStatBinVersion) { - // / -- XML-, Save - // bin . + // Старый/чужой формат -- падаем в XML-миграцию, следующий Save + // перезапишет bin актуальной версией. mudlog("mob_stat.bin: unsupported version, fall back to XML", CMP, kLvlImmortal, SYSLOG, true); return false; } @@ -236,7 +236,7 @@ static bool LoadBinary() { } tmp_mob.month = month; tmp_mob.year = year; - // kills: nz (, ). + // Разреженные kills: nz пар (индекс, значение). uint8_t nz = 0; if (!BinRead(in, nz)) { break; @@ -267,14 +267,14 @@ static bool LoadBinary() { return true; } -// XML (mob_stat_new.xml). Save -// , . +// Разовая миграция со старого XML (mob_stat_new.xml). После первого Save +// данные уже в бинаре, и этот путь больше не используется. static void LoadXmlLegacy() { char buf_[kMaxInputLength]; pugi::xml_document doc; - // KOI8-R; , - // (issue #3681). KOI8-R . + // Файл лежит на диске в KOI8-R; читаем через границу кодировки, а разбираем уже + // буфер в нативной кодировке движка (issue #3681). Под KOI8-R это тождество. const std::string xml_mob_stat = native_text::read_data_file(MUD::StateManager().Path(state::EStateFile::kMobStat)); pugi::xml_parse_result result = doc.load_buffer(xml_mob_stat.data(), xml_mob_stat.size()); if (!result) { @@ -304,7 +304,7 @@ static void LoadXmlLegacy() { kill_date = static_cast(xml_date.as_ullong(kill_date)); } - // + // инит статы конкретного моба по месяцам MobKillStat tmp_time(kill_date); for (pugi::xml_node xml_time = xml_mob.child("t"); xml_time; xml_time = xml_time.next_sibling("t")) { @@ -347,7 +347,7 @@ void Save() { for (const auto &stat : i.second.stats) { BinWrite(buf, static_cast(stat.month)); BinWrite(buf, static_cast(stat.year)); - // : kills (, ). + // Разреженно: только ненулевые kills как пары (индекс, значение). uint8_t nz = 0; for (int g = 0; g <= kMaxGroupSize; ++g) { if (stat.kills.at(g) > 0) { @@ -364,9 +364,9 @@ void Save() { } } - // -- : , - // (. ). .tmp + rename - // . + // Запись синхронно в основном потоке -- без отдельного треда: замеряем, + // нужен ли он вообще (см. лог времени ниже). Через .tmp + rename для + // атомарности. const std::string filename = MUD::StateManager().Path(state::EStateFile::kMobStatBin); const std::string tmp = filename + ".tmp"; { @@ -394,14 +394,14 @@ void ClearZoneStat(ZoneVnum zone_vnum) { void ShowStats(CharData *ch) { std::stringstream out; - out << " : " + out << " Всего уникальных мобов в статистике убийств: " << mob_stat_register.size() << "\r\n" - << " :"; + << " Количество уникальных мобов по месяцам:"; for (auto & count_stat : count_stats) { out << " " << std::setw(2) << std::setfill('0') << count_stat.first << ":" << count_stat.second; } - out << "\r\n" << " :"; + out << "\r\n" << " Количество убитых мобов по месяцам:"; for (auto & kill_stat : kill_stats) { out << " " << std::setw(2) << std::setfill('0') << kill_stat.first << ":" << kill_stat.second; } @@ -414,7 +414,7 @@ void UpdateMobNode(std::list &node_list, int members) { auto date = GetDate(); auto k = node_list.rbegin(); const int months = k->month + k->year * 12; - // + // сравнение номера месяца с переключением на следующий if (months == date.first + date.second * 12) { k->kills.at(members) += 1; } else { @@ -423,7 +423,7 @@ void UpdateMobNode(std::list &node_list, int members) { node.year = date.second; node.kills.at(members) += 1; node_list.push_back(node); - // - + // проверка на переполнение кол-ва месяцев if (node_list.size() > kMobStatHistorySize + 1) { node_list.erase(node_list.begin()); } @@ -446,7 +446,7 @@ void GetLastMobKill(CharData *mob, std::string &result) { const auto killtime = i->second.date; result = asctime(localtime(&killtime)); } else { - result = "!\r\n"; + result = "никогда!\r\n"; } } @@ -519,9 +519,9 @@ void ShowZoneMobKillsStat(CharData *ch, ZoneVnum zone_vnum, int months) { } std::stringstream out; - out << " " << zone_vnum - << ", : " << months << "\r\n" - " vnum : : pk : = (n3=100 100 )\r\n\r\n"; + out << "Статистика убийств мобов в зоне номер " << zone_vnum + << ", месяцев: " << months << "\r\n" + " vnum : имя : pk : группа = убийств (n3=100 моба убили 100 раз втроем)\r\n\r\n"; for (auto & i : sort_list) { out << i.first << " : " << fmt::format("{:>20}", PrintMobName(i.first, 20)) << " : " diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 7a89968df..223b742c6 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -17,9 +17,9 @@ namespace text_id { /// -/// =/= -/// - . -/// . +/// Содержит списки соответствия номер=строка/строка=номер для конверта +/// внутри-игровых констант в строковые ИД и обратно. +/// Нужно для независимости файлов от изменения номеров констант в коде. /// class TextIdNode { public: @@ -33,11 +33,11 @@ class TextIdNode { std::unordered_map str_to_num; }; -// +// общий список конвертируемых констант std::array text_id_list; /// -/// . +/// Инит текстовых ИД параметров предметов для сохранения в файл. /// void InitObjVals() { // issue.magic-items: unified magic-item payload. The FIRST string per key is canonical (Add keeps the @@ -78,7 +78,7 @@ void InitObjVals() { } /// -/// , . +/// Общий инит системы текстовых ИД, дергается при старте мада. /// void Init() { /// OBJ_VALS @@ -87,8 +87,8 @@ void Init() { } /// -/// . -/// \return -1, +/// Конвертирование текстового ИД константы в ее значение в коде. +/// \return значение константы или -1, если ничего не было найдено /// int ToNum(EIdType type, const std::string &str) { if (type < kTextIdCount) { @@ -98,8 +98,8 @@ int ToNum(EIdType type, const std::string &str) { } /// -/// . -/// \return , +/// Конвертирование значения константы в ее текстовый ИД. +/// \return текстовый ИД константы или пустая строка, если ничего не было найдено /// std::string ToStr(EIdType type, int num) { if (type < kTextIdCount) { @@ -109,7 +109,7 @@ std::string ToStr(EIdType type, int num) { } /// -/// =/= +/// Добавление соответствия значение=константа/константа=значение /// void TextIdNode::Add(int num, std::string str) { num_to_str.insert(std::make_pair(num, str)); @@ -117,7 +117,7 @@ void TextIdNode::Add(int num, std::string str) { } /// -/// -> +/// Конвертирование значение -> текстовый ИД /// std::string TextIdNode::ToStr(int num) const { auto i = num_to_str.find(num); @@ -128,7 +128,7 @@ std::string TextIdNode::ToStr(int num) const { } /// -/// -> +/// Конвертирование текстовый ИД -> значение /// int TextIdNode::ToNum(const std::string &str) const { auto i = str_to_num.find(str); @@ -143,8 +143,8 @@ int TextIdNode::ToNum(const std::string &str) const { namespace parse { char buf[kMaxStringLength]; /// -/// \param text -/// \return -1, +/// Попытка конвертирования \param text в с перехватом исключения +/// \return число или -1, в случае неудачи /// int CastToInt(const char *text) { int result = -1; @@ -160,10 +160,10 @@ int CastToInt(const char *text) { } /// -/// pugixml -/// - -/// -/// \return -1 +/// Обертка на pugixml для чтения числового аттрибута +/// с логирование в имм- и сислог +/// В конфиге это выглядит как +/// \return -1 в случае неудачи int ReadAttrAsInt(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); if (!attr) { @@ -173,7 +173,7 @@ int ReadAttrAsInt(const pugi::xml_node &node, const char *text) { return CastToInt(attr.value()); } -// , attr_int, , , -1 +// тоже самое, что и attr_int, только, если элемента нет, возвращает -1 int ReadAttrAsIntT(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); if (!attr) { @@ -183,8 +183,8 @@ int ReadAttrAsIntT(const pugi::xml_node &node, const char *text) { } /// -/// , ReadAttrAsInt, child_value() -/// 1234 +/// Тоже, что и ReadAttrAsInt, только для чтения child_value() +/// В конфиге это выглядит как 1234 /// int ReadChildValueAsInt(const pugi::xml_node &node, const char *text) { pugi::xml_node child_node = node.child(text); @@ -196,7 +196,7 @@ int ReadChildValueAsInt(const pugi::xml_node &node, const char *text) { } /// -/// ReadAttrAsInt, \return +/// Аналог ReadAttrAsInt, \return строку со значением или пустую строку /// std::string ReadAattrAsStr(const pugi::xml_node &node, const char *text) { pugi::xml_attribute attr = node.attribute(text); @@ -210,7 +210,7 @@ std::string ReadAattrAsStr(const pugi::xml_node &node, const char *text) { } /// -/// ReadChildValueAsInt, \return +/// Аналог ReadChildValueAsInt, \return строку со значением или пустую строку /// std::string ReadChildValueAsStr(const pugi::xml_node &node, const char *text) { pugi::xml_node child_node = node.child(text); @@ -243,8 +243,8 @@ pugi::xml_node GetChild(const pugi::xml_node &node, const char *name) { } /// -/// -/// \return true - () +/// проверка валидности внума объекта с логированием ошибки в имм и сислог +/// \return true - если есть прототип объекта (рнум) с данным внумом /// bool IsValidObjVnum(int vnum) { if (GetObjRnum(vnum) < 0) { @@ -259,8 +259,8 @@ bool IsValidObjVnum(int vnum) { // ===================================================================================================================== /** - * value . - * Ecxeption: , "string is empty"; + * Прочитать значение value как строку. + * Ecxeption: если строка пуста, сообщение "string is empty"; */ const char *ReadAsStr(const char *value) { if (strcmp(value, "") == 0) { @@ -270,8 +270,8 @@ const char *ReadAsStr(const char *value) { } /** - * value int. - * Ecxeption: , - value. + * Прочитать значение value как int. + * Ecxeption: при неудаче, сообщение - содержимое value. */ int ReadAsInt(const char *value) { try { @@ -282,7 +282,7 @@ int ReadAsInt(const char *value) { } /** - * value int, 'I' num_set. + * Прочитать значение value как массив int, разделенный 'I' и записать знаения в num_set. */ void ReadAsIntSet(std::unordered_set &num_set, const char *value) { if (strcmp(value, "") == 0) { @@ -299,8 +299,8 @@ void ReadAsIntSet(std::unordered_set &num_set, const char *value) { } /** - * value float. - * Ecxeption: , - value. + * Прочитать значение value как float. + * Ecxeption: при неудаче, сообщение - содержимое value. */ float ReadAsFloat(const char *value) { try { @@ -311,8 +311,8 @@ float ReadAsFloat(const char *value) { } /** - * value double. - * Ecxeption: , - value. + * Прочитать значение value как double. + * Ecxeption: при неудаче, сообщение - содержимое value. */ double ReadAsDouble(const char *value) { try { @@ -323,9 +323,9 @@ double ReadAsDouble(const char *value) { } /** - * value bool. - * true, "1", "true", "T", "t", "Y" "y" false . - * Ecxeption: value , - "value is empty". + * Прочитать значение value как bool. + * Возвращает true, если значение "1", "true", "T", "t", "Y" или "y" и false в ином случае. + * Ecxeption: если value пусто, сообщение - "value is empty". */ bool ReadAsBool(const char *value) { if (strcmp(value, "") == 0) { @@ -335,9 +335,9 @@ bool ReadAsBool(const char *value) { || strcmp(value, "T") == 0 || strcmp(value, "y") == 0 || strcmp(value, "Y") == 0); } -// issue.xml-parse-cleaning: DataNode ( -// AttrInt/AttrStr, ). // -// -> def. +// issue.xml-parse-cleaning: единые толерантные читатели атрибутов DataNode (заменяют +// локальные AttrInt/AttrStr, ранее продублированные в загрузчиках). Пустой/отсутствующий/ +// неразобранный атрибут -> def. int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { const char *v = node.GetValue(key); if (!v || !*v) { @@ -352,11 +352,11 @@ int AttrInt(const parser_wrapper::DataNode &node, const char *key, int def) { std::string AttrStr(const parser_wrapper::DataNode &node, const char *key, const char *def) { const char *v = node.GetValue(key); - // : DataNode - // , . - // ݣ , , ݣ - // , -- cfg/mechanics/obj_sets.xml 120 - // 6,7 (issue #3681). + // Перекодировки здесь НЕТ и быть не должно: DataNode приводит содержимое файла к нативной + // кодировке один раз на документ, поэтому значение уже нативное. Пока перекодировка стояла + // ещё и здесь, каждое поле переводилось дважды, а так как конфиги движок ещё и пишет + // обратно, файл рос на каждой загрузке -- cfg/mechanics/obj_sets.xml так дорос со 120 КБ + // до 6,7 ГБ и убивал загрузку по памяти (issue #3681). return (v && *v) ? std::string(v) : std::string(def); } @@ -469,7 +469,7 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { beg_idx = in_buffer.find_first_not_of(' '); if (beg_idx != std::string::npos) { - // + // случай с кавычками if (in_buffer[beg_idx] == '\'') { if (std::string::npos != (beg_idx = in_buffer.find_first_not_of('\'', beg_idx))) { if (std::string::npos == (end_idx = in_buffer.find_first_of('\'', beg_idx))) { @@ -480,7 +480,7 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { in_buffer.erase(0, ++end_idx); } } - // + // случай с одним параметром через пробел } else { if (std::string::npos != (beg_idx = in_buffer.find_first_not_of(' ', beg_idx))) { if (std::string::npos == (end_idx = in_buffer.find_first_of(' ', beg_idx))) { @@ -499,14 +499,14 @@ void GetOneParam(std::string &in_buffer, std::string &out_buffer) { out_buffer.clear(); } -// , - () +// регистронезависимое сравнение двух строк по длине первой, флаг - для учета длины строк (неравенство) bool CompareParam(const std::string &buffer, const char *str, bool full) { if (!str || !*str || buffer.empty() || (full && buffer.length() != strlen(str))) { return false; } - // , : UTF-8 ң - // (issue #3681). -- , . + // Посимвольно, а не побайтово: под UTF-8 регистр русской буквы по одному байту не берётся + // (issue #3681). Шаг обеих строк -- на длину символа, они здесь всегда в одной кодировке. std::string::size_type i; for (i = 0; i != buffer.length() && *str;) { if (!native_text::chars_equal_ci(buffer.c_str() + i, str)) { @@ -524,14 +524,14 @@ bool CompareParam(const std::string &buffer, const char *str, bool full) { } } -// +// тоже самое с обоими аргументами стринг bool CompareParam(const std::string &buffer, const std::string &buffer2, bool full) { if (buffer.empty() || buffer2.empty() || (full && buffer.length() != buffer2.length())) { return false; } - // , (issue #3681). + // Посимвольно, как и в перегрузке выше (issue #3681). std::string::size_type i; for (i = 0; i != buffer.length() && i != buffer2.length();) { if (!native_text::chars_equal_ci(buffer.c_str() + i, buffer2.c_str() + i)) { diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 7d729ae6c..dec5c04ca 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -117,7 +117,7 @@ bool IsAbbr(const char *arg1, const char *arg2) { return false; } - // (issue #3681): UTF-8 . + // Посимвольно (issue #3681): побайтное сравнение под UTF-8 теряет регистронезависимость. while (*arg1 && *arg2) { if (!native_text::chars_equal_ci(arg1, arg2)) { return false; @@ -200,10 +200,10 @@ std::vector Split(const std::string s, char delimiter) { TrimLeft(token); tokens.push_back(token); } - return tokens; // 1 + return tokens; //если разделитель не найден вернется 1 элемент содержащий полную строку } -// +// первое слово разделенное маской std::string FirstWordOnString(std::string s, std::string mask) { int pos = s.find_first_of(mask); if (pos > 0) @@ -211,9 +211,9 @@ std::string FirstWordOnString(std::string s, std::string mask) { return s; } -// one_argument string -// , , remains -// ExtractFirstArgument(str, str) - +// аналог one_argument для string +// пропускает ведущие пробелы, возвращает первое слово, в remains остаток после пробела +// безопасно вызывать как ExtractFirstArgument(str, str) - нет проблем с алиасингом std::string ExtractFirstArgument(const std::string &s, std::string &remains) { auto start = s.find_first_not_of(' '); if (start == std::string::npos) { @@ -267,8 +267,8 @@ void ConvertToLow(char *text) { } std::string SubstStrToLow(std::string s) { - // NB: "ToLow", . , - // -- . + // NB: имя говорит "ToLow", а тело поднимает регистр. Расхождение предсуществующее, + // поведение сохранено намеренно -- меняется только байтовая семантика на символьную. native_text::to_upper(s); return s; } @@ -365,11 +365,11 @@ std::string FixDot(std::string s) { return s; } -// ģ , . -- -// Windows-1251 ( , KOI8-R), -// -- UTF-8 : KOI8-R -> Windows-1251 -// , -// (issue #3681). native_text::sort_key , . +// Сортировка идёт по ключу, а не по перекодированной на месте строке. Прежний трюк -- перегнать +// список в Windows-1251 (там русские буквы стоят по алфавиту, в отличие от KOI8-R), отсортировать +// и перегнать обратно -- под UTF-8 портил текст: побайтовая таблица KOI8-R -> Windows-1251 для +// многобайтовой строки не значит ничего, и обратное преобразование уже не восстанавливало исходник +// (issue #3681). native_text::sort_key даёт ровно тот же порядок, ничего не меняя в самих строках. void SortKoiString(std::vector &str) { std::sort(str.begin(), str.end(), [](const std::string &a, const std::string &b) { return native_text::sort_key(a) < native_text::sort_key(b); @@ -442,9 +442,9 @@ std::string CompressSymbol(std::string s, const char ch) { const char *first_letter(const char *txt) { if (txt) { while (*txt && !a_isalpha(*txt)) { - //, - // , - // (. CLEAR_SCREEN) + //Предполагается, что для отправки клиенту используется только управляющий код с цветом + //На данный момент в коде присутствует только еще один управляющий код для очистки экрана, + //но он не используется (см. CLEAR_SCREEN) if ('\x1B' == *txt) { while (*txt && 'm' != *txt) { ++txt; @@ -738,9 +738,9 @@ char *str_str(const char *cs, const char *ct) { if (!cs || !ct) { return nullptr; } - // : UTF-8 - // ( -- , KOI8-R), , - // (issue #3681). + // Сравниваем и шагаем ПО СИМВОЛАМ: под UTF-8 побайтовое приведение регистра для кириллицы + // неверно (таблица регистра -- байтовая, KOI8-R), и поиск подстроки то не находил совпадения, + // то находил ложные на границе байтов (issue #3681). while (*cs) { const char *t = ct; while (*cs && !native_text::chars_equal_ci(cs, t)) { @@ -988,24 +988,24 @@ void kill_ems(std::string &str) { std::string utils::OutWordsList(const std::vector &words, size_t max_length, const std::string &separator, const std::string &prefix) { - // prefix , - // separator (first true -- ģ - // ", "). . + // prefix печатается один раз и занимает место на первой строке, но не + // склеивается через separator (first остаётся true -- первое слово идёт + // сразу за префиксом без ", "). Видимую длину считаем без цветокодов. std::string result = prefix; size_t line_length = GetStringWithoutColors(prefix).size(); bool first = true; - // separator -- , - // (", " , " " ). - // , - // \r\n ( "," ",\r\n", " " -- "\r\n"). + // separator -- это и есть то, что стоит между словами на одной строке + // (", " для списка, " " для обычного переноса по словам). На переносе + // строки хвостовые пробелы у разделителя срезаем, чтобы не оставлять их + // перед \r\n (для "," получаем ",\r\n", для " " -- просто "\r\n"). std::string eol_separator = separator; while (!eol_separator.empty() && eol_separator.back() == ' ') { eol_separator.pop_back(); } for (const auto &word : words) { - // -- (&R, &n ..) - // , + // ширину считаем по видимой длине -- цветокоды (&R, &n и т.п.) на экране + // места не занимают, иначе строки с цветом переносятся раньше времени const size_t word_len = GetStringWithoutColors(word).size(); if (!first) { if (line_length + separator.size() + word_len > max_length) { @@ -1039,17 +1039,17 @@ std::string utils::WrapText(const std::string &text, size_t max_length) { std::istringstream stream(text); std::string line; bool first = true; - // '\n', -- - // () + // разбиваем по '\n', каждую строку переносим отдельно -- авторские + // переносы и пустые строки (абзацы) сохраняются while (std::getline(stream, line)) { - if (!line.empty() && line.back() == '\r') { // '\r' \r\n + if (!line.empty() && line.back() == '\r') { // срезаем хвостовой '\r' от \r\n line.pop_back(); } if (!first) { result += "\r\n"; } first = false; - // max_length == 0 -- ; Σ + // max_length == 0 -- без переноса; иначе пустая строка вернётся пустой result += (max_length == 0) ? line : OutWordsList(line, max_length, " "); } return result; @@ -1070,21 +1070,21 @@ std::string sprintGender(int gender_value) { // ---- name/file sanitization + number formatting (moved from interpreter.cpp, // issue.interpreter-cleaning Bucket 4) ---- -// name ( ) +// замена в name русских символов на англ в нижнем регистре (для файлов) void CreateFileName(std::string &name) { - // KOI8-R -- , . - // , : - // UTF-8 AtoL ţ , - // 105 70 , (issue #3681). + // Имя файла собирается по байтам KOI8-R -- ровно так, как собиралось до миграции. Иначе у + // уже существующего персонажа доска получит другое имя файла, и старая просто потеряется: + // под UTF-8 байтовый AtoL резал русскую букву пополам и оставлял её в имени как есть, отчего + // рядом со 105 старыми досками завелось 70 новых, с кириллицей в имени (issue #3681). // - // KOI8-R : KOI8-R to_koi8 -- - // , . + // Приводим строку к KOI8-R и дальше работаем прежним байтовым кодом: под KOI8-R to_koi8 -- + // тождество, так что поведение там не меняется ни на байт. name = native_text::to_koi8(name); for (unsigned i = 0; i != name.length(); ++i) name[i] = LOWER(codepages::AtoL(name[i])); } -// +// вывод экспы аля диабла std::string ExpFormat(long long exp) { std::string prefix; if (exp < 0) { @@ -1094,20 +1094,20 @@ std::string ExpFormat(long long exp) { if (exp < 1000000) return (prefix + fmt::format("{}", exp)); else if (exp < 1000000000) - return (prefix + fmt::format("{}", exp / 1000) + " "); + return (prefix + fmt::format("{}", exp / 1000) + " тыс"); else if (exp < 1000000000000LL) - return (prefix + fmt::format("{}", exp / 1000000) + " "); + return (prefix + fmt::format("{}", exp / 1000000) + " млн"); else - return (prefix + fmt::format("{}", exp / 1000000000LL) + " "); + return (prefix + fmt::format("{}", exp / 1000000000LL) + " млрд"); } -// * + ( ) +// * Конвертация имени в нижний регистр + первый сивмол в верхний (для единообразного поиска в контейнерах) void name_convert(std::string &text) { if (!text.empty()) { utils::ConvertToLow(text); - // , : UTF-8 , - // UPPER -- " " - // (issue #3681). + // Первую букву поднимаем целиком, а не первый байт: под UTF-8 у русской буквы их два, + // и байтовый UPPER превращал имя в мусор -- "имя щщщщщ одобрить" не находило + // персонажа (issue #3681). native_text::capitalize_first(text); } } From 2b2a4ff2c1d17351b2c27e2079c58dc99fe88d0d Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 15:23:33 +0200 Subject: [PATCH 060/110] =?UTF-8?q?refactor(utf8):=20=D0=B2=D1=8B=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD=D1=83=D1=82=D1=8C=20=D1=82=D0=BE=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=91=D1=80=D1=82=D0=BA=D0=B8=20=D0=B8=20=D0=BC=D1=91=D1=80?= =?UTF-8?q?=D1=82=D0=B2=D1=8B=D0=B9=20pred=5Fseparator=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Koi8rToUtf8 и EngineStringToUtf8 стали тождественными, когда движок перешёл на UTF-8: переводить нечего, потребитель и так ждёт UTF-8. Обёртки с именами, которые врут о том, что делают, убраны вместе со всеми 69 вызовами. Заодно удалён pred_separator из utils.h -- мёртвый код, пользователей вне заголовка не было; с ним ушёл и enum separator_mode, существовавший только ради него. --- .../network/admin_api/crud_handlers.cpp | 45 ++++++------- src/engine/network/admin_api/json_helpers.cpp | 19 ------ src/engine/network/admin_api/json_helpers.h | 2 - src/engine/network/admin_api/serializers.cpp | 64 +++++++++--------- src/engine/observability/event_sink.cpp | 6 -- src/engine/observability/event_sink.h | 5 -- src/gameplay/affects/affect_data.cpp | 4 +- src/gameplay/fight/fight_hit.cpp | 4 +- src/gameplay/mechanics/damage.cpp | 8 +-- src/simulator/scenario_runner.cpp | 28 ++++---- src/utils/utils.h | 66 ------------------- 11 files changed, 75 insertions(+), 176 deletions(-) diff --git a/src/engine/network/admin_api/crud_handlers.cpp b/src/engine/network/admin_api/crud_handlers.cpp index be279396e..7f8c13cb5 100644 --- a/src/engine/network/admin_api/crud_handlers.cpp +++ b/src/engine/network/admin_api/crud_handlers.cpp @@ -180,11 +180,11 @@ void HandleListZones(DescriptorData* d) const ZoneData &zone = zone_table[zrn]; json zone_obj; zone_obj["vnum"] = zone.vnum; - zone_obj["name"] = admin_api::json::Koi8rToUtf8(zone.name); + zone_obj["name"] = zone.name; zone_obj["level"] = zone.level; if (!zone.author.empty()) { - zone_obj["author"] = admin_api::json::Koi8rToUtf8(zone.author); + zone_obj["author"] = zone.author; } response["zones"].push_back(zone_obj); } @@ -275,9 +275,9 @@ void HandleListMobs(DescriptorData* d, const char* zone_vnum_str) json mob_data; mob_data["vnum"] = mob_vnum; - mob_data["name"] = admin_api::json::Koi8rToUtf8(mob_proto[i].player_data.PNames[grammar::ECase::kNom]); - mob_data["aliases"] = admin_api::json::Koi8rToUtf8(mob_proto[i].get_npc_name()); - mob_data["short_desc"] = admin_api::json::Koi8rToUtf8(mob_proto[i].player_data.long_descr); + mob_data["name"] = mob_proto[i].player_data.PNames[grammar::ECase::kNom]; + mob_data["aliases"] = mob_proto[i].get_npc_name(); + mob_data["short_desc"] = mob_proto[i].player_data.long_descr; mob_data["level"] = mob_proto[i].GetLevel(); response["mobs"].push_back(mob_data); @@ -430,9 +430,9 @@ void HandleListObjects(DescriptorData* d, const char* zone_vnum_str) json obj_data; obj_data["vnum"] = obj->get_vnum(); - obj_data["name"] = admin_api::json::Koi8rToUtf8(obj->get_short_description()); - obj_data["aliases"] = admin_api::json::Koi8rToUtf8(obj->get_aliases()); - obj_data["short_desc"] = admin_api::json::Koi8rToUtf8(obj->get_short_description()); + obj_data["name"] = obj->get_short_description(); + obj_data["aliases"] = obj->get_aliases(); + obj_data["short_desc"] = obj->get_short_description(); obj_data["type"] = static_cast(obj->get_type()); response["objects"].push_back(obj_data); @@ -657,7 +657,7 @@ void HandleListRooms(DescriptorData* d, const char* zone_vnum_str) json room_obj; room_obj["vnum"] = world[rnum]->vnum; - room_obj["name"] = admin_api::json::Koi8rToUtf8(world[rnum]->name); + room_obj["name"] = world[rnum]->name; response["rooms"].push_back(room_obj); } @@ -876,7 +876,7 @@ void HandleListTriggers(DescriptorData* d, const char* zone_vnum_str) json trig_data; trig_data["vnum"] = trig_vnum; - trig_data["name"] = admin_api::json::Koi8rToUtf8(trig->get_name()); + trig_data["name"] = trig->get_name(); trig_data["attach_type"] = static_cast(trig->get_attach_type()); response["triggers"].push_back(trig_data); @@ -908,7 +908,6 @@ void HandleGetTrigger(DescriptorData* d, int trig_vnum) void HandleUpdateTrigger(DescriptorData* d, int trig_vnum, const char* json_data) { using admin_api::json::Utf8ToKoi8r; - using admin_api::json::Koi8rToUtf8; int rnum = find_trig_rnum(trig_vnum); if (rnum < 0 || !trig_index[rnum] || !trig_index[rnum]->proto) @@ -1000,7 +999,6 @@ void HandleUpdateTrigger(DescriptorData* d, int trig_vnum, const char* json_data void HandleCreateTrigger(DescriptorData* d, int zone_vnum, const char* json_data) { using admin_api::json::Utf8ToKoi8r; - using admin_api::json::Koi8rToUtf8; try { @@ -1110,7 +1108,7 @@ void HandleCreateTrigger(DescriptorData* d, int zone_vnum, const char* json_data std::string olc_output; if (temp_d->output && temp_d->bufptr > 0) { - olc_output = Koi8rToUtf8(std::string(temp_d->output, temp_d->bufptr)); + olc_output = std::string(temp_d->output, temp_d->bufptr); } // Clean up @@ -1197,10 +1195,10 @@ void HandleGetPlayers(DescriptorData* d) } json player; - player["name"] = admin_api::json::Koi8rToUtf8(ch->get_name().c_str()); + player["name"] = ch->get_name().c_str(); player["level"] = ch->GetLevel(); player["remort"] = ch->get_remort(); - player["class"] = admin_api::json::Koi8rToUtf8(MUD::Class(ch->GetClass()).GetName().c_str()); + player["class"] = MUD::Class(ch->GetClass()).GetName().c_str(); player["room"] = GET_ROOM_VNUM(ch->in_room); player["is_immortal"] = (ch->GetLevel() >= kLvlImmortal); @@ -1219,7 +1217,6 @@ void HandleGetPlayers(DescriptorData* d) // Helper: convert rnum args back to vnums for a single command static json SerializeZoneCommand(const reset_com &cmd, int index) { - using admin_api::json::Koi8rToUtf8; json obj; obj["index"] = index; @@ -1233,34 +1230,34 @@ static json SerializeZoneCommand(const reset_com &cmd, int index) obj["max_in_world"] = cmd.arg2; obj["room_vnum"] = world[cmd.arg3]->vnum; obj["max_in_room"] = cmd.arg4; - obj["comment"] = Koi8rToUtf8(mob_proto[cmd.arg1].get_npc_name()); + obj["comment"] = mob_proto[cmd.arg1].get_npc_name(); break; case 'O': obj["obj_vnum"] = obj_proto[cmd.arg1]->get_vnum(); obj["max_in_world"] = cmd.arg2; obj["room_vnum"] = world[cmd.arg3]->vnum; obj["load_percent"] = cmd.arg4; - obj["comment"] = Koi8rToUtf8(obj_proto[cmd.arg1]->get_short_description()); + obj["comment"] = obj_proto[cmd.arg1]->get_short_description(); break; case 'G': obj["obj_vnum"] = obj_proto[cmd.arg1]->get_vnum(); obj["max_in_world"] = cmd.arg2; obj["load_percent"] = cmd.arg4; - obj["comment"] = Koi8rToUtf8(obj_proto[cmd.arg1]->get_short_description()); + obj["comment"] = obj_proto[cmd.arg1]->get_short_description(); break; case 'E': obj["obj_vnum"] = obj_proto[cmd.arg1]->get_vnum(); obj["max_in_world"] = cmd.arg2; obj["eq_position"] = cmd.arg3; obj["load_percent"] = cmd.arg4; - obj["comment"] = Koi8rToUtf8(obj_proto[cmd.arg1]->get_short_description()); + obj["comment"] = obj_proto[cmd.arg1]->get_short_description(); break; case 'P': obj["obj_vnum"] = obj_proto[cmd.arg1]->get_vnum(); obj["max_in_world"] = cmd.arg2; obj["target_obj_vnum"] = obj_proto[cmd.arg3]->get_vnum(); obj["load_percent"] = cmd.arg4; - obj["comment"] = Koi8rToUtf8(obj_proto[cmd.arg1]->get_short_description()); + obj["comment"] = obj_proto[cmd.arg1]->get_short_description(); break; case 'D': obj["room_vnum"] = world[cmd.arg1]->vnum; @@ -1282,8 +1279,8 @@ static json SerializeZoneCommand(const reset_com &cmd, int index) case 'V': obj["trigger_type"] = cmd.arg1; obj["context"] = cmd.arg2; - if (cmd.sarg1) obj["var_name"] = Koi8rToUtf8(cmd.sarg1); - if (cmd.sarg2) obj["var_value"] = Koi8rToUtf8(cmd.sarg2); + if (cmd.sarg1) obj["var_name"] = cmd.sarg1; + if (cmd.sarg2) obj["var_value"] = cmd.sarg2; break; case 'F': obj["room_vnum"] = world[cmd.arg1]->vnum; @@ -1697,7 +1694,7 @@ void HandleResetZone(DescriptorData* d, int zone_vnum) response["status"] = "ok"; response["message"] = "Zone reset successfully"; response["zone_vnum"] = zone_vnum; - response["zone_name"] = admin_api::json::Koi8rToUtf8(zone_table[zrn].name); + response["zone_name"] = zone_table[zrn].name; SendJsonResponse(d, response); } diff --git a/src/engine/network/admin_api/json_helpers.cpp b/src/engine/network/admin_api/json_helpers.cpp index d476baa2e..915924b05 100644 --- a/src/engine/network/admin_api/json_helpers.cpp +++ b/src/engine/network/admin_api/json_helpers.cpp @@ -36,26 +36,7 @@ json SerializeBitvector(Bitvector bits, size_t plane) // String Conversion (KOI8-R <-> UTF-8) // ============================================================================ -std::string Koi8rToUtf8(const char* koi8r) -{ - if (!koi8r) - { - return ""; - } - // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. - // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как - // KOI8-R и удваивала каждую букву (issue #3681). - return std::string(koi8r); -} - -std::string Koi8rToUtf8(const std::string& koi8r) -{ - // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. - // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как - // KOI8-R и удваивала каждую букву (issue #3681). - return koi8r; -} std::string Utf8ToKoi8r(const std::string& utf8) { diff --git a/src/engine/network/admin_api/json_helpers.h b/src/engine/network/admin_api/json_helpers.h index 36f6d85a0..e084c926c 100644 --- a/src/engine/network/admin_api/json_helpers.h +++ b/src/engine/network/admin_api/json_helpers.h @@ -209,8 +209,6 @@ inline std::optional ParseNested(const json& j, const char* key) * * Used when serializing entity data to JSON (which requires UTF-8) */ -std::string Koi8rToUtf8(const char* koi8r); -std::string Koi8rToUtf8(const std::string& koi8r); /** * \brief Convert UTF-8 string to KOI8-R for game data diff --git a/src/engine/network/admin_api/serializers.cpp b/src/engine/network/admin_api/serializers.cpp index 0623aa9c7..ec1b86eb7 100644 --- a/src/engine/network/admin_api/serializers.cpp +++ b/src/engine/network/admin_api/serializers.cpp @@ -29,19 +29,19 @@ json SerializeMob(const CharData& mob, int vnum) // Names (all 6 Russian cases + aliases) json names; - names["aliases"] = Koi8rToUtf8(mob.get_npc_name()); - names["nominative"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kNom]); - names["genitive"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kGen]); - names["dative"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kDat]); - names["accusative"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kAcc]); - names["instrumental"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kIns]); - names["prepositional"] = Koi8rToUtf8(mob.player_data.PNames[grammar::ECase::kPre]); + names["aliases"] = mob.get_npc_name(); + names["nominative"] = mob.player_data.PNames[grammar::ECase::kNom]; + names["genitive"] = mob.player_data.PNames[grammar::ECase::kGen]; + names["dative"] = mob.player_data.PNames[grammar::ECase::kDat]; + names["accusative"] = mob.player_data.PNames[grammar::ECase::kAcc]; + names["instrumental"] = mob.player_data.PNames[grammar::ECase::kIns]; + names["prepositional"] = mob.player_data.PNames[grammar::ECase::kPre]; mob_obj["names"] = names; // Descriptions json descriptions; - descriptions["short_desc"] = Koi8rToUtf8(mob.player_data.long_descr); - descriptions["long_desc"] = Koi8rToUtf8(mob.player_data.description); + descriptions["short_desc"] = mob.player_data.long_descr; + descriptions["long_desc"] = mob.player_data.description; mob_obj["descriptions"] = descriptions; // Stats (level, HP, damage, etc.) @@ -190,10 +190,10 @@ json SerializeObject(const CObjectPrototype& obj, int vnum) { json obj_data; obj_data["vnum"] = vnum; - obj_data["aliases"] = Koi8rToUtf8(obj.get_aliases()); - obj_data["short_desc"] = Koi8rToUtf8(obj.get_short_description()); - obj_data["description"] = Koi8rToUtf8(obj.get_description()); - obj_data["action_desc"] = Koi8rToUtf8(obj.get_action_description()); + obj_data["aliases"] = obj.get_aliases(); + obj_data["short_desc"] = obj.get_short_description(); + obj_data["description"] = obj.get_description(); + obj_data["action_desc"] = obj.get_action_description(); obj_data["type"] = static_cast(obj.get_type()); obj_data["spec_param"] = obj.get_spec_param(); // symmetry: ParseObjectUpdate reads it @@ -241,7 +241,7 @@ json SerializeObject(const CObjectPrototype& obj, int vnum) case grammar::ECase::kPre: case_name = "prepositional"; break; default: continue; } - names[case_name] = Koi8rToUtf8(obj.get_PName(ecase)); + names[case_name] = obj.get_PName(ecase); } obj_data["names"] = names; @@ -284,8 +284,8 @@ json SerializeObject(const CObjectPrototype& obj, int vnum) for (const auto &ed : obj.get_ex_description()) { json extra; - extra["keywords"] = Koi8rToUtf8(ed.keyword); - extra["description"] = Koi8rToUtf8(ed.description); + extra["keywords"] = ed.keyword; + extra["description"] = ed.description; extra_descs.push_back(extra); } obj_data["extra_descriptions"] = extra_descs; @@ -309,7 +309,7 @@ json SerializeRoom(RoomData& room, int vnum) { json room_data; room_data["vnum"] = vnum; - room_data["name"] = Koi8rToUtf8(room.name); + room_data["name"] = room.name; // Description: temp_description holds a freshly-edited (unsaved) value; the // persisted description lives in the shared pool by description_num. Reading @@ -317,11 +317,11 @@ json SerializeRoom(RoomData& room, int vnum) // (issue #3401) -- fall back to the pooled text. if (room.temp_description) { - room_data["description"] = Koi8rToUtf8(room.temp_description); + room_data["description"] = room.temp_description; } else if (room.description_num > 0) { - room_data["description"] = Koi8rToUtf8(GlobalObjects::descriptions().get(room.description_num).c_str()); + room_data["description"] = GlobalObjects::descriptions().get(room.description_num).c_str(); } room_data["sector_type"] = static_cast(room.sector_type); @@ -347,11 +347,11 @@ json SerializeRoom(RoomData& room, int vnum) world[room.dir_option[dir]->to_room()]->vnum : -1; if (!room.dir_option[dir]->general_description.empty()) { - exit_obj["description"] = Koi8rToUtf8(room.dir_option[dir]->general_description); + exit_obj["description"] = room.dir_option[dir]->general_description; } if (room.dir_option[dir]->keyword) { - exit_obj["keyword"] = Koi8rToUtf8(room.dir_option[dir]->keyword); + exit_obj["keyword"] = room.dir_option[dir]->keyword; } // exit_info used to be a byte: a single 30-bit plane serialized as a plain number. // BitsetFlags keeps that flag identity, so the JSON stays a number (get_plane(0)). @@ -369,11 +369,11 @@ json SerializeRoom(RoomData& room, int vnum) json ed_obj; if (!ed.keyword.empty()) { - ed_obj["keyword"] = Koi8rToUtf8(ed.keyword); + ed_obj["keyword"] = ed.keyword; } if (!ed.description.empty()) { - ed_obj["description"] = Koi8rToUtf8(ed.description); + ed_obj["description"] = ed.description; } extra_descrs.push_back(ed_obj); } @@ -401,19 +401,19 @@ json SerializeZoneData(const ZoneData& zone, int vnum) { json zone_data; zone_data["vnum"] = vnum; - zone_data["name"] = Koi8rToUtf8(zone.name); + zone_data["name"] = zone.name; if (!zone.comment.empty()) { - zone_data["comment"] = Koi8rToUtf8(zone.comment); + zone_data["comment"] = zone.comment; } if (!zone.author.empty()) { - zone_data["author"] = Koi8rToUtf8(zone.author); + zone_data["author"] = zone.author; } if (!zone.location.empty()) { - zone_data["location"] = Koi8rToUtf8(zone.location); + zone_data["location"] = zone.location; } if (!zone.description.empty()) { - zone_data["description"] = Koi8rToUtf8(zone.description); + zone_data["description"] = zone.description; } zone_data["level"] = zone.level; @@ -453,7 +453,7 @@ json SerializeTrigger(const Trigger& trig, int vnum) { json trig_data; trig_data["vnum"] = vnum; - trig_data["name"] = Koi8rToUtf8(trig.get_name()); + trig_data["name"] = trig.get_name(); trig_data["attach_type"] = static_cast(trig.get_attach_type()); trig_data["trigger_type"] = trig.get_trigger_type(); trig_data["narg"] = trig.narg; @@ -463,12 +463,12 @@ json SerializeTrigger(const Trigger& trig, int vnum) // Argument if (!trig.arglist.empty()) { - trig_data["arglist"] = Koi8rToUtf8(trig.arglist); + trig_data["arglist"] = trig.arglist; } if (trig.get_script_language() == TriggerScriptLanguage::Lua) { - trig_data["script"] = Koi8rToUtf8(trig.get_lua_script_source()); + trig_data["script"] = trig.get_lua_script_source(); } else { @@ -479,7 +479,7 @@ json SerializeTrigger(const Trigger& trig, int vnum) auto cmd = *trig.cmdlist; while (cmd) { - commands.push_back(Koi8rToUtf8(cmd->cmd)); + commands.push_back(cmd->cmd); cmd = cmd->next; } } diff --git a/src/engine/observability/event_sink.cpp b/src/engine/observability/event_sink.cpp index 327460f1d..971a2baac 100644 --- a/src/engine/observability/event_sink.cpp +++ b/src/engine/observability/event_sink.cpp @@ -52,12 +52,6 @@ void FlushAllSinks() { } } -std::string EngineStringToUtf8(const std::string& koi8r) { - // Движок держит текст в UTF-8, потребитель тоже ждёт UTF-8 -- границы здесь больше нет. - // Перекодировка, оставшаяся с байтовых времён, теперь разбирала бы готовый UTF-8 как - // KOI8-R и удваивала каждую букву (issue #3681). - return koi8r; -} } // namespace observability diff --git a/src/engine/observability/event_sink.h b/src/engine/observability/event_sink.h index ebc99f213..2d47949f6 100644 --- a/src/engine/observability/event_sink.h +++ b/src/engine/observability/event_sink.h @@ -51,11 +51,6 @@ bool HasAnyEventSink(); void EmitToAllSinks(const Event& ev); void FlushAllSinks(); -// Engine-side strings (character names, room descriptions, etc.) live in -// KOI8-R; nlohmann::json validates UTF-8 on serialization and rejects KOI8-R -// bytes. Emitters must convert text fields through this helper before putting -// them into Event.attrs. -std::string EngineStringToUtf8(const std::string& koi8r); } // namespace observability diff --git a/src/gameplay/affects/affect_data.cpp b/src/gameplay/affects/affect_data.cpp index 0b6a6641a..e0d99d1b5 100644 --- a/src/gameplay/affects/affect_data.cpp +++ b/src/gameplay/affects/affect_data.cpp @@ -53,8 +53,8 @@ void EmitAffectEvent(const char *kind, const CharData *ch, ev.name = kind; ev.ts_unix_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - ev.attrs["target_name"] = observability::EngineStringToUtf8( - GET_NAME(ch) ? GET_NAME(ch) : ""); + ev.attrs["target_name"] = + GET_NAME(ch) ? GET_NAME(ch) : ""; ev.attrs["duration"] = static_cast(af.duration); ev.attrs["modifier"] = static_cast(af.modifier); ev.attrs["location"] = static_cast(af.location); diff --git a/src/gameplay/fight/fight_hit.cpp b/src/gameplay/fight/fight_hit.cpp index 1698bae9b..d90870a14 100644 --- a/src/gameplay/fight/fight_hit.cpp +++ b/src/gameplay/fight/fight_hit.cpp @@ -883,8 +883,8 @@ void EmitMissEvent(CharData *ch, CharData *victim, const char *reason) { ev.name = "miss"; ev.ts_unix_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - ev.attrs["attacker_name"] = observability::EngineStringToUtf8(GET_NAME(ch) ? GET_NAME(ch) : ""); - ev.attrs["victim_name"] = observability::EngineStringToUtf8(GET_NAME(victim) ? GET_NAME(victim) : ""); + ev.attrs["attacker_name"] = GET_NAME(ch) ? GET_NAME(ch) : ""; + ev.attrs["victim_name"] = GET_NAME(victim) ? GET_NAME(victim) : ""; ev.attrs["reason"] = std::string(reason); observability::EmitToAllSinks(ev); } diff --git a/src/gameplay/mechanics/damage.cpp b/src/gameplay/mechanics/damage.cpp index e1d0afb63..4c34fdd30 100644 --- a/src/gameplay/mechanics/damage.cpp +++ b/src/gameplay/mechanics/damage.cpp @@ -821,8 +821,8 @@ int Damage::Process(CharData *ch, CharData *victim) { ev.name = "damage"; ev.ts_unix_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - ev.attrs["attacker_name"] = observability::EngineStringToUtf8(GET_NAME(ch) ? GET_NAME(ch) : ""); - ev.attrs["victim_name"] = observability::EngineStringToUtf8(GET_NAME(victim) ? GET_NAME(victim) : ""); + ev.attrs["attacker_name"] = GET_NAME(ch) ? GET_NAME(ch) : ""; + ev.attrs["victim_name"] = GET_NAME(victim) ? GET_NAME(victim) : ""; ev.attrs["dam"] = static_cast(dam); ev.attrs["real_dam"] = static_cast(real_dam); ev.attrs["over_dam"] = static_cast(over_dam); @@ -837,9 +837,9 @@ int Damage::Process(CharData *ch, CharData *victim) { // Чармис/поднятая нежить -- атаковал не сам PC, а его подчинённый. // Визуализатору это нужно, чтобы отделить вклад хозяина и слуг. ev.attrs["attacker_is_charmie"] = IsCharmice(ch); - ev.attrs["attacker_master_name"] = observability::EngineStringToUtf8( + ev.attrs["attacker_master_name"] = (IsCharmice(ch) && ch->has_master() && GET_NAME(ch->get_master())) - ? GET_NAME(ch->get_master()) : ""); + ? GET_NAME(ch->get_master()) : ""; observability::EmitToAllSinks(ev); } SendToTC(victim, false, true, true, "&MПолучен урон = %d&n\r\n", dam); diff --git a/src/simulator/scenario_runner.cpp b/src/simulator/scenario_runner.cpp index b458ee696..1a6bc76cb 100644 --- a/src/simulator/scenario_runner.cpp +++ b/src/simulator/scenario_runner.cpp @@ -124,9 +124,9 @@ class HeadlessDescriptor { e.ts_unix_ms = NowUnixMs(); e.attrs["round"] = static_cast(round_no); e.attrs["role"] = std::string(role); - e.attrs["target_name"] = observability::EngineStringToUtf8( - GET_NAME(ch_) ? GET_NAME(ch_) : ""); - e.attrs["text"] = observability::EngineStringToUtf8(text); + e.attrs["target_name"] = + GET_NAME(ch_) ? GET_NAME(ch_) : ""; + e.attrs["text"] = text; observability::EmitToAllSinks(e); } ResetBuffer(); @@ -379,7 +379,7 @@ void AddParticipantAttrs(observability::Event& e, const char* role, const Partic using T = std::decay_t; if constexpr (std::is_same_v) { e.attrs[std::string(role) + "_type"] = std::string("player"); - e.attrs[std::string(role) + "_class"] = observability::EngineStringToUtf8(s.class_name); + e.attrs[std::string(role) + "_class"] = s.class_name; e.attrs[std::string(role) + "_level"] = static_cast(s.level); } else if constexpr (std::is_same_v) { e.attrs[std::string(role) + "_type"] = std::string("mob"); @@ -399,16 +399,16 @@ void EmitCharState(const char* role, e.ts_unix_ms = NowUnixMs(); e.attrs["round"] = static_cast(round_no); e.attrs["role"] = std::string(role); - e.attrs["target_name"] = observability::EngineStringToUtf8( - GET_NAME(ch) ? GET_NAME(ch) : ""); + e.attrs["target_name"] = + GET_NAME(ch) ? GET_NAME(ch) : ""; // Identity: PC vs NPC + display fields. У PC имя задано simulator'ом // ('attacker' / 'victim'), уровень и класс полезны для контекста. // У моба важен vnum + short_descr (полное имя из прототипа). e.attrs["is_npc"] = ch->IsNpc(); if (ch->IsNpc()) { e.attrs["vnum"] = static_cast(GET_MOB_VNUM(ch)); - e.attrs["short_descr"] = observability::EngineStringToUtf8( - ch->get_npc_name()); + e.attrs["short_descr"] = + ch->get_npc_name(); } else { e.attrs["vnum"] = static_cast(-1); e.attrs["short_descr"] = std::string(); @@ -417,8 +417,8 @@ void EmitCharState(const char* role, // Реморт PC -- часть "уровня прокачки", влияет на выданные скиллы // и фиты. Веб-UI показывает рядом с уровнем. e.attrs["remort"] = static_cast(ch->IsNpc() ? 0 : ch->get_remort()); - e.attrs["class_name"] = observability::EngineStringToUtf8( - ch->IsNpc() ? std::string() : MUD::Class(ch->GetClass()).GetName()); + e.attrs["class_name"] = + ch->IsNpc() ? std::string() : MUD::Class(ch->GetClass()).GetName(); e.attrs["hp"] = static_cast(ch->get_hit()); e.attrs["max_hp"] = static_cast(ch->get_max_hit()); e.attrs["move"] = static_cast(ch->get_move()); @@ -467,7 +467,7 @@ void EmitCharState(const char* role, feats_list += feat.GetName(); } } - e.attrs["feats_list"] = observability::EngineStringToUtf8(feats_list); + e.attrs["feats_list"] = feats_list; // Список активных аффектов с их типом (spell name) и оставшейся // длительностью; '|'-разделитель -- web-UI парсит и рисует chip'ами. std::string aff_list; @@ -479,7 +479,7 @@ void EmitCharState(const char* role, aff_list += fmt::format(" ({}t)", a->duration); } e.attrs["affects_count"] = static_cast(aff_count); - e.attrs["affects_list"] = observability::EngineStringToUtf8(aff_list); + e.attrs["affects_list"] = aff_list; // Список одетых предметов: 'slot:vnum:name', разделители '|'. Веб-UI // рендерит их в state-панели чтобы было видно, чем именно бьётся / // защищается персонаж. @@ -498,7 +498,7 @@ void EmitCharState(const char* role, obj->get_vnum(), obj->get_short_description()); } - e.attrs["equip_list"] = observability::EngineStringToUtf8(equip_list); + e.attrs["equip_list"] = equip_list; e.attrs["aff_silence"] = AFF_FLAGGED(ch, EAffect::kSilence) ? true : false; e.attrs["aff_charmed"] = AFF_FLAGGED(ch, EAffect::kCharmed) ? true : false; e.attrs["aff_sleep"] = AFF_FLAGGED(ch, EAffect::kSleep) ? true : false; @@ -517,7 +517,7 @@ void EmitCharState(const char* role, // FlagData::sprintbits пишет nothing_string ("ничего") когда // ни одного флага не выставлено -- для UI пустая строка лучше. if (s == "ничего") s.clear(); - e.attrs["flags_list"] = observability::EngineStringToUtf8(s); + e.attrs["flags_list"] = s; } observability::EmitToAllSinks(e); } diff --git a/src/utils/utils.h b/src/utils/utils.h index 711a63beb..4ed9ed35f 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -533,72 +533,6 @@ inline char a_lcc(const unsigned char c) { return a_lcc_table[c]; } -enum separator_mode { - A_ISSPACE, - A_ISASCII, - A_ISPRINT, - A_ISLOWER, - A_ISUPPER, - A_ISDIGIT, - A_ISALPHA, - A_ISALNUM, - A_ISXDIGIT -}; - -class pred_separator { - bool (*pred)(unsigned char); - bool l_not; - public: - explicit - pred_separator(separator_mode _mode, bool _l_not = false) : l_not(_l_not) { - switch (_mode) { - case A_ISSPACE: pred = a_isspace; - break; - case A_ISASCII: pred = a_isascii; - break; - case A_ISPRINT: pred = a_isprint; - break; - case A_ISLOWER: pred = a_islower; - break; - case A_ISUPPER: pred = a_isupper; - break; - case A_ISDIGIT: pred = a_isdigit; - break; - case A_ISALPHA: pred = a_isalpha; - break; - case A_ISALNUM: pred = a_isalnum; - break; - case A_ISXDIGIT: pred = a_isxdigit; - break; - } - } - - explicit - pred_separator() : pred(a_isspace), l_not(false) {} - - void reset() {} - - bool operator()(std::string::const_iterator &next, std::string::const_iterator end, std::string &tok) { - tok = std::string(); - - if (l_not) - for (; next != end && !pred(*next); ++next) {} - else - for (; next != end && pred(*next); ++next) {} - - if (next == end) - return false; - - if (l_not) - for (; next != end && pred(*next); ++next) - tok += *next; - else - for (; next != end && !pred(*next); ++next) - tok += *next; - - return true; - } -}; // ВЕЯРМН ЯЙНОХОЮЯРЕМН template From b4b89032c0ad164f5bba45c0b1a5b5c226b98707 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 18:40:10 +0200 Subject: [PATCH 061/110] =?UTF-8?q?revert(utf8):=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D0=B2=D0=BE=D1=8E=20=D1=88=D0=B0?= =?UTF-8?q?=D0=BF=D0=BA=D1=83=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B0=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Своя шапка (lib/greeting.utf8 + отдельный путь чтения в login.cpp) не нужна: экран входа снова берётся из system_msg.xml, как и всё остальное. Убраны файл, макрос GREETING_UTF8_FILE, функция GetGreeting со своим чтением и нормализацией переводов строк, строка в .gitattributes и включения, которые были нужны только ей. --- .gitattributes | 1 - lib/greeting.utf8 | 22 --------------- src/engine/boot/boot_constants.h | 4 --- src/engine/ui/login.cpp | 48 +------------------------------- src/utils/native_text.h | 3 +- 5 files changed, 2 insertions(+), 76 deletions(-) delete mode 100644 lib/greeting.utf8 diff --git a/.gitattributes b/.gitattributes index 843cec6ec..a7cea6554 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,3 @@ /lib/cfg/** working-tree-encoding=KOI8-R eol=lf /bin.win/** working-tree-encoding=windows-1251 eol=crlf *.dll -diff -working-tree-encoding -/lib/greeting.utf8 working-tree-encoding=UTF-8 eol=lf diff --git a/lib/greeting.utf8 b/lib/greeting.utf8 deleted file mode 100644 index 77de9b982..000000000 --- a/lib/greeting.utf8 +++ /dev/null @@ -1,22 +0,0 @@ - - ┌────────────────────────────────────────────────────────┐ - │ 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_вики - -Введите имя персонажа (или «новый», чтобы создать нового): \ No newline at end of file diff --git a/src/engine/boot/boot_constants.h b/src/engine/boot/boot_constants.h index c5ad8280d..69873f5e3 100644 --- a/src/engine/boot/boot_constants.h +++ b/src/engine/boot/boot_constants.h @@ -108,10 +108,6 @@ enum SetStuffMode { #define PLAYER_Z_PREFIX LIB_PLRS "" LIB_Z #define HELP_PAGE_FILE LIB_HELP "screen" // for HELP -// Нарядный экран входа, уходит клиенту байт в байт (issue #3681). Необязателен: если файла нет -// или в нём битый UTF-8 -- берётся обычная шапка из system_msg.xml. Лежит в корне lib: каталог -// text/ в мастере разобран, а этот файл ни к справке, ни к конфигам не относится. -#define GREETING_UTF8_FILE "greeting.utf8" // issue.daily-quest: daily_quest.xml перенесён в cfg/quests и грузится через CfgManager. diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index b05f94091..fa99e521c 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -6,10 +6,8 @@ #include "interpreter.h" #include "utils/russian_keys.h" #include "utils/native_text.h" -#include "utils/utf8.h" #include "engine/boot/boot_constants.h" -#include #include #include "engine/ui/system_messages.h" #include "engine/core/config.h" @@ -1542,50 +1540,6 @@ static void HandleInit(DescriptorData *d, char * /*argument*/) { return; } -// Экран приветствия. Файл lib/text/greeting.utf8 всегда в UTF-8 и написан полной палитрой: -// скруглённая рамка, типографское тире, кавычки-лапки. Он один на все кодировки -- клиенту в -// alt/win/koi8 (и всему KOI8-R-сборке целиком) то же самое доезжает уже приведённым к KOI8-R, -// где скруглённые углы становятся обычными, тире -- дефисом, лапки -- палочками (issue #3681). -// -// Файл необязателен: нет его или в нём битый UTF-8 -- берётся прежняя шапка из system_msg.xml. -static const std::string &GetGreeting() { - static std::string greeting; - static bool loaded = false; - if (!loaded) { - loaded = true; - std::ifstream in(GREETING_UTF8_FILE, std::ios::binary); - if (in) { - std::ostringstream body; - body << in.rdbuf(); - const std::string raw = body.str(); - if (utf8::is_valid(raw)) { - // В файле переводы строк обычные, а выводу нужен CRLF -- ровно та же - // нормализация, что делает загрузчик system_msg.xml. - std::string crlf; - crlf.reserve(raw.size() + raw.size() / 32); - for (const char c : raw) { - if (c == '\r') { - continue; - } - if (c == '\n') { - crlf += "\r\n"; - } else { - crlf += c; - } - } - greeting = native_text::from_utf8(crlf); - } else { - log("SYSERR: %s is not valid UTF-8, falling back to the plain greeting", - GREETING_UTF8_FILE); - } - } - } - if (!greeting.empty()) { - return greeting; - } - return system_messages::GetText(system_messages::ESystemMsg::kGreetings); -} - static void HandleGetKeytable(DescriptorData *d, char *argument) { if (strlen(argument) > 0) argument[0] = argument[strlen(argument) - 1]; @@ -1599,7 +1553,7 @@ static void HandleGetKeytable(DescriptorData *d, char *argument) { } d->keytable = (ubyte) *argument - (ubyte) '0'; ip_log(d->host); - iosystem::write_to_output(GetGreeting().c_str(), d); + iosystem::write_to_output(system_messages::GetText(system_messages::ESystemMsg::kGreetings).c_str(), d); d->state = EConState::kGetName; return; } diff --git a/src/utils/native_text.h b/src/utils/native_text.h index f8366684d..91f754588 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -199,8 +199,7 @@ std::string from_disk_text(const std::string &text); // Read and write MUST stay symmetric. If the engine writes the native encoding while the rest of // the world is KOI8-R, then the first save quietly converts every file it touches, rolling back // to a KOI8-R build stops being possible, and the world is no longer the world we started with. -// The files that are deliberately UTF-8 (lib/text/greeting.utf8) are read raw and never go -// through here (issue #3681). +// (issue #3681). std::string to_disk(const std::string &text); // Pad `s` on the right with spaces to `width` CHARACTERS. The replacement for printf's "%-Ns" From 40808b2d249946ce90b5bea7730a31b9bf41800d Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 19:02:50 +0200 Subject: [PATCH 062/110] =?UTF-8?q?fix(utf8):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D1=88=D0=B8?= =?UTF-8?q?=D0=B5=D1=81=D1=8F=20=D0=BF=D0=BE=D0=B4=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Граница записи стояла на сейвах, ренте, досках, индексах и конфигах, но мимо неё писали дружины, персональные хранилища, обменник, посылки, слава и титулы. Под UTF-8 эти файлы уходили бы на диск в нативной кодировке, а весь остальной мир остаётся KOI8-R -- то есть требование "внешние файлы не меняются" держалось не везде, и откат на прежнюю сборку сломал бы названия дружин, титулы, имена вещей в хранилищах и лотах. Проведены: файл и объявление дружины, сундук и ингредиентный сундук, опыт, pk-лог, история и лог сундука, персональные хранилища (включая запись о неудалённом предмете), база обменника, посылки, список славы, список титулов. Пересчёта охвата: писателей текста, идущих на диск мимо to_disk, не осталось. --- src/gameplay/clans/chest_saver.cpp | 7 +++++- src/gameplay/clans/house.cpp | 17 +++++++++++--- src/gameplay/clans/house_exp.cpp | 31 +++++++++++++++++++++---- src/gameplay/clans/ingr_chest_saver.cpp | 7 +++++- src/gameplay/communication/parcel.cpp | 6 ++++- src/gameplay/economics/exchange.cpp | 6 ++++- src/gameplay/mechanics/depot.cpp | 18 +++++++++++--- src/gameplay/mechanics/glory.cpp | 6 ++++- src/gameplay/mechanics/title.cpp | 10 ++++++-- 9 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/gameplay/clans/chest_saver.cpp b/src/gameplay/clans/chest_saver.cpp index 77d977392..d8261ff2f 100644 --- a/src/gameplay/clans/chest_saver.cpp +++ b/src/gameplay/clans/chest_saver.cpp @@ -1,5 +1,6 @@ // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "chest_saver.h" #include "house.h" @@ -52,7 +53,11 @@ bool save_one_clan_chest(ObjData *chest, const std::string &filename) { } const auto written = out.tellp(); const auto contents = out.str(); - file.write(contents.data(), static_cast(written)); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(contents.substr(0, static_cast(written))); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); return true; } diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index f97b2ff2c..2e52fa32a 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -784,7 +784,11 @@ bool write_if_changed(const std::string &filename, const std::string &contents, log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return false; } - file.write(contents.data(), static_cast(contents.size())); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(contents); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); cache = contents; @@ -2457,7 +2461,11 @@ void Clan::save_chest() { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); break; } @@ -2610,7 +2618,10 @@ void Clan::write_mod(const std::string &arg) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } - file << arg; + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + file << native_text::to_disk(arg); file.close(); mod_text = arg; diff --git a/src/gameplay/clans/house_exp.cpp b/src/gameplay/clans/house_exp.cpp index a50b0e0bd..1037daa1e 100644 --- a/src/gameplay/clans/house_exp.cpp +++ b/src/gameplay/clans/house_exp.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2009 Krodo // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "house_exp.h" #include "utils/grammar/gender.h" @@ -55,7 +56,11 @@ void ClanExp::save(const std::string &abbrev) const { for (ExpListType::const_iterator it = list_.begin(); it != list_.end(); ++it) { out << *it << "\n"; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); } // * Загрузка списка экспы и буффера конкретного клана (по аббревиатуре). @@ -146,9 +151,15 @@ void ClanPkLog::save(const std::string &abbrev) { return; } + std::ostringstream out; for (std::list::const_iterator i = pk_log.begin(); i != pk_log.end(); ++i) { - file << *i; + out << *i; } + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); need_save = false; @@ -252,9 +263,15 @@ void ClanExpHistory::save(const std::string &abbrev) const { return; } + std::ostringstream out; for (HistoryExpListType::const_iterator i = list_.begin(); i != list_.end(); ++i) { - file << i->first << " " << i->second << "\n"; + out << i->first << " " << i->second << "\n"; } + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } @@ -376,10 +393,16 @@ void ClanChestLog::save(const std::string &abbrev) { return; } + std::ostringstream out; for (std::list::const_iterator i = chest_log_.begin(), iend = chest_log_.end(); i != iend; ++i) { - file << *i; + out << *i; } + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); need_save_ = false; diff --git a/src/gameplay/clans/ingr_chest_saver.cpp b/src/gameplay/clans/ingr_chest_saver.cpp index fce8be52e..d7fd6eb82 100644 --- a/src/gameplay/clans/ingr_chest_saver.cpp +++ b/src/gameplay/clans/ingr_chest_saver.cpp @@ -1,5 +1,6 @@ // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "ingr_chest_saver.h" #include "house.h" @@ -57,7 +58,11 @@ bool save_one_chest(ObjData *chest, const std::string &filename) { // реально заполненную часть по tellp(). const auto written = out.tellp(); const auto contents = out.str(); - file.write(contents.data(), static_cast(written)); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(contents.substr(0, static_cast(written))); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); return true; } diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index a1834d206..1c4599710 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -725,7 +725,11 @@ void save() { log("SYSERR: error opening file: %s! (%s %s %d)", FILE_NAME, __FILE__, __func__, __LINE__); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); return; diff --git a/src/gameplay/economics/exchange.cpp b/src/gameplay/economics/exchange.cpp index 3582cb57b..a52f8360f 100644 --- a/src/gameplay/economics/exchange.cpp +++ b/src/gameplay/economics/exchange.cpp @@ -1049,7 +1049,11 @@ void exchange_database_save(bool backup) { mudlog(buf, BRF, kLvlImmortal, SYSLOG, true); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); log("Exchange: done saving database."); diff --git a/src/gameplay/mechanics/depot.cpp b/src/gameplay/mechanics/depot.cpp index c58e031da..5b4bbca04 100644 --- a/src/gameplay/mechanics/depot.cpp +++ b/src/gameplay/mechanics/depot.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2007 Krodo // Part of Bylins http://www.mud.ru +#include "utils/native_text.h" #include "depot.h" #include "engine/db/player_index.h" #include "administration/privilege.h" @@ -272,7 +273,10 @@ void add_purged_message(long uid, int obj_vnum, unsigned int obj_uid) { log("Хранилище: error open file: %s! (%s %s %d)", name.c_str(), __FILE__, __func__, __LINE__); return; } - file << generate_purged_text(uid, obj_vnum, obj_uid); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + file << native_text::to_disk(generate_purged_text(uid, obj_vnum, obj_uid)); } void delete_purged_entry(long uid) { @@ -546,7 +550,11 @@ void save_timedata() { log("Хранилище: error open file: %s! (%s %s %d)", depot_file, __FILE__, __func__, __LINE__); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } @@ -586,7 +594,11 @@ void write_obj_file(const std::string &name, int file_type, const ObjListType &c log("Хранилище: error open file: %s! (%s %s %d)", filename, __FILE__, __func__, __LINE__); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } diff --git a/src/gameplay/mechanics/glory.cpp b/src/gameplay/mechanics/glory.cpp index 7bae3338d..22aa48567 100644 --- a/src/gameplay/mechanics/glory.cpp +++ b/src/gameplay/mechanics/glory.cpp @@ -283,7 +283,11 @@ void save_glory() { log("Glory: не удалось открыть файл на запись: %s", glory_file); return; } - file << out.rdbuf(); + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } diff --git a/src/gameplay/mechanics/title.cpp b/src/gameplay/mechanics/title.cpp index 16704b143..936b94d2a 100644 --- a/src/gameplay/mechanics/title.cpp +++ b/src/gameplay/mechanics/title.cpp @@ -413,9 +413,15 @@ void TitleSystem::save_title_list() { log("Error open file: %s! (%s %s %d)", title_file.c_str(), __FILE__, __func__, __LINE__); return; } + std::ostringstream out; for (TitleListType::const_iterator it = title_list.begin(); it != title_list.end(); ++it) - file << it->first << " " << it->second->unique << "\n" << it->second->pre_title << "\n" << it->second->title - << "\n"; + out << it->first << " " << it->second->unique << "\n" << it->second->pre_title << "\n" << it->second->title + << "\n"; + // Граница записи: на диск уходит кодировка мира (сейчас KOI8-R), зеркально + // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на + // прежнюю сборку становится невозможен (issue #3681). + const std::string on_disk = native_text::to_disk(out.str()); + file.write(on_disk.data(), static_cast(on_disk.size())); file.close(); } From 1b5f90065f858ed12e2c354c32e02e71cd7b2c17 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 19:25:14 +0200 Subject: [PATCH 063/110] =?UTF-8?q?fix(utf8):=20=D1=82=D0=B8=D1=82=D1=83?= =?UTF-8?q?=D0=BB=20=D1=81=20=D0=BA=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D0=B5=D0=B9=20=D0=BE=D1=82=D0=B2=D0=B5=D1=80=D0=B3=D0=B0?= =?UTF-8?q?=D0=BB=D1=81=D1=8F=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=BC=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_alphabet ходил по байтам с условием "байт >= 192", то есть кириллица KOI8-R. Под UTF-8 у русской буквы два байта, и хвостовой (0x80..0xBF) попадал в "меньше 192" -- любой русский титул отвергался на второй позиции: "Недопустимый символ в позиции 2". Проверка переведена на символы. Набор допустимых букв оставлен ровно прежним: 0xC0..0xFF в KOI8-R -- это а-я и А-Я без "ё", поэтому "ё" по-прежнему допустима только если перечислена в allowed. Найдено вживую на боевом мире при попытке поставить титул. --- src/gameplay/mechanics/title.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/gameplay/mechanics/title.cpp b/src/gameplay/mechanics/title.cpp index 936b94d2a..00b3b9c79 100644 --- a/src/gameplay/mechanics/title.cpp +++ b/src/gameplay/mechanics/title.cpp @@ -260,15 +260,23 @@ bool TitleSystem::check_pre_title(const std::string& text, CharData *ch) { * \return 0 не сканало, 1 сканало */ bool TitleSystem::check_alphabet(const std::string &text, CharData *ch, const std::string &allowed) { - int i = 0; - std::string::size_type idx; - for (std::string::const_iterator it = text.begin(); it != text.end(); ++it, ++i) { - unsigned char c = static_cast(*it); - idx = allowed.find(*it); - if (c < 192 && idx == std::string::npos) { - SendMsgToChar(ch, "Недопустимый символ '%c' в позиции %d.\r\n", *it, ++i); - return false; + // Проверяем по символам, а не по байтам. Раньше условие было "байт >= 192", то есть + // кириллица KOI8-R; под UTF-8 у русской буквы два байта, и хвостовой (0x80..0xBF) попадал + // в "меньше 192" -- титул с кириллицей отвергался на второй позиции (issue #3681). + // + // Набор букв оставлен ровно прежним: диапазон 0xC0..0xFF в KOI8-R -- это а-я и А-Я без "ё", + // поэтому "ё" по-прежнему допустима только если перечислена в allowed. + int position = 0; + for (const std::string_view symbol : native_text::chars(text)) { + ++position; + const char32_t code = native_text::first_char_code(std::string(symbol).c_str()); + const bool russian_letter = (code >= 0x0410 && code <= 0x044F); + if (russian_letter || native_text::list_contains_char(allowed, symbol)) { + continue; } + SendMsgToChar(ch, "Недопустимый символ '%.*s' в позиции %d.\r\n", + static_cast(symbol.size()), symbol.data(), position); + return false; } return true; } From fbcd57ed7dc43d44d3bf01241548ccf7151eebf9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 19:42:09 +0200 Subject: [PATCH 064/110] =?UTF-8?q?fix(utf8):=20XML-=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D1=8B=20=D1=82=D0=BE=D0=B6=D0=B5=20=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B5=20=D0=BC=D0=B8=D1=80=D0=B0=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Опыт на боевом мире: после прогона и корректной остановки из 15736 файлов переписалось 40, и ровно один сменил кодировку -- userdata/glory_const.xml. Причина: шесть мест звали pugi::save_file напрямую, минуя границу записи, -- слава, именные вещи, уникальные мобы, ПОЧТА, трафик зон и рецепты ремесла. Добавлен native_text::write_file: однострочная обёртка над to_disk для тех, кто иначе унёс бы на диск нативную кодировку. Все шесть проведены через неё. Прямых save_file вне границы не осталось. --- src/engine/db/db.cpp | 7 ++++++- src/gameplay/communication/mail.cpp | 7 ++++++- src/gameplay/crafting/craft.cpp | 7 ++++++- src/gameplay/mechanics/glory_const.cpp | 7 ++++++- src/gameplay/mechanics/named_stuff.cpp | 7 ++++++- src/gameplay/mechanics/sets_drop.cpp | 7 ++++++- src/utils/native_text.cpp | 10 ++++++++++ src/utils/native_text.h | 5 +++++ 8 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index 63e1c1231..6c04ebf8b 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -1,3 +1,4 @@ +#include #include #include "utils/native_text.h" #include "gameplay/affects/affect_messages.h" @@ -1030,7 +1031,11 @@ void ZoneTrafficSave() { zone_node.append_attribute("traffic") = i.traffic; } - doc.save_file(MUD::StateManager().Path(state::EStateFile::kZoneTraffic).c_str()); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной + // (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + native_text::write_file(MUD::StateManager().Path(state::EStateFile::kZoneTraffic), xml.str()); } void zone_traffic_load() { pugi::xml_document doc; diff --git a/src/gameplay/communication/mail.cpp b/src/gameplay/communication/mail.cpp index 5090724bd..38555ca4c 100644 --- a/src/gameplay/communication/mail.cpp +++ b/src/gameplay/communication/mail.cpp @@ -8,6 +8,7 @@ * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * ************************************************************************ */ +#include #include "mail.h" #include "utils/native_text.h" #include "administration/privilege.h" @@ -589,7 +590,11 @@ void save() { msg_n.append_attribute("t") = i->second.text.c_str(); } - doc.save_file(MAIL_XML_FILE); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной + // (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + native_text::write_file(MAIL_XML_FILE, xml.str()); need_save = false; } diff --git a/src/gameplay/crafting/craft.cpp b/src/gameplay/crafting/craft.cpp index 4f68ecef7..72113f64d 100644 --- a/src/gameplay/crafting/craft.cpp +++ b/src/gameplay/crafting/craft.cpp @@ -4,6 +4,7 @@ * \author Anton Gorev */ +#include #include "craft.h" #include "utils/native_text.h" #include "gameplay/mechanics/magic_item.h" @@ -1549,7 +1550,11 @@ bool CCraftModel::export_object(const ObjVnum vnum, const char *filename) { decl.append_attribute("version") = "1.0"; decl.append_attribute("encoding") = "koi8-r"; - return document.save_file(filename); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной. Объявление выше + // так и заявляет koi8-r, значит и байты должны быть koi8-r (issue #3681). + std::ostringstream xml; + document.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + return native_text::write_file(filename, xml.str()); } const std::string CObject::KIND = "simple object"; diff --git a/src/gameplay/mechanics/glory_const.cpp b/src/gameplay/mechanics/glory_const.cpp index d63f9d5b2..85649e7aa 100644 --- a/src/gameplay/mechanics/glory_const.cpp +++ b/src/gameplay/mechanics/glory_const.cpp @@ -3,6 +3,7 @@ // Copyright (c) 2010 Krodo // Part of Bylins http://www.mud.ru +#include #include "glory_const.h" #include "utils/russian_keys.h" #include "utils/native_text.h" @@ -894,7 +895,11 @@ void save() { spent_node.set_name("total_spent"); spent_node.append_attribute("amount") = total_spent; - doc.save_file(LIB_USERDATA"glory_const.xml"); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной + // (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + native_text::write_file(LIB_USERDATA"glory_const.xml", xml.str()); } void load() { diff --git a/src/gameplay/mechanics/named_stuff.cpp b/src/gameplay/mechanics/named_stuff.cpp index 8655b97c4..7d1dde22b 100644 --- a/src/gameplay/mechanics/named_stuff.cpp +++ b/src/gameplay/mechanics/named_stuff.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2010 WorM // Part of Bylins http://www.mud.ru +#include #include "named_stuff.h" #include "utils/russian_keys.h" #include "utils/native_text.h" @@ -67,7 +68,11 @@ void save() { stuf_node.append_attribute("cant_msg_a") = i->second->cant_msg_a.c_str(); } - doc.save_file(LIB_USERDATA"named_items.xml"); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной + // (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + native_text::write_file(LIB_USERDATA"named_items.xml", xml.str()); } bool check_named(CharData *ch, const ObjData *obj, const bool simple) { diff --git a/src/gameplay/mechanics/sets_drop.cpp b/src/gameplay/mechanics/sets_drop.cpp index 56e5272e6..33d23f3c7 100644 --- a/src/gameplay/mechanics/sets_drop.cpp +++ b/src/gameplay/mechanics/sets_drop.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2012 Krodo // Part of Bylins http://www.mud.ru +#include #include "sets_drop.h" #include "utils/native_text.h" @@ -964,7 +965,11 @@ void save_unique_mobs() { mob_node.append_attribute("vnum") = it->first; mob_node.append_attribute("level") = it->second; } - doc.save_file(MUD::StateManager().Path(state::EStateFile::kUniqueMobs).c_str()); + // Граница записи: XML уходит на диск в кодировке мира, а не в нативной + // (issue #3681). + std::ostringstream xml; + doc.save(xml, "\t", pugi::format_default, pugi::encoding_utf8); + native_text::write_file(MUD::StateManager().Path(state::EStateFile::kUniqueMobs), xml.str()); } void save_drop_table() { diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 98e0f57a4..60341b6f9 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -580,6 +580,16 @@ std::size_t char_offset(std::string_view s, std::size_t chars) { return utf8::byte_offset(s, chars); } +bool write_file(const std::string &path, const std::string &text) { + const std::string on_disk = to_disk(text); + std::ofstream out(path, std::ios::binary); + if (!out) { + return false; + } + out.write(on_disk.data(), static_cast(on_disk.size())); + return out.good(); +} + std::string pad_right(std::string_view s, std::size_t width) { const std::size_t len = char_count(s); std::string out(s); diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 91f754588..15b9047e5 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -202,6 +202,11 @@ std::string from_disk_text(const std::string &text); // (issue #3681). std::string to_disk(const std::string &text); +// Записать текст в файл в кодировке мира. Однострочная обёртка над to_disk для тех, кто иначе +// звал бы pugi::save_file или свой ofstream и уносил бы на диск нативную кодировку. Возвращает +// false, если файл не открылся (issue #3681). +bool write_file(const std::string &path, const std::string &text); + // Pad `s` on the right with spaces to `width` CHARACTERS. The replacement for printf's "%-Ns" // wherever the value can hold Russian: printf counts the field width in bytes, so under UTF-8 a // Cyrillic word ate twice its share and the column drifted. Under KOI8-R this is byte-for-byte From c68254effe1a468a6e48d159f737c02e8ad5e4df Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 20:06:01 +0200 Subject: [PATCH 065/110] =?UTF-8?q?fix(utf8):=20=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=83=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BA=20=D1=82=D0=B5=D0=BC=20=D0=B6=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=B0=D0=BC,=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE=20=D0=B8=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=83=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Опыт на боевом мире вскрыл, что предыдущий коммит сделал хуже: обменник, посылки, хранилища, дружины, титулы и слава читают свои файлы МИМО границы, то есть держат в памяти байты KOI8-R. Добавленная запись через to_disk приняла их за UTF-8 и прогнала через словарь замен -- в exchange.db названия лотов превратились в латинскую транслитерацию. Правило простое и должно соблюдаться попарно: если подсистема читает сырой файл, она и пишет сырой; если читает через границу -- пишет через границу. Теперь все шесть читают через границу, и запись стала верной. Побочно чинится и отображение: до этого названия лотов, посылок, вещей в хранилищах, дружин и титулов уезжали клиенту байтами чужой кодировки. Читатели дружин и славы переведены на istringstream поверх read_data_file: разбор ниже по коду не изменился ни строкой. --- src/gameplay/clans/house.cpp | 31 ++++++++++++++------------- src/gameplay/clans/house_exp.cpp | 31 +++++++++++++++------------ src/gameplay/communication/parcel.cpp | 13 +++++++---- src/gameplay/economics/exchange.cpp | 25 ++++++++++++++------- src/gameplay/mechanics/depot.cpp | 17 ++++++++------- src/gameplay/mechanics/glory.cpp | 7 +++--- src/gameplay/mechanics/title.cpp | 7 +++--- 7 files changed, 76 insertions(+), 55 deletions(-) diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 2e52fa32a..5aba4d979 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -193,8 +193,10 @@ Clan::~Clan() { } // релоад одного отдельного клана, абр. указывать на латинице! void Clan::ClanReload(const std::string &index) { - std::ifstream file(LIB_CLANS "index"); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(LIB_CLANS "index")); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", LIB_CLANS "index", __FILE__, __func__, __LINE__); return; } @@ -202,7 +204,6 @@ void Clan::ClanReload(const std::string &index) { std::list clanIndex; while (file >> buffer) clanIndex.push_back(buffer); - file.close(); // ищем наш клан for (const auto &it : clanIndex) { if (it == index) { @@ -227,8 +228,10 @@ void Clan::ClanLoadSingle(const std::string &index) { const auto tempClan = std::make_shared(); std::string filename = LIB_CLANS + index + "/" + index; - std::ifstream file(filename.c_str()); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } @@ -488,7 +491,6 @@ void Clan::ClanLoadSingle(const std::string &index) { } } - file.close(); // тут нужно проверить наличие критичных для клана полей // т.к. загрузка без привязки к положению в файле - что-то может не проинициализироваться @@ -575,7 +577,6 @@ void Clan::ClanLoadSingle(const std::string &index) { tempClan->frList[victim] = tempRecord; } } - pkFile.close(); } //подгружаем кланстафф std::ifstream stuffFile((filename + ".stuff").c_str()); @@ -627,8 +628,10 @@ void Clan::ClanLoad() { Clan::ClanList.clear(); // файл со списком кланов - std::ifstream file(LIB_CLANS "index"); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(LIB_CLANS "index")); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", LIB_CLANS "index", __FILE__, __func__, __LINE__); return; } @@ -636,7 +639,6 @@ void Clan::ClanLoad() { std::list clanIndex; while (file >> buffer) clanIndex.push_back(buffer); - file.close(); // собственно грузим кланы for (const auto &it : clanIndex) { Clan::ClanLoadSingle(it); @@ -789,7 +791,6 @@ bool write_if_changed(const std::string &filename, const std::string &contents, // прежнюю сборку становится невозможен (issue #3681). const std::string on_disk = native_text::to_disk(contents); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); cache = contents; return true; @@ -2466,7 +2467,6 @@ void Clan::save_chest() { // прежнюю сборку становится невозможен (issue #3681). const std::string on_disk = native_text::to_disk(out.str()); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); break; } } @@ -2622,7 +2622,6 @@ void Clan::write_mod(const std::string &arg) { // чтению -- иначе первое же сохранение переводит файл в UTF-8, и откат на // прежнюю сборку становится невозможен (issue #3681). file << native_text::to_disk(arg); - file.close(); mod_text = arg; } @@ -2643,8 +2642,10 @@ void Clan::load_mod() { std::string abbrev = this->get_file_abbrev(); std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".mod"; - std::ifstream file(filename.c_str(), std::ios::binary); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } diff --git a/src/gameplay/clans/house_exp.cpp b/src/gameplay/clans/house_exp.cpp index 1037daa1e..6c86a44e9 100644 --- a/src/gameplay/clans/house_exp.cpp +++ b/src/gameplay/clans/house_exp.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2009 Krodo // Part of Bylins http://www.mud.ru +#include #include "utils/native_text.h" #include "house_exp.h" #include "utils/grammar/gender.h" @@ -66,8 +67,10 @@ void ClanExp::save(const std::string &abbrev) const { // * Загрузка списка экспы и буффера конкретного клана (по аббревиатуре). void ClanExp::load(const std::string &abbrev) { std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".exp"; - std::ifstream file(filename.c_str()); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } @@ -161,15 +164,16 @@ void ClanPkLog::save(const std::string &abbrev) { const std::string on_disk = native_text::to_disk(out.str()); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); need_save = false; } void ClanPkLog::load(const std::string &abbrev) { std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".war"; - std::ifstream file(filename.c_str(), std::ios::binary); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } @@ -180,7 +184,6 @@ void ClanPkLog::load(const std::string &abbrev) { buffer += "\r\n"; pk_log.push_back(buffer); } - file.close(); } void ClanPkLog::check(CharData *ch, CharData *victim) { @@ -234,8 +237,10 @@ void ClanExpHistory::add_exp(long exp) { void ClanExpHistory::load(const std::string &abbrev) { std::string filename = LIB_CLANS + abbrev + "/" + abbrev + "-history.exp"; - std::ifstream file(filename.c_str(), std::ios::binary); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { log("Error open file: %s! (%s %s %d)", filename.c_str(), __FILE__, __func__, __LINE__); return; } @@ -245,7 +250,6 @@ void ClanExpHistory::load(const std::string &abbrev) { while (file >> buffer >> exp) { list_[buffer] = exp; } - file.close(); } void ClanExpHistory::save(const std::string &abbrev) const { @@ -272,7 +276,6 @@ void ClanExpHistory::save(const std::string &abbrev) const { // прежнюю сборку становится невозможен (issue #3681). const std::string on_disk = native_text::to_disk(out.str()); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); } /** @@ -404,15 +407,16 @@ void ClanChestLog::save(const std::string &abbrev) { const std::string on_disk = native_text::to_disk(out.str()); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); need_save_ = false; } void ClanChestLog::load(const std::string &abbrev) { std::string filename = LIB_CLANS + abbrev + "/" + abbrev + ".log"; - std::ifstream file(filename.c_str(), std::ios::binary); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(filename.c_str())); + if (file.str().empty()) { return; } @@ -422,7 +426,6 @@ void ClanChestLog::load(const std::string &abbrev) { buffer += "\r\n"; chest_log_.push_back(buffer); } - file.close(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 1c4599710..4d13e4f93 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2008 Krodo // Part of Bylins http://www.mud.ru +#include #include "parcel.h" #include "utils/native_text.h" #include "engine/db/player_index.h" @@ -669,18 +670,22 @@ void load() { int fsize = ftell(fl); char *data, *readdata; - CREATE(readdata, fsize + 1); + std::vector raw(fsize + 1, '\0'); fseek(fl, 0L, SEEK_SET); - if (!fread(readdata, fsize, 1, fl) || ferror(fl)) { + if (!fread(raw.data(), fsize, 1, fl) || ferror(fl)) { fclose(fl); log("SYSERR: Memory error or cann't read parcel database file."); - free(readdata); return; }; fclose(fl); + // Граница чтения: база лежит на диске в KOI8-R, движок держит текст в нативной кодировке + // (issue #3681). + const std::string native = native_text::from_disk_text(std::string(raw.data(), fsize)); + CREATE(readdata, native.size() + 1); + memcpy(readdata, native.c_str(), native.size() + 1); + data = readdata; - *(data + fsize) = '\0'; for (fsize = 0; *data && *data != '$'; fsize++) { int error; diff --git a/src/gameplay/economics/exchange.cpp b/src/gameplay/economics/exchange.cpp index a52f8360f..2f036dc4f 100644 --- a/src/gameplay/economics/exchange.cpp +++ b/src/gameplay/economics/exchange.cpp @@ -8,6 +8,7 @@ * $Revision$ * ************************************************************************ */ +#include #include "exchange.h" #include "utils/native_text.h" #include "administration/privilege.h" @@ -870,19 +871,24 @@ int LoadExchange() { fseek(fl, 0L, SEEK_END); fsize = ftell(fl); - CREATE(readdata, fsize + 1); + std::vector raw(fsize + 1, '\0'); fseek(fl, 0L, SEEK_SET); - auto actual_size = fread(readdata, 1, fsize, fl); + auto actual_size = fread(raw.data(), 1, fsize, fl); if (!actual_size || ferror(fl)) { fclose(fl); log("SYSERR: Memory error or cann't read exchange database file. (exchange.cpp)"); - free(readdata); return (0); }; fclose(fl); + // Граница чтения: база лежит на диске в KOI8-R, а движок работает с текстом в нативной + // кодировке. Без этого названия лотов оставались бы байтами чужой кодировки -- и на экране, + // и при обратной записи через to_disk, которая приняла бы их за UTF-8 (issue #3681). + const std::string native = native_text::from_disk_text(std::string(raw.data(), actual_size)); + CREATE(readdata, native.size() + 1); + memcpy(readdata, native.c_str(), native.size() + 1); + data = readdata; - *(data + actual_size) = '\0'; // Новая база или старая? get_buf_line(&data, buffer); @@ -956,22 +962,25 @@ int exchange_database_reload(bool loadbackup) { fseek(fl, 0L, SEEK_END); fsize = ftell(fl); - CREATE(readdata, fsize + 1); + std::vector raw(fsize + 1, '\0'); fseek(fl, 0L, SEEK_SET); - auto actual_size = fread(readdata, 1, fsize, fl); + auto actual_size = fread(raw.data(), 1, fsize, fl); if (!actual_size || ferror(fl)) { fclose(fl); if (loadbackup) log("SYSERR: Memory error or cann't read exchange database backup file. (exchange.cpp)"); else log("SYSERR: Memory error or cann't read exchange database file. (exchange.cpp)"); - free(readdata); return (0); }; fclose(fl); + // Граница чтения, как и выше (issue #3681). + const std::string native = native_text::from_disk_text(std::string(raw.data(), actual_size)); + CREATE(readdata, native.size() + 1); + memcpy(readdata, native.c_str(), native.size() + 1); + data = readdata; - *(data + actual_size) = '\0'; // Новая база или старая? get_buf_line(&data, buffer); diff --git a/src/gameplay/mechanics/depot.cpp b/src/gameplay/mechanics/depot.cpp index 5b4bbca04..8214c747b 100644 --- a/src/gameplay/mechanics/depot.cpp +++ b/src/gameplay/mechanics/depot.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2007 Krodo // Part of Bylins http://www.mud.ru +#include #include "utils/native_text.h" #include "depot.h" #include "engine/db/player_index.h" @@ -216,20 +217,19 @@ std::string generate_purged_text(long uid, int obj_vnum, unsigned int obj_uid) { return out.str(); } - const std::shared_ptr databuf(new char[fsize + 1], std::default_delete()); + std::vector raw(fsize + 1, '\0'); fseek(fl, 0L, SEEK_SET); - if (!databuf - || !fread(databuf.get(), fsize, 1, fl) - || ferror(fl)) { + if (!fread(raw.data(), fsize, 1, fl) || ferror(fl)) { fclose(fl); log("Хранилище: ошибка чтения файла предметов (%s).", filename); return out.str(); } fclose(fl); - char *data = databuf.get(); - data[fsize] = '\0'; + // Граница чтения: файлы хранилищ лежат на диске в KOI8-R (issue #3681). + std::string databuf = native_text::from_disk_text(std::string(raw.data(), fsize)); + char *data = databuf.data(); int error = 0; for (fsize = 0; *data && *data != '$'; fsize++) { @@ -308,8 +308,9 @@ bool show_purged_message(CharData *ch) { return true; } std::ostringstream out; - out << "\r\n" << file.rdbuf(); - SendMsgToChar(out.str(), ch); + out << file.rdbuf(); + // Граница чтения: файл записан в кодировке мира (issue #3681). + SendMsgToChar("\r\n" + native_text::from_disk_text(out.str()), ch); remove(name.c_str()); purged_list.erase(it); need_save_purged_list = true; diff --git a/src/gameplay/mechanics/glory.cpp b/src/gameplay/mechanics/glory.cpp index 22aa48567..381629b01 100644 --- a/src/gameplay/mechanics/glory.cpp +++ b/src/gameplay/mechanics/glory.cpp @@ -140,8 +140,10 @@ void GloryNode::copy_glory(const GloryNodePtr& k) { // * Загрузка общего списка славы, проверка на валидность чара, славы, сверка мд5 файла. void load_glory() { const char *glory_file = LIB_USERDATA"glory.lst"; - std::ifstream file(glory_file); - if (!file.is_open()) { + // Граница чтения: файл лежит на диске в кодировке мира, поднимаем его целиком -- + // дальше разбор идёт по нативному тексту и не меняется (issue #3681). + std::istringstream file(native_text::read_data_file(glory_file)); + if (file.str().empty()) { log("Glory: не удалось открыть файл на чтение: %s", glory_file); return; } @@ -288,7 +290,6 @@ void save_glory() { // прежнюю сборку становится невозможен (issue #3681). const std::string on_disk = native_text::to_disk(out.str()); file.write(on_disk.data(), static_cast(on_disk.size())); - file.close(); } // * Аналог бывшего макроса GET_GLORY(). diff --git a/src/gameplay/mechanics/title.cpp b/src/gameplay/mechanics/title.cpp index 00b3b9c79..1a482eaf6 100644 --- a/src/gameplay/mechanics/title.cpp +++ b/src/gameplay/mechanics/title.cpp @@ -450,10 +450,11 @@ void TitleSystem::load_title_list() { std::getline(file, pre_title); std::getline(file, title); WaitingTitlePtr temp(new waiting_title); - temp->title = title; - temp->pre_title = pre_title; + // Граница чтения: список лежит на диске в кодировке мира (issue #3681). + temp->title = native_text::from_disk_line(title.c_str()); + temp->pre_title = native_text::from_disk_line(pre_title.c_str()); temp->unique = unique; - title_list[name] = temp; + title_list[native_text::from_disk_line(name.c_str())] = temp; } file.close(); } From b38b68c24edcf14261bf09151d45ffbd63c005d9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 20:25:39 +0200 Subject: [PATCH 066/110] =?UTF-8?q?fix(utf8):=20pk-=D0=BB=D0=BE=D0=B3=20?= =?UTF-8?q?=D0=B8=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=89=D0=B8=20=D0=B4=D1=80=D1=83=D0=B6=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=20=D1=82=D0=BE=D0=B6=D0=B5=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20=D1=87=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Второй прогон опыта: из 15736 файлов утёк ровно один -- userdata/clans/ir/ir.pkl, причины ("пидарас", "доигрался дурачок") превратились в латинскую транслитерацию. Его читатель не подошёл под общий шаблон замены: проверка была не "!is_open", а "is_open", поэтому файл остался на сыром ifstream. То же самое было у .stuff -- названий именных вещей дружины. Оба переведены на istringstream поверх read_data_file. Сырых ifstream в клановой подсистеме не осталось. --- src/gameplay/clans/house.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 5aba4d979..40dea7c97 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -542,8 +542,9 @@ void Clan::ClanLoadSingle(const std::string &index) { } // подгружаем пкл/дрл - std::ifstream pkFile((filename + ".pkl").c_str()); - if (pkFile.is_open()) { + // Граница чтения: pk-лог лежит на диске в кодировке мира (issue #3681). + std::istringstream pkFile(native_text::read_data_file(filename + ".pkl")); + if (!pkFile.str().empty()) { int author = 0; while (pkFile >> author) { int victim = 0; @@ -579,8 +580,9 @@ void Clan::ClanLoadSingle(const std::string &index) { } } //подгружаем кланстафф - std::ifstream stuffFile((filename + ".stuff").c_str()); - if (stuffFile.is_open()) { + // Граница чтения: названия именных вещей дружины лежат в кодировке мира (issue #3681). + std::istringstream stuffFile(native_text::read_data_file(filename + ".stuff")); + if (!stuffFile.str().empty()) { int i; while (stuffFile >> i) { ClanStuffName temp; From f5cad346de148df1935d9ddcf8fe2bc6952d7c06 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Mon, 17 Aug 2026 23:31:39 +0200 Subject: [PATCH 067/110] =?UTF-8?q?fix(utf8):=20=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D1=8F=20=D0=B8=20admin=20API=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=BB=D0=B8=20=D1=82=D0=B5=D0=BA=D1=81=D1=82?= =?UTF-8?q?,=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D1=8F=20=D0=B5=D0=B3=D0=BE=20=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=80=D0=B0=D0=B7=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Наблюдаемость и admin API компилируются только при -Dotel/-Dadmin_api, а все сборки, на которых шёл аудит, были без них -- поэтому подсистема выпала из проверки целиком, и обнаружилось это уже в Grafana: кириллица в логах пришла кашей вида "пгӷ|пӈӷпӕ" -- UTF-8-байты, прогнанные через KOI8-R->UTF-8 ещё раз. Раньше конверсия была верна: движок держал KOI8-R, а OTLP и JSON требуют UTF-8. Теперь движок держит UTF-8, и оба протокола получают ровно то, что нужно, без всякой конверсии. Так что koi8r_to_utf8 выкинут вместе со всеми 14 вызовами, а в admin API выкинут utf8_to_koi8r на входящем логине: имя из JSON уже нативное. Второй тамошний конвертер (koi8r_to_utf8) не звался ниоткуда -- ушёл как мёртвый. --- src/engine/network/admin_api.cpp | 37 ++--------------------- src/engine/observability/helpers.cpp | 23 -------------- src/engine/observability/helpers.h | 7 ----- src/engine/observability/log_sender.cpp | 6 ++-- src/engine/observability/metrics.cpp | 17 ++--------- src/engine/observability/trace_sender.cpp | 8 ++--- src/engine/observability/traces.cpp | 10 +++--- src/utils/tracing/trace_sender.h | 2 +- 8 files changed, 18 insertions(+), 92 deletions(-) diff --git a/src/engine/network/admin_api.cpp b/src/engine/network/admin_api.cpp index 0ef231b0c..72a7ee014 100644 --- a/src/engine/network/admin_api.cpp +++ b/src/engine/network/admin_api.cpp @@ -31,38 +31,6 @@ using json = nlohmann::json; using namespace admin_api::handlers; -// ============================================================================ -// Helper functions for encoding conversion -// ============================================================================ - -// Convert KOI8-R string to UTF-8 for JSON -std::string koi8r_to_utf8(const std::string &koi8r) { - char utf8_buf[kMaxSockBuf * 6]; - char koi8r_buf[kMaxSockBuf * 6]; - - utf8_buf[0] = '\0'; - - strncpy(koi8r_buf, koi8r.c_str(), sizeof(koi8r_buf) - 1); - koi8r_buf[sizeof(koi8r_buf) - 1] = 0; - - codepages::koi_to_utf8(koi8r_buf, utf8_buf); - - return std::string(utf8_buf); -} - -// Convert UTF-8 string to KOI8-R (for incoming JSON data) -std::string utf8_to_koi8r(const std::string &utf8) { - char koi8r_buf[kMaxSockBuf * 6]; - char utf8_buf[kMaxSockBuf * 6]; - - strncpy(utf8_buf, utf8.c_str(), sizeof(utf8_buf) - 1); - utf8_buf[sizeof(utf8_buf) - 1] = '\0'; - - codepages::utf8_to_koi(utf8_buf, koi8r_buf); - - return std::string(koi8r_buf); -} - // ============================================================================ // Admin API socket I/O and chunking // ============================================================================ @@ -254,10 +222,9 @@ void admin_api_parse(DescriptorData *d, char *argument) { client_ip = d->host; } - // Convert username from UTF-8 to KOI8-R (server's internal encoding) - std::string username = utf8_to_koi8r(username_utf8); + // JSON is UTF-8 and so is the engine, so the name goes through as it is. + const std::string &username = username_utf8; - // Authenticate with KOI8-R username if (admin_api_authenticate(d, username.c_str(), password.c_str())) { json response; response["status"] = "ok"; diff --git a/src/engine/observability/helpers.cpp b/src/engine/observability/helpers.cpp index ffd987ee4..3b8b40c01 100644 --- a/src/engine/observability/helpers.cpp +++ b/src/engine/observability/helpers.cpp @@ -1,5 +1,4 @@ #include "helpers.h" -#include "utils/utils_encoding.h" namespace observability { @@ -21,28 +20,6 @@ double ScopedMetric::elapsed_seconds() const { return m_timer.delta().count(); } -std::string koi8r_to_utf8(const std::string& input) { - if (input.empty()) { - return input; - } - // Fast path: ASCII-only strings need no conversion - bool has_high = false; - for (unsigned char c : input) { - if (c >= 128) { - has_high = true; - break; - } - } - if (!has_high) { - return input; - } - // Each KOI8-R byte expands to at most 3 UTF-8 bytes, plus null terminator - std::string output(input.size() * 3 + 1, '\0'); - codepages::koi_to_utf8(const_cast(input.c_str()), &output[0]); - output.resize(strlen(output.c_str())); - return output; -} - } // namespace observability // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/engine/observability/helpers.h b/src/engine/observability/helpers.h index c2ff59b81..0f39b98bd 100644 --- a/src/engine/observability/helpers.h +++ b/src/engine/observability/helpers.h @@ -34,13 +34,6 @@ class ScopedMetric { utils::CExecutionTimer m_timer; }; -/** - * Convert string from KOI8-R to UTF-8. - * Safe to call on ASCII strings (pass through unchanged). - * Used to sanitize all strings before sending to OTEL (protobuf requires UTF-8). - */ -std::string koi8r_to_utf8(const std::string& input); - using utils::NowTs; } // namespace observability diff --git a/src/engine/observability/log_sender.cpp b/src/engine/observability/log_sender.cpp index 265fb5277..fb7308702 100644 --- a/src/engine/observability/log_sender.cpp +++ b/src/engine/observability/log_sender.cpp @@ -62,7 +62,7 @@ static void AddAttributesToLogRecord( baggage->GetAllEntries([&log_record](opentelemetry::nostd::string_view key, opentelemetry::nostd::string_view value) { std::string key_str(key.data(), key.size()); - std::string value_str(koi8r_to_utf8(std::string(value.data(), value.size()))); + std::string value_str(value.data(), value.size()); log_record->SetAttribute(key_str, value_str); return true; // continue iteration }); @@ -70,7 +70,7 @@ static void AddAttributesToLogRecord( // Add user attributes for (const auto& [key, value] : user_attributes) { - log_record->SetAttribute(key, koi8r_to_utf8(value)); + log_record->SetAttribute(key, value); } } @@ -99,7 +99,7 @@ static void LogWithLevel(logging::LogLevel level, // OTEL log records carry their own timestamp metadata. const auto sep = message.find(" :: "); const std::string body = (sep != std::string::npos) ? message.substr(sep + 4) : message; - log_record->SetBody(koi8r_to_utf8(body)); + log_record->SetBody(body); // Automatically add trace context + user attributes AddAttributesToLogRecord(log_record, attributes); diff --git a/src/engine/observability/metrics.cpp b/src/engine/observability/metrics.cpp index caf1bfdb1..204b72633 100644 --- a/src/engine/observability/metrics.cpp +++ b/src/engine/observability/metrics.cpp @@ -16,17 +16,6 @@ namespace observability { #ifdef WITH_OTEL static std::unordered_map>> histogram_cache; -// Convert all string attribute values from KOI8-R to UTF-8 at the API boundary. -// Callers MUST pass raw KOI8-R strings -- do NOT call koi8r_to_utf8() before passing attrs here, -// that would cause double-conversion and produce garbage. -// NOTE: ISpan::SetAttribute(string) also auto-converts -- same rule applies there. -static std::map ToUtf8Attrs(const std::map& attrs) { - std::map result; - for (const auto& [k, v] : attrs) { - result[k] = koi8r_to_utf8(v); - } - return result; -} #endif void OtelMetrics::RecordCounter(const std::string& name, int64_t value) { @@ -54,7 +43,7 @@ void OtelMetrics::RecordCounter(const std::string& name, int64_t value, if (meter) { if (value >= 0) { auto counter = meter->CreateUInt64Counter(name); - counter->Add(static_cast(value), ToUtf8Attrs(attributes)); + counter->Add(static_cast(value), attributes); } } } @@ -98,7 +87,7 @@ void OtelMetrics::RecordHistogram(const std::string& name, double value, histogram = it->second.get(); } auto context = opentelemetry::context::Context{}; - histogram->Record(value, ToUtf8Attrs(attributes), context); + histogram->Record(value, attributes, context); } } #else @@ -133,7 +122,7 @@ void OtelMetrics::RecordGauge(const std::string& name, double value, if (meter) { auto histogram = meter->CreateDoubleHistogram(name + ".gauge"); auto context = opentelemetry::context::Context{}; - histogram->Record(value, ToUtf8Attrs(attributes), context); + histogram->Record(value, attributes, context); } } #else diff --git a/src/engine/observability/trace_sender.cpp b/src/engine/observability/trace_sender.cpp index b764c5a54..fc29d95f8 100644 --- a/src/engine/observability/trace_sender.cpp +++ b/src/engine/observability/trace_sender.cpp @@ -36,13 +36,13 @@ void OtelSpan::End() { void OtelSpan::AddEvent(const std::string& name) { if (m_span) { - m_span->AddEvent(observability::koi8r_to_utf8(name)); + m_span->AddEvent(name); } } void OtelSpan::SetAttribute(const std::string& key, const std::string& value) { if (m_span) { - m_span->SetAttribute(key, observability::koi8r_to_utf8(value)); + m_span->SetAttribute(key, value); } } @@ -73,7 +73,7 @@ std::unique_ptr OtelTraceSender::StartSpan(const std::string& name) { if (observability::OtelProvider::Instance().IsEnabled()) { auto tracer = trace_api::Provider::GetTracerProvider()->GetTracer("bylins-tracer", "1.0.0"); if (tracer) { - auto span = tracer->StartSpan(observability::koi8r_to_utf8(name)); + auto span = tracer->StartSpan(name); return std::make_unique(span); } } @@ -96,7 +96,7 @@ std::unique_ptr OtelTraceSender::StartChildSpan( opentelemetry::trace::StartSpanOptions options; options.parent = otel_parent->GetContext(); - auto span = tracer->StartSpan(observability::koi8r_to_utf8(name), {}, options); + auto span = tracer->StartSpan(name, {}, options); return std::make_unique(span); } } diff --git a/src/engine/observability/traces.cpp b/src/engine/observability/traces.cpp index fd3d8268d..7b231952c 100644 --- a/src/engine/observability/traces.cpp +++ b/src/engine/observability/traces.cpp @@ -15,13 +15,13 @@ void Span::End() { void Span::AddEvent(const std::string& name) { if (m_span) { - m_span->AddEvent(koi8r_to_utf8(name)); + m_span->AddEvent(name); } } void Span::SetAttribute(const std::string& key, const std::string& value) { if (m_span) { - m_span->SetAttribute(key, koi8r_to_utf8(value)); + m_span->SetAttribute(key, value); } } @@ -41,7 +41,7 @@ Span OtelTraces::StartSpan(const std::string& name) { if (OtelProvider::Instance().IsEnabled()) { auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("bylins-tracer", "1.0.0"); if (tracer) { - return Span(tracer->StartSpan(koi8r_to_utf8(name))); + return Span(tracer->StartSpan(name)); } } return Span(); @@ -52,9 +52,9 @@ Span OtelTraces::StartSpan(const std::string& name, if (OtelProvider::Instance().IsEnabled()) { auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("bylins-tracer", "1.0.0"); if (tracer) { - auto span = tracer->StartSpan(koi8r_to_utf8(name)); + auto span = tracer->StartSpan(name); for (const auto& attr : attributes) { - span->SetAttribute(attr.first, koi8r_to_utf8(attr.second)); + span->SetAttribute(attr.first, attr.second); } return Span(span); } diff --git a/src/utils/tracing/trace_sender.h b/src/utils/tracing/trace_sender.h index 9d9a26309..dd32e37b1 100644 --- a/src/utils/tracing/trace_sender.h +++ b/src/utils/tracing/trace_sender.h @@ -30,7 +30,7 @@ class ISpan { virtual void End() = 0; virtual void AddEvent(const std::string& name) = 0; - // String values are auto-converted from KOI8-R to UTF-8 -- pass raw KOI8-R, never call koi8r_to_utf8() first. + // String values go out as they are: the engine holds UTF-8 and OTLP wants UTF-8. virtual void SetAttribute(const std::string& key, const std::string& value) = 0; virtual void SetAttribute(const std::string& key, int64_t value) = 0; virtual void SetAttribute(const std::string& key, double value) = 0; From c535efc973a0ed919cb23bfc03ffb62826db6915 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:08:00 +0200 Subject: [PATCH 068/110] =?UTF-8?q?fix(utf8):=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=BF=D0=B8=D1=88=D1=83?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B5=20=D0=BC=D0=B8=D1=80=D0=B0=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Логи -- внешне видимый файл, и снаружи ожидается прежняя кодировка. После флипа syslog поехал в UTF-8: движок держит текст нативным, а на записи границы не было. Заодно это чинило бы неверный вход syslog_converter (koi_to_win/koi_to_alt): он правит буфер на месте, считая байты кои-восьмыми, и получал UTF-8. Граница ставится в write_log_message, через которую идёт LogManager, плюс отдельно в shop/olc/imm и персональные логи -- те пишут в файл напрямую через vfprintf. --- src/utils/logger.cpp | 28 +++++++++++++++++++++++---- src/utils/logging/file_log_sender.cpp | 7 ++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/utils/logger.cpp b/src/utils/logger.cpp index 7f5634fe4..9715c18b3 100644 --- a/src/utils/logger.cpp +++ b/src/utils/logger.cpp @@ -6,6 +6,7 @@ #include "gameplay/mechanics/minions.h" #include "engine/entities/char_data.h" #include "utils/utils_encoding.h" +#include "utils/native_text.h" #include "utils/utils_string.h" #include "engine/ui/color.h" #include "backtrace.h" @@ -30,6 +31,25 @@ // дескрипторы открытых файлов логов для сброса буфера при креше std::list opened_files; +namespace { +// Граница записи для логов, которые пишутся прямо в файл мимо LogManager (shop/olc/imm +// и персональные). Логи -- внешне видимые файлы и остаются в кодировке мира, KOI8-R, +// поэтому текст переводится здесь, а не уходит нативным (issue #3681). +void vfprintf_on_disk(FILE *file, const char *format, va_list args) { + va_list measure; + va_copy(measure, args); + const int need = vsnprintf(nullptr, 0, format, measure); + va_end(measure); + if (need < 0) { + return; + } + std::string text(static_cast(need) + 1, '\0'); + vsnprintf(&text[0], text.size(), format, args); + text.resize(static_cast(need)); + fputs(native_text::to_disk(text).c_str(), file); +} +} // namespace + void pers_log(CharData *ch, const char *format, ...) { if (!ch) { log("NULL character resieved! (%s %s %d)", __FILE__, __func__, __LINE__); @@ -58,7 +78,7 @@ void pers_log(CharData *ch, const char *format, ...) { write_time(ch->desc->pers_log); va_list args; va_start(args, format); - vfprintf(ch->desc->pers_log, format, args); + vfprintf_on_disk(ch->desc->pers_log, format, args); va_end(args); fprintf(ch->desc->pers_log, "\n"); } @@ -232,7 +252,7 @@ void shop_log(const char *format, ...) { write_time(file); va_list args; va_start(args, format); - vfprintf(file, format, args); + vfprintf_on_disk(file, format, args); va_end(args); fprintf(file, "\n"); @@ -254,7 +274,7 @@ void olc_log(const char *format, ...) { write_time(file); va_list args; va_start(args, format); - vfprintf(file, format, args); + vfprintf_on_disk(file, format, args); va_end(args); fprintf(file, "\n"); @@ -276,7 +296,7 @@ void imm_log(const char *format, ...) { write_time(file); va_list args; va_start(args, format); - vfprintf(file, format, args); + vfprintf_on_disk(file, format, args); va_end(args); fprintf(file, "\n"); diff --git a/src/utils/logging/file_log_sender.cpp b/src/utils/logging/file_log_sender.cpp index ff54a0f81..bbf543d40 100644 --- a/src/utils/logging/file_log_sender.cpp +++ b/src/utils/logging/file_log_sender.cpp @@ -2,15 +2,20 @@ #include "utils/logger.h" #include "engine/core/config.h" #include "engine/db/global_objects.h" +#include "utils/native_text.h" #include namespace logging { -static void write_log_message(const std::string& message, FILE* file) { +static void write_log_message(const std::string& native_message, FILE* file) { if (!file) { return; } + // Граница записи: логи -- внешне видимый файл, и кодировка у них прежняя, KOI8-R. + // Дальше syslog_converter (koi_to_win/koi_to_alt) правит буфер на месте, считая + // байты кои-восьмыми, так что перевести надо именно здесь (issue #3681). + const std::string message = native_text::to_disk(native_message); if (!runtime_config.output_thread() && runtime_config.log_stderr().empty()) { fputs(message.c_str(), file); fputs("\n", file); From ee932a62399b0e701790964187b354b55cfeba6a Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:08:00 +0200 Subject: [PATCH 069/110] =?UTF-8?q?fix(utf8):=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=89=D0=B5=D0=B9=20=D0=B8=D0=B3=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=BB=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=BC=D0=BE=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Запись шла через to_disk, а чтение -- сырым fread. То есть в память вещи попадали кои-восьмыми байтами, а обратно уходили через to_koi8: тот видит невалидный UTF-8, разбирает его как Latin-1 и гонит через словарь транслита. Кириллица превращалась в латинскую кашу -- 'верий.свет' становился 'AIEUAxAOA.OxAO'. Ровно та же асимметрия, что была в обменнике и посылках, но в самом дорогом файле: имена, алиасы и метки вещей. Порча необратима: транслит склеивает разные буквы в одну латинскую. CRC считается до перевода -- по дисковым байтам, иначе сумма не сойдётся. --- src/engine/db/obj_save.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/engine/db/obj_save.cpp b/src/engine/db/obj_save.cpp index b26105816..99800f075 100644 --- a/src/engine/db/obj_save.cpp +++ b/src/engine/db/obj_save.cpp @@ -1594,10 +1594,21 @@ int Crash_load(CharData *ch) { }; fclose(fl); // Сверка CRC из уже прочитанного буфера, без повторного чтения файла. + // Считается по дисковым байтам -- до перевода в нативную кодировку, иначе сумма не сойдётся. FileCRC::verify_from_content(ch->get_uid(), FileCRC::kTextObjs, readdata, fsize); + // Граница чтения: файл лежит в кодировке мира (сейчас KOI8-R), в память вещи идут + // нативными -- зеркало к to_disk на записи. Без этого имена, алиасы и метки вещей + // уезжают в транслит при первом же сохранении (issue #3681). + { + const std::string native = native_text::from_disk_text(std::string(readdata, static_cast(fsize))); + free(readdata); + CREATE(readdata, native.size() + 1); + std::memcpy(readdata, native.data(), native.size()); + readdata[native.size()] = '\0'; + } + data = readdata; - *(data + fsize) = '\0'; //Создание объектов long timer_dec = time(0) - SAVEINFO(index)->rent.time; From 51af2532de297e5c367f7b7c28c363b5dcfad0a3 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:08:15 +0200 Subject: [PATCH 070/110] =?UTF-8?q?fix(utf8):=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=82=D0=BE=D0=B6=D0=B5=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StateManager читал LoadLines сырым getline, а names.cpp конвертировал только на записи -- та же однобокая пара, что съела метки вещей. Через эти списки идут имена персонажей: new_name.lst, apr_name.lst, dis_name.lst, xnames.lst, unfreeze.lst, баны, стоп-оффтоп. Граница переехала внутрь StateManager, на все три операции сразу, а своя конверсия в names.cpp убрана, иначе вышла бы двойная. --- src/administration/names.cpp | 7 ++----- src/engine/boot/state_manager.cpp | 10 +++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/administration/names.cpp b/src/administration/names.cpp index 43887b5b0..451ddfc31 100644 --- a/src/administration/names.cpp +++ b/src/administration/names.cpp @@ -129,11 +129,8 @@ static void NewNames::save() { for (const auto &it : NewNameList) { lines.push_back(it.first); } - // Граница записи: имена уходят на диск в кодировке мира (сейчас KOI8-R), зеркально - // чтению (issue #3681). - for (auto &line : lines) { - line = native_text::to_disk(line); - } + // Кодировку диска держит сам StateManager -- здесь конвертировать уже не надо, + // иначе получилась бы двойная конверсия (issue #3681). MUD::StateManager().SaveLines(state::EStateFile::kPendingNames, lines); } diff --git a/src/engine/boot/state_manager.cpp b/src/engine/boot/state_manager.cpp index 92cc55279..2845d5a57 100644 --- a/src/engine/boot/state_manager.cpp +++ b/src/engine/boot/state_manager.cpp @@ -11,6 +11,7 @@ #include #include "utils/logger.h" +#include "utils/native_text.h" namespace state { @@ -61,7 +62,10 @@ std::vector StateManager::LoadLines(EStateFile file) const { if (!line.empty() && line.back() == '\r') { line.pop_back(); // tolerate CRLF files } - lines.push_back(std::move(line)); + // Граница чтения: списки лежат на диске в кодировке мира (сейчас KOI8-R), в память + // идут нативными. Здесь же имена персонажей, титулы и баны, и без пары к to_disk + // на записи они уезжали в транслит при первой же перезаписи файла (issue #3681). + lines.push_back(native_text::from_disk_line(line.c_str())); } return lines; } @@ -76,7 +80,7 @@ bool StateManager::SaveLines(EStateFile file, const std::vector &li return false; } for (const auto &l : lines) { - out << l << '\n'; + out << native_text::to_disk(l) << '\n'; // граница записи, зеркало к LoadLines } out.flush(); if (!out.good()) { @@ -105,7 +109,7 @@ bool StateManager::AppendLine(EStateFile file, const std::string &line) const { log("SYSERR: StateManager: cannot open '%s' for append", path.c_str()); return false; } - out << line << '\n'; + out << native_text::to_disk(line) << '\n'; // граница записи, зеркало к LoadLines return out.good(); } From e02f36b561cc4b1e565a282e633d446941c0acb9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:08:16 +0200 Subject: [PATCH 071/110] =?UTF-8?q?fix(utf8):=20=D0=BA=D0=B8=D1=80=D0=B8?= =?UTF-8?q?=D0=BB=D0=BB=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=BE=D0=BB=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Хэш -- сохранённые данные, и посчитан он по дисковым байтам. Движок стал держать пароль нативным, и те же буквы дали другие байты: у всех, чей пароль с кириллицей, вход отвалился ('Bad PW'), у кого латиница -- работал. В логе перед падением это видно как 210 неудачных входов за девять секунд по семи персонажам. Перед crypt пароль приводится к дисковой кодировке: старые хэши сходятся снова, новые остаются пригодными для отката на koi8-сборку. Заодно длина пароля считается в символах -- по strlen русский пароль из восьми букв выглядел шестнадцатисимвольным. --- src/administration/accounts.cpp | 5 ++++- src/administration/password.cpp | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/administration/accounts.cpp b/src/administration/accounts.cpp index 87b452337..43691268a 100644 --- a/src/administration/accounts.cpp +++ b/src/administration/accounts.cpp @@ -245,7 +245,10 @@ void Account::set_password(const std::string &password) { } bool Account::compare_password(const std::string &password) { - return CompareParam(this->hash_password, CRYPT(password.c_str(), this->hash_password.c_str()), true); + // Тот же приём, что в Password::compare_password: хэш посчитан по дисковым байтам, + // поэтому пароль приводим к дисковой кодировке перед crypt (issue #3681). + return CompareParam(this->hash_password, + CRYPT(native_text::to_disk(password).c_str(), this->hash_password.c_str()), true); } bool Account::quest_is_available(int id) { diff --git a/src/administration/password.cpp b/src/administration/password.cpp index d3df2b664..f5f6097ba 100644 --- a/src/administration/password.cpp +++ b/src/administration/password.cpp @@ -9,6 +9,7 @@ #include "engine/ui/interpreter.h" #include "engine/entities/char_data.h" #include "engine/entities/char_player.h" +#include "utils/native_text.h" // для ручного отключения крипования (на локалке лучше собирайте через make test и не парьтесь) //#define NOCRYPT // в случае сборки без криптования просто пишем пароль в открытом виде @@ -28,6 +29,14 @@ namespace Password { +// Хэш пароля -- сохранённые данные, и посчитан он когда-то по дисковым байтам (KOI8-R). +// Движок теперь держит текст нативным, поэтому кириллический пароль дал бы другие байты +// и не сошёлся бы с сохранённым хэшем. Приводим к дисковой форме перед crypt: старые хэши +// продолжают сходиться, а новые остаются пригодными для отката (issue #3681). +static std::string password_bytes(const std::string &pwd) { + return native_text::to_disk(pwd); +} + const char *BAD_PASSWORD = "Пароль должен быть от 8 до 50 символов и не должен быть именем персонажа."; const unsigned int MIN_PWD_LENGTH = 8; const unsigned int MAX_PWD_LENGTH = 50; @@ -54,7 +63,7 @@ std::string generate_md5_hash(const std::string &pwd) { } key[12] = '$'; key[13] = '\0'; - return CRYPT(pwd.c_str(), key); + return CRYPT(password_bytes(pwd).c_str(), key); #endif } @@ -108,10 +117,10 @@ bool get_password_type(const CharData *ch) { bool compare_password(CharData *ch, const std::string &pwd) { bool result = 0; if (get_password_type(ch)) - result = CompareParam(ch->get_passwd(), CRYPT(pwd.c_str(), ch->get_passwd().c_str()), 1); + result = CompareParam(ch->get_passwd(), CRYPT(password_bytes(pwd).c_str(), ch->get_passwd().c_str()), 1); else { // если пароль des сошелся - конвертим сразу в md5 (10 - бывший MAX_PWD_LENGTH) - char *s = (char *) CRYPT(pwd.c_str(), ch->get_passwd().c_str()); + char *s = (char *) CRYPT(password_bytes(pwd).c_str(), ch->get_passwd().c_str()); if (s && !strncmp(s, ch->get_passwd().c_str(), 10)) { set_password(ch, pwd); result = 1; @@ -131,7 +140,13 @@ bool compare_password(CharData *ch, const std::string &pwd) { bool check_password(const CharData *ch, const char *pwd) { // при вырубленном криптовании на локалке пароль можно ставить любой #ifndef NOCRYPT - if (!pwd || !str_cmp(pwd, GET_PC_NAME(ch)) || strlen(pwd) > MAX_PWD_LENGTH || strlen(pwd) < MIN_PWD_LENGTH) + if (!pwd) { + return 0; + } + // Длина считается в символах: с UTF-8 кириллица занимает по два байта, и по strlen + // восьмибуквенный русский пароль выглядел бы шестнадцатисимвольным (issue #3681). + const std::size_t length = native_text::char_count(pwd); + if (!str_cmp(pwd, GET_PC_NAME(ch)) || length > MAX_PWD_LENGTH || length < MIN_PWD_LENGTH) return 0; #else UNUSED_ARG(ch); @@ -145,7 +160,7 @@ bool check_password(const CharData *ch, const char *pwd) { * \return 0 - не сошлось, 1 - сошлось */ bool compare_password(std::string const &hash, std::string const &pass) { - return CompareParam(hash.c_str(), CRYPT(pass.c_str(), hash.c_str()), 1); + return CompareParam(hash.c_str(), CRYPT(password_bytes(pass).c_str(), hash.c_str()), 1); } } // namespace Password From 4cbeb77a1d98f2255c0231bd8921212b107aace2 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:08:16 +0200 Subject: [PATCH 072/110] =?UTF-8?q?fix(utf8):=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20=D1=81=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D0=B0=D1=81=D1=8C=20=D0=B2=20=D0=B1=D0=B0?= =?UTF-8?q?=D0=B9=D1=82=D0=B0=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kMaxNameLength -- двадцать, и по strlen в него переставали влезать имена длиннее десяти русских букв: персонаж с таким именем не мог войти, а бог -- проставить падежи через set. Считаем символы. --- src/engine/ui/cmd_god/do_set.cpp | 7 ++++--- src/engine/ui/login.cpp | 16 ++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/engine/ui/cmd_god/do_set.cpp b/src/engine/ui/cmd_god/do_set.cpp index 61c364026..66534863b 100644 --- a/src/engine/ui/cmd_god/do_set.cpp +++ b/src/engine/ui/cmd_god/do_set.cpp @@ -30,6 +30,7 @@ #include #include +#include "utils/native_text.h" enum class ESetVict { kPc, @@ -626,7 +627,7 @@ int PerformSet(CharData *ch, CharData *vict, int mode, char *val_arg) { SendMsgToChar(buf, ch); } else { for (i = grammar::ECase::kFirstCase; i <= grammar::ECase::kLastCase; i++) { - if (strlen(npad[i]) < kMinNameLength || strlen(npad[i]) > kMaxNameLength) { + if (static_cast(native_text::char_count(npad[i])) < kMinNameLength || static_cast(native_text::char_count(npad[i])) > kMaxNameLength) { sprintf(buf, "Падеж номер %d некорректен.\r\n", ++i); SendMsgToChar(buf, ch); return (0); @@ -634,8 +635,8 @@ int PerformSet(CharData *ch, CharData *vict, int mode, char *val_arg) { } if (_parse_name(npad[0], npad[0]) || - strlen(npad[0]) < kMinNameLength || - strlen(npad[0]) > kMaxNameLength || + static_cast(native_text::char_count(npad[0])) < kMinNameLength || + static_cast(native_text::char_count(npad[0])) > kMaxNameLength || !IsNameAvailable(npad[0]) || reserved_word(npad[0]) || fill_word(npad[0])) { SendMsgToChar("Некорректное имя.\r\n", ch); return (0); diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index fa99e521c..a1c2ba765 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -1134,8 +1134,8 @@ static void HandleGetName(DescriptorData *d, char *argument) { return; } else { if (parse_exist_name(argument, tmp_name) || - strlen(tmp_name) < (kMinNameLength - 1) || // дабы можно было войти чарам с 4 буквами - strlen(tmp_name) > kMaxNameLength || + static_cast(native_text::char_count(tmp_name)) < (kMinNameLength - 1) || // дабы можно было войти чарам с 4 буквами + static_cast(native_text::char_count(tmp_name)) > kMaxNameLength || !IsValidName(tmp_name) || fill_word(tmp_name) || reserved_word(tmp_name)) { iosystem::write_to_output("Некорректное имя. Повторите, пожалуйста.\r\n" "Имя : ", d); return; @@ -1166,7 +1166,7 @@ static void HandleGetName(DescriptorData *d, char *argument) { return; } - if (strlen(tmp_name) < (kMinNameLength)) { + if (static_cast(native_text::char_count(tmp_name)) < (kMinNameLength)) { iosystem::write_to_output("Некорректное имя. Повторите, пожалуйста.\r\n" "Имя : ", d); return; } @@ -1200,7 +1200,7 @@ static void HandleGetName(DescriptorData *d, char *argument) { } else // player unknown -- make new character { // еще одна проверка - if (strlen(tmp_name) < (kMinNameLength)) { + if (static_cast(native_text::char_count(tmp_name)) < (kMinNameLength)) { iosystem::write_to_output("Некорректное имя. Повторите, пожалуйста.\r\n" "Имя : ", d); return; } @@ -1245,8 +1245,8 @@ static void HandleNewChar(DescriptorData *d, char *argument) { } if (_parse_name(argument, tmp_name) || - strlen(tmp_name) < kMinNameLength || - strlen(tmp_name) > kMaxNameLength || + static_cast(native_text::char_count(tmp_name)) < kMinNameLength || + static_cast(native_text::char_count(tmp_name)) > kMaxNameLength || !IsValidName(tmp_name) || fill_word(tmp_name) || reserved_word(tmp_name)) { iosystem::write_to_output("Некорректное имя. Повторите, пожалуйста.\r\n" "Имя : ", d); return; @@ -1496,8 +1496,8 @@ static void HandleNameCase(DescriptorData *d, char *argument, int step) { GetCase(GET_PC_NAME(d->character), d->character->get_sex(), cur.idx, argument); } if (!_parse_name(argument, tmp_name) - && strlen(tmp_name) >= kMinNameLength - && strlen(tmp_name) <= kMaxNameLength + && static_cast(native_text::char_count(tmp_name)) >= kMinNameLength + && static_cast(native_text::char_count(tmp_name)) <= kMaxNameLength && !strn_cmp(tmp_name, GET_PC_NAME(d->character), std::min(kMinNameLength, strlen(GET_PC_NAME(d->character)) - 1))) { From d427dba8b93f04414bac90856a0df3908ff94b60 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:27:50 +0200 Subject: [PATCH 073/110] =?UTF-8?q?fix(utf8):=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B4=D1=80=D1=83=D0=B6=D0=B8=D0=BD=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B8=D0=BB=D0=B8=D1=81=D1=8C=20=D1=81=20?= =?UTF-8?q?=D0=B4=D0=B2=D1=83=D1=85=20=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сундук читался сырым fread при записи через to_disk -- та же однобокая пара, что и у файла вещей: имена и метки вещей в сундуке уходили в транслит. Звания резались по байтам: MAX_RANK_LENGHT -- предел в символах (так и написано игроку), а с UTF-8 кириллица идёт по два байта, и 'патриарх' превращался в 'патри'. Причём substr мог разрубить символ пополам. Считаем символы и режем по границе символа. --- src/gameplay/clans/house.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/gameplay/clans/house.cpp b/src/gameplay/clans/house.cpp index 40dea7c97..2cb545f90 100644 --- a/src/gameplay/clans/house.cpp +++ b/src/gameplay/clans/house.cpp @@ -133,8 +133,10 @@ void prepare_write_mod(CharData *ch, std::string ¶m) { * и перевод всего слова в нижний регистр. */ void check_rank(std::string &rank) { - if (rank.size() > MAX_RANK_LENGHT) { - rank = rank.substr(0, MAX_RANK_LENGHT); + // Предел -- в символах (так и сказано игроку), а с UTF-8 кириллица занимает по два байта: + // по size() десятибуквенное звание резалось до пяти букв, да ещё и посреди символа (issue #3681). + if (native_text::char_count(rank) > MAX_RANK_LENGHT) { + rank = rank.substr(0, native_text::char_offset(rank, MAX_RANK_LENGHT)); } utils::ConvertToLow(rank); } @@ -1872,7 +1874,8 @@ void Clan::hcontrol_rank(CharData *ch, std::string &text) { SendMsgToChar(HCONTROL_FORMAT, ch); return; } - if (rank_male.size() > MAX_RANK_LENGHT || rank_female.size() > MAX_RANK_LENGHT) { + if (native_text::char_count(rank_male) > MAX_RANK_LENGHT + || native_text::char_count(rank_female) > MAX_RANK_LENGHT) { SendMsgToChar(ch, "Звание не должно быть длиннее %d символов.\r\n", MAX_RANK_LENGHT); return; } @@ -2546,8 +2549,18 @@ void Clan::ChestLoad() { } fclose(fl); + // Граница чтения: сундук лежит на диске в кодировке мира, в память идёт нативным -- + // зеркало к to_disk в ChestSaver. Без этого имена и метки вещей в сундуке уезжают + // в транслит при первом же сохранении дружины (issue #3681). + { + const std::string native = native_text::from_disk_text(std::string(databuf, static_cast(fsize))); + delete[] databuf; + databuf = new char[native.size() + 1]; + std::memcpy(databuf, native.data(), native.size()); + databuf[native.size()] = '\0'; + } + data = databuf; - *(data + fsize) = '\0'; for (fsize = 0; *data && *data != '$'; fsize++) { const auto obj = read_one_object_new(&data, &error); From b5f8e148e713911eb9a621342384f3e9d7fbb372 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 15:27:50 +0200 Subject: [PATCH 074/110] =?UTF-8?q?fix(utf8):=20=D1=81=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=20=D0=B8=D0=BC=D1=91=D0=BD=20=D1=81?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BD=D0=B8=D0=B2=D0=B0=D0=BB=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B8=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B0=D0=BB=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D1=83=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_names_similar сравнивал первые kMinNameLength байт -- у русского имени это две с половиной буквы -- и поднимал регистр побайтово, что кириллице не помогает. Считаем символы и сравниваем через compare_ci. Заодно список одобренных имён читается через границу: файл лежит в кодировке мира. --- src/administration/name_adviser.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/administration/name_adviser.cpp b/src/administration/name_adviser.cpp index e8a3f4975..755bd4eb3 100644 --- a/src/administration/name_adviser.cpp +++ b/src/administration/name_adviser.cpp @@ -4,6 +4,7 @@ #include "engine/entities/char_player.h" #include "engine/db/global_objects.h" +#include "utils/native_text.h" NameAdviser::NameAdviser() { std::srand(static_cast((std::time(nullptr)))); @@ -106,6 +107,8 @@ void NameAdviser::init() { std::string line; while (std::getline(approved_names_file, line)) { + // Граница чтения: файл лежит в кодировке мира (issue #3681). + line = native_text::from_disk_line(line.c_str()); std::istringstream iss(line); std::string char_name; @@ -127,21 +130,16 @@ void NameAdviser::init() { } bool NameAdviser::is_names_similar(const std::string &left, const std::string &right) { - if ((left.length() < kMinNameLength) || (right.length() < kMinNameLength)) { + // Сравниваются первые kMinNameLength символов без учёта регистра. По байтам это была бы + // половина русского имени, да ещё и с побайтовым UPPER, который кириллицу не берёт (#3681). + if ((native_text::char_count(left) < kMinNameLength) || (native_text::char_count(right) < kMinNameLength)) { return false; } - std::string short_left = left.substr(0, kMinNameLength); - for (auto &ch: short_left) { - ch = UPPER(ch); - } - - std::string short_rigth = right.substr(0, kMinNameLength); - for (auto &ch: short_rigth) { - ch = UPPER(ch); - } + const std::string short_left = left.substr(0, native_text::char_offset(left, kMinNameLength)); + const std::string short_right = right.substr(0, native_text::char_offset(right, kMinNameLength)); - return short_left == short_rigth; + return native_text::compare_ci(short_left, short_right) == 0; } void NameAdviser::remove_duplicates() { From ff62b2b043c4a5045da37a3cd3e44abee4cc8d5f Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 21:38:56 +0200 Subject: [PATCH 075/110] =?UTF-8?q?feat(utf8):=20to=5Fdisk=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BC=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D1=82=20=D0=B8=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=BB=D1=87=D0=B0=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вся порча этой миграции имеет одну подпись: в to_disk пришло то, что не валидный UTF-8. Значит строка не проходила границу чтения и держит дисковые байты. Старый to_disk разбирал их как Latin-1 и гнал через словарь транслита -- необратимо и молча. Теперь такие байты уходят на диск как есть (для диска они уже в нужной кодировке, файл остаётся цел), а в лог идёт SYSERR. Пропущенная граница видна с первого прогона, без нагрузочного стенда и без потери данных -- ровно то, чего не хватало 18.08. Байты в жалобе печатаются шестнадцатеричными нарочно: логгер пишет через этот же to_disk, и сырые байты в сообщении заставили бы жалобу жаловаться на саму себя. --- src/utils/native_text.cpp | 33 +++++++++++++++++++++++++++++++++ tests/native_text.cpp | 17 +++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 60341b6f9..5f753685a 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -15,6 +15,10 @@ and back: the straightforward version cost 17x on a string-heavy benchmark. #include "utf8.h" +#include "logger.h" + +#include +#include #include #include #include @@ -600,6 +604,35 @@ std::string pad_right(std::string_view s, std::size_t width) { } std::string to_disk(const std::string &text) { + // Предохранитель. Всё нативное -- валидный UTF-8; если сюда пришло что-то другое, значит + // строка не проходила границу чтения и держит дисковые байты (KOI8-R) как есть. + // Транслитерировать их нельзя: to_koi8 разберёт такие байты как Latin-1 и прогонит через + // словарь замен, а это необратимо -- 'верий.свет' превращается в 'AIEUAxAOA.OxAO'. Именно + // так были съедены метки вещей, сундуки дружин и списки имён (issue #3681). + // + // Поэтому пишем байты как есть -- для диска они уже в нужной кодировке, файл остаётся цел, -- + // и жалуемся в лог: дыру видно сразу, без нагрузочного прогона и без потери данных. + if (!utf8::is_valid(text)) { + static std::atomic seen{0}; + const unsigned long n = seen.fetch_add(1); + if (n < 10 || n % 10000 == 0) { + // Байты печатаются шестнадцатеричными нарочно: сунуть их в сообщение как есть + // значило бы отдать логгеру невалидный UTF-8, а он пишет через этот же to_disk -- + // и жалоба принялась бы жаловаться сама на себя без конца. + std::string head; + const std::size_t show = std::min(text.size(), 16); + char byte[4]; + for (std::size_t i = 0; i < show; ++i) { + std::snprintf(byte, sizeof(byte), "%02x", static_cast(text[i])); + head += byte; + head += ' '; + } + log("SYSERR: to_disk got non-UTF-8 text (#%lu, %zu bytes) -- a read boundary is missing " + "somewhere; writing the bytes through unchanged. First bytes: %s", + n + 1, text.size(), head.c_str()); + } + return text; + } return to_koi8(text); } diff --git a/tests/native_text.cpp b/tests/native_text.cpp index b7250cf1f..13b197984 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -470,4 +470,21 @@ TEST(NativeText, CharOffsetCutsOnCharacterBoundaries) { EXPECT_EQ(native_text::char_offset("abcdef", 4), 4u); } +TEST(NativeText, ToDiskNeverTransliteratesDiskBytes) { + // Пара к границе чтения. Если строка не проходила from_disk, она держит кои-восьмые байты, + // и старый to_disk разбирал их как Latin-1 и гнал через словарь транслита: 'верий.свет' + // становился 'AIEUAxAOA.OxAO' -- необратимо. Так были съедены метки вещей, сундуки дружин + // и списки имён (issue #3681). Теперь такие байты уходят на диск как есть. + const std::string koi8_bytes = "\xD7\xC5\xD2\xC9\xCA.\xD3\xD7\xC5\xD4"; // 'верий.свет' в KOI8-R + EXPECT_EQ(native_text::to_disk(koi8_bytes), koi8_bytes) << "дисковые байты не должны меняться"; + + // А нативный текст по-прежнему переводится в кодировку диска. + const std::string native = native_text::from_koi8(koi8_bytes); + EXPECT_NE(native, koi8_bytes) << "проверка построена на том, что кодировки различаются"; + EXPECT_EQ(native_text::to_disk(native), koi8_bytes); + + // Чистая латиница одинакова в обеих кодировках и не трогается ни в одном из случаев. + EXPECT_EQ(native_text::to_disk("plain ascii"), "plain ascii"); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 5eb3b6517d6ae7b36121639a3d1fb6228e4cf1e9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 21:38:56 +0200 Subject: [PATCH 076/110] =?UTF-8?q?fix(utf8):=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8B=20=D0=B4=D0=BB=D0=B8=D0=BD=D1=8B=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D1=81=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D1=8E=D1=82=D1=81=D1=8F=20=D0=B2=20=D1=81=D0=B8=D0=BC=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D0=B0=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Продолжение истории со званиями дружин. Там, где предел означает 'сколько букв', он сравнивался с длиной в байтах, и для кириллицы вдвое ужимался, а обрезание по байтовому индексу вдобавок могло разрубить символ пополам. Метка на вещи (32 символа превращались в 16), название комнаты в OLC, имена мобов и комнат из скриптов, длина запроса богам. Пределы, охраняющие буфер, оставлены байтовыми -- они про размер памяти, а не про число букв. --- src/engine/olc/redit.cpp | 5 +++-- src/engine/scripting/dg_scripts.cpp | 32 ++++++++++++++-------------- src/engine/ui/cmd/do_sign.cpp | 7 ++++-- src/engine/ui/cmd_god/do_inspect.cpp | 6 ++++-- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/engine/olc/redit.cpp b/src/engine/olc/redit.cpp index 3f7954312..85b6d093a 100644 --- a/src/engine/olc/redit.cpp +++ b/src/engine/olc/redit.cpp @@ -679,8 +679,9 @@ void redit_parse(DescriptorData *d, char *arg) { case REDIT_NAME: if (OLC_ROOM(d)->name) free(OLC_ROOM(d)->name); - if (strlen(arg) > MAX_ROOM_NAME) - arg[MAX_ROOM_NAME - 1] = '\0'; + // Предел -- в символах, и режем по границе символа (issue #3681). + if (native_text::char_count(arg) > MAX_ROOM_NAME) + arg[native_text::char_offset(arg, MAX_ROOM_NAME - 1)] = '\0'; OLC_ROOM(d)->name = str_dup((arg && *arg) ? arg : "неопределено"); break; diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index d0f88f130..cf6b4b79c 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -2427,8 +2427,8 @@ void find_replacement(void *go, } } else if (!str_cmp(field, "iname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kNom] = subfield; } else @@ -2436,8 +2436,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "rname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kGen] = subfield; } else @@ -2445,8 +2445,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "dname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kDat] = subfield; } else @@ -2454,8 +2454,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "vname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kAcc] = subfield; } else @@ -2463,8 +2463,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "tname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kIns] = subfield; } else @@ -2472,8 +2472,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "pname")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->player_data.PNames[grammar::ECase::kPre] = subfield; } else @@ -2481,8 +2481,8 @@ void find_replacement(void *go, } else if (!str_cmp(field, "name")) { if (*subfield) { - if (strlen(subfield) > MAX_MOB_NAME) - subfield[MAX_MOB_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_MOB_NAME) + subfield[native_text::char_offset(subfield, MAX_MOB_NAME - 1)] = '\0'; mob->set_name(subfield); } else { @@ -3926,8 +3926,8 @@ void find_replacement(void *go, if (*subfield) { if (room->name) free(room->name); - if (strlen(subfield) > MAX_ROOM_NAME) - subfield[MAX_ROOM_NAME - 1] = '\0'; + if (native_text::char_count(subfield) > MAX_ROOM_NAME) + subfield[native_text::char_offset(subfield, MAX_ROOM_NAME - 1)] = '\0'; room->name = str_dup(subfield); } else snprintf(str, str_size, "%s", room->name); diff --git a/src/engine/ui/cmd/do_sign.cpp b/src/engine/ui/cmd/do_sign.cpp index 276fd7db9..00e58cf4f 100644 --- a/src/engine/ui/cmd/do_sign.cpp +++ b/src/engine/ui/cmd/do_sign.cpp @@ -5,6 +5,7 @@ #include "gameplay/mechanics/liquid.h" #include "gameplay/fight/pk.h" #include "engine/core/utils_char_obj.inl" +#include "utils/native_text.h" // чтоб не абузили длину. персональные пофиг, а клановые не надо. const int kMaxLabelLength = 32; @@ -72,8 +73,10 @@ void DoSign(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { GET_NAME(ch), target->get_short_description().c_str(), GET_OBJ_VNUM(target)); act("Вы затерли надписи на $o5.", false, ch, target, nullptr, kToChar); } else if (labels) { - if (strlen(labels) > kMaxLabelLength) - labels[kMaxLabelLength] = '\0'; + // Предел -- в символах: по байтам русская метка обрезалась вдвое короче + // и могла разрубить символ пополам (issue #3681). + if (native_text::char_count(labels) > static_cast(kMaxLabelLength)) + labels[native_text::char_offset(labels, kMaxLabelLength)] = '\0'; // убираем тильды for (int i = 0; labels[i] != '\0'; i++) diff --git a/src/engine/ui/cmd_god/do_inspect.cpp b/src/engine/ui/cmd_god/do_inspect.cpp index eb43e8c58..01e3a47df 100644 --- a/src/engine/ui/cmd_god/do_inspect.cpp +++ b/src/engine/ui/cmd_god/do_inspect.cpp @@ -13,6 +13,7 @@ #include "fmt/chrono.h" #include "utils/utils_time.h" #include "engine/db/player_index.h" +#include "utils/native_text.h" const int kMaxRequestLength{65}; const int kMinRequestLength{3}; @@ -607,11 +608,12 @@ bool InspectRequestDeque::IsBusy(const CharData *ch) { bool InspectRequestDeque::IsArgsValid(const CharData *ch, const std::vector &args) { auto &request_text = args[kRequestTextPos]; - if (request_text.length() < kMinRequestLength) { + // Длина запроса -- в символах: по байтам русский текст считался вдвое длиннее (issue #3681). + if (native_text::char_count(request_text) < kMinRequestLength) { SendMsgToChar("Слишком короткий запрос.\r\n", ch); return false; } - if (request_text.length() > kMaxRequestLength) { + if (native_text::char_count(request_text) > kMaxRequestLength) { SendMsgToChar("Слишком длинный запрос.\r\n", ch); return false; } From a044152b42e7fc4073a3075e9e7e242f5ec5cacf Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Tue, 18 Aug 2026 21:38:56 +0200 Subject: [PATCH 077/110] =?UTF-8?q?fix(utf8):=20=D0=BE=D0=B1=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=20=D0=B1=D1=83?= =?UTF-8?q?=D1=84=D0=B5=D1=80=D1=83=20=D0=BE=D1=82=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B4=D0=BE=20=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=8B=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=B0?= =?UTF-8?q?=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На сетевой границе строка от клиента после конверсии в нативную кодировку длиннее почти вдвое, и в буфер ввода могла не влезть -- strncpy рубил её по байтовому индексу, то есть посреди символа. Это самая ходовая точка в движке: каждая длинная реплика любого не-UTF-8 клиента. Битый символ дальше уезжал и в вывод, и на диск. То же в подстановке (substitute) и в тексте квеста. Бюджет остаётся байтовым -- это размер буфера, -- но truncate_offset отступает до границы символа. --- src/engine/core/iosystem.cpp | 10 ++++++++-- src/gameplay/quests/quested.cpp | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index a428506d1..cdb1093a6 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -457,8 +457,12 @@ int process_input(DescriptorData *t) { if (t->keytable != kCodePageUTF8) { 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'; + // После конверсии строка длиннее (кириллица идёт по два байта), так что в буфер + // она может не влезть. Отступаем до границы символа: обрезать посреди символа -- + // значит отдать движку битый UTF-8 (issue #3681). + const std::size_t fits = native_text::truncate_offset(native, kMaxInputLength - 1); + std::memcpy(tmp, native.data(), fits); + tmp[fits] = '\0'; space_left = space_left + len_i - strlen(tmp); } @@ -583,6 +587,8 @@ int perform_subst(DescriptorData *t, char *orig, char *subst) { // terminate the string in case of an overflow from strncat newsub[kMaxInputLength - 1] = '\0'; + // ...и, если strncat оборвал строку посреди символа, отступаем до его границы (issue #3681). + newsub[native_text::truncate_offset(newsub, strlen(newsub))] = '\0'; strcpy(subst, newsub); return (0); diff --git a/src/gameplay/quests/quested.cpp b/src/gameplay/quests/quested.cpp index 4a3409ef3..105d23a8f 100644 --- a/src/gameplay/quests/quested.cpp +++ b/src/gameplay/quests/quested.cpp @@ -8,6 +8,7 @@ #include "utils/buffered_file_writer.h" #include "engine/entities/char_data.h" +#include "utils/native_text.h" void smash_tilde(char *str); @@ -19,8 +20,10 @@ void Quested::add(CharData *ch, int vnum, char *text) { skip_spaces(&text); std::string text_node = *text ? text : ""; + // Предел байтовый (это бюджет буфера триггерной строки), но рубить символ + // пополам нельзя -- truncate_offset отступает до границы (issue #3681). if (text_node.size() > kMaxTrglineLength) { - text_node = text_node.substr(0, kMaxTrglineLength); + text_node = text_node.substr(0, native_text::truncate_offset(text_node, kMaxTrglineLength)); } quested_[vnum] = text_node; } From ec9ddd0f81521920be07a20fee93549d77a69051 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Wed, 19 Aug 2026 13:55:34 +0200 Subject: [PATCH 078/110] =?UTF-8?q?build(utf8):=20=D0=BD=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B8=D0=B7=20KOI8-=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=B0=20(?= =?UTF-8?q?#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Флип сделан снятием working-tree-encoding с /src и /tests, а это преобразование времени checkout'а: содержимое блоба не менялось, поэтому при переключении ветки git оставляет ранее выкаченные файлы в KOI8-R и считает дерево чистым. git status молчит, сборка идёт, и бинарь получает кои-восьмые строковые литералы при движке, который считает весь текст нативным UTF-8. Ровно это и случилось на боевом 19.08. Диагноз читается прямо из жалоб предохранителя: он назвал длины строк 43/51/49/49 байт для 'Сейчас>6.5 (0,0)', 'Равноденствие>20.2 (8,1)' и соседних -- это литералы из weather.cpp, и в UTF-8 они весили бы 49/64/59/60. Воспроизводится на раз: старое дерево, git checkout ветки -- 565 из 1138 файлов остаются KOI8, git показывает чистое дерево. Теперь meson setup проверяет исходники и отказывается конфигурировать, называя причину и команду лечения: rm -rf src tests && git checkout -- src tests --- CLAUDE.md | 12 +++++++ meson.build | 9 +++++ tools/meson/check_sources_utf8.py | 58 +++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100755 tools/meson/check_sources_utf8.py diff --git a/CLAUDE.md b/CLAUDE.md index 0591f03a4..c0bb98a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -406,6 +406,18 @@ Sources are UTF-8. Edit `.cpp`/`.h` directly with the Edit tool — the KOI8-R c gone (issue #3681). Git always stored these blobs as UTF-8; what changed is that the working tree no longer converts them on checkout. +**Переключились на ветку с флипом в уже существующем дереве — перевыкачайте исходники:** + +```bash +rm -rf src tests && git checkout -- src tests +``` + +`working-tree-encoding` применяется во время checkout'а, а содержимое блоба при флипе не менялось, +поэтому git оставляет ранее выкаченные файлы в KOI8-R и считает дерево чистым — `git status` молчит. +Собранный из такого дерева бинарь получает кои-восьмые строковые литералы при движке, который +считает весь текст UTF-8: расходятся сравнения имён, доски, кодировка лога, а запись на диск гонит +такие литералы через словарь транслита. `meson setup` теперь это проверяет и отказывается собирать. + Still KOI8-R on disk, and still needing care: the world and configs (`lib/`, `lib.template/`, `/lib/cfg/**`, `/lib/misc/**`, `/lib/text/help/**`, `/lib/etc/board/**`). For those the old rule holds — convert, edit, convert back: diff --git a/meson.build b/meson.build index 366768c20..92fb19750 100644 --- a/meson.build +++ b/meson.build @@ -349,6 +349,15 @@ endif subdir('src/third_party_libs/libfort') dependencies += [fmt_dep, fort_dep] +# Исходники обязаны быть в UTF-8 (issue #3681). Дерево, выкаченное до флипа, при +# переключении ветки остаётся в KOI8-R -- git молчит, дерево числится чистым, а +# собранный отсюда бинарь получает кои-восьмые строковые литералы, и движок расходится +# сам с собой. Ловим это здесь, а не по кракозябрам в логе боевого сервера. +encoding_check = run_command(py3, files('tools/meson/check_sources_utf8.py'), meson.project_source_root(), check: false) +if encoding_check.returncode() != 0 + error(encoding_check.stdout().strip()) +endif + git = find_program('git', required: false) git_rev = 'unknown' if git.found() diff --git a/tools/meson/check_sources_utf8.py b/tools/meson/check_sources_utf8.py new file mode 100755 index 000000000..999608c17 --- /dev/null +++ b/tools/meson/check_sources_utf8.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Убедиться, что исходники в рабочем дереве -- UTF-8 (issue #3681). + +Флип на UTF-8 сделан снятием working-tree-encoding с /src и /tests в .gitattributes, +а это преобразование времени checkout'а. Дерево, выкаченное до флипа, при переключении +на ветку остаётся в KOI8-R: содержимое блоба не изменилось, и git молча оставляет файлы +как есть -- git status показывает чистое дерево. + +Собранный из такого дерева бинарь получает кои-восьмые строковые литералы, а движок +считает весь текст нативным UTF-8. Дальше расходится всё: сравнения имён, доски, +кодировка лога, а конверсия на диск гонит такие литералы через словарь транслита. + +Чинится принудительной перевыкачкой: rm -rf src tests && git checkout -- src tests +""" + +import sys +from pathlib import Path + +SUFFIXES = {".cpp", ".h", ".hpp", ".cc", ".inl"} + + +def main() -> int: + root = Path(sys.argv[1] if len(sys.argv) > 1 else ".") + bad = [] + for directory in ("src", "tests"): + base = root / directory + if not base.is_dir(): + continue + for path in base.rglob("*"): + if path.suffix not in SUFFIXES or not path.is_file(): + continue + try: + path.read_bytes().decode("utf-8") + except UnicodeDecodeError: + bad.append(path.relative_to(root)) + + if not bad: + return 0 + + print( + f"исходники не в UTF-8: {len(bad)} файлов.\n" + "\n" + "Похоже, рабочее дерево выкачено до флипа на UTF-8 (issue #3681): флип сделан\n" + "снятием working-tree-encoding в .gitattributes, а это преобразование времени\n" + "checkout'а. При переключении ветки git оставляет такие файлы в KOI8-R и считает\n" + "дерево чистым -- git status ничего не покажет. Собранный отсюда бинарь получит\n" + "кои-восьмые строковые литералы, а движок считает весь текст нативным UTF-8.\n" + "\n" + "Лечится перевыкачкой:\n" + " rm -rf src tests && git checkout -- src tests\n" + "\n" + "Например: " + ", ".join(str(x) for x in sorted(bad)[:4]) + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From 030d39c83866fc317a9ce2f54853a95c31e69d75 Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 20 Aug 2026 11:45:40 +0200 Subject: [PATCH 079/110] fix(utf8): step over a whole filter letter, not one byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ветка перевела разбор фильтров на посимвольный switch (native_text::first_char_code), но шаг по строке остался побайтовым: case rus::kIUpper: argument = one_argument(++argument, buf_tmp); В UTF-8 русская буква занимает два байта, поэтому в потоке оставался хвостовой байт и приклеивался к значению: "базар ф Имеч" искал не "меч", а "\x98меч" -- отсюда каша в выводе и пустая выборка. Ломались все фильтры сразу: базар, ценности, армор. Шагаем на длину символа через native_text::char_bytes. Мест 17: двенадцать в objects_filter.cpp (все ветки switch) и пять в do_print_armor.cpp, включая "default: ++argument" -- на незнакомой русской букве разбор тоже съезжал на полсимвола и дальше читал мусор. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/ui/cmd_god/do_print_armor.cpp | 11 +++++----- src/engine/ui/objects_filter.cpp | 27 +++++++++++++----------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/engine/ui/cmd_god/do_print_armor.cpp b/src/engine/ui/cmd_god/do_print_armor.cpp index 9fa366274..4782b5e37 100644 --- a/src/engine/ui/cmd_god/do_print_armor.cpp +++ b/src/engine/ui/cmd_god/do_print_armor.cpp @@ -38,7 +38,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { bool find_param = false; while (*argument) { switch (native_text::first_char_code(argument)) { - case rus::kEmUpper: argument = one_argument(++argument, tmpbuf); + case rus::kEmUpper: argument = one_argument(argument + native_text::char_bytes(argument), tmpbuf); if (utils::IsAbbr(tmpbuf, "булат")) { filter.material = EObjMaterial::kBulat; } else if (utils::IsAbbr(tmpbuf, "бронза")) { @@ -81,7 +81,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } find_param = true; break; - case rus::kTeUpper: argument = one_argument(++argument, tmpbuf); + case rus::kTeUpper: argument = one_argument(argument + native_text::char_bytes(argument), tmpbuf); if (utils::IsAbbr(tmpbuf, "броня") || utils::IsAbbr(tmpbuf, "armor")) { filter.type = EObjType::kArmor; } else if (utils::IsAbbr(tmpbuf, "легкие") || utils::IsAbbr(tmpbuf, "легкая")) { @@ -96,7 +96,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { } find_param = true; break; - case rus::kOUpper: argument = one_argument(++argument, tmpbuf); + case rus::kOUpper: argument = one_argument(argument + native_text::char_bytes(argument), tmpbuf); if (utils::IsAbbr(tmpbuf, "тело")) { filter.wear = EWearFlag::kBody; filter.wear_message = 3; @@ -123,7 +123,7 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { break; case rus::kAUpper: { bool tmp_find = false; - argument = one_argument(++argument, tmpbuf); + argument = one_argument(argument + native_text::char_bytes(argument), tmpbuf); if (!strlen(tmpbuf)) { SendMsgToChar("Неверный аффект предмета.\r\n", ch); return; @@ -197,7 +197,8 @@ void DoPrintArmor(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { find_param = true; break; } - default: ++argument; + // Незнакомая буква тоже пропускается целиком, иначе разбор съезжает на полсимвола. + default: argument += native_text::char_bytes(argument); } } if (!find_param) { diff --git a/src/engine/ui/objects_filter.cpp b/src/engine/ui/objects_filter.cpp index ec0b9671b..81cf22bc4 100644 --- a/src/engine/ui/objects_filter.cpp +++ b/src/engine/ui/objects_filter.cpp @@ -827,46 +827,49 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch return false; } while (*argument) { + // Буква фильтра съедается целиком: first_char_code читает символ, а шаг должен быть на + // столько же байт. В UTF-8 русская буква занимает два, и "++argument" оставлял в потоке + // хвостовой байт -- он приклеивался к значению, и "базар ф Имеч" искал не "меч". switch (native_text::first_char_code(argument)) { - case rus::kIUpper: argument = one_argument(++argument, buf_tmp); + case rus::kIUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (strlen(buf_tmp) == 0) { SendMsgToChar("Укажите имя предмета.\r\n", ch); return false; } filter.name = buf_tmp; break; - case rus::kTeUpper: argument = one_argument(++argument, buf_tmp); + case rus::kTeUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_type(buf_tmp)) { SendMsgToChar("Неверный тип предмета.\r\n", ch); return false; } break; - case rus::kEsUpper: argument = one_argument(++argument, buf_tmp); + case rus::kEsUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_state(buf_tmp)) { SendMsgToChar("Неверное состояние предмета.\r\n", ch); return false; } break; - case rus::kOUpper: argument = one_argument(++argument, buf_tmp); + case rus::kOUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_wear(buf_tmp)) { SendMsgToChar("Неверное место одевания предмета.\r\n", ch); return false; } break; - case rus::kTseUpper: argument = one_argument(++argument, buf_tmp); + case rus::kTseUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_cost(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: Ц<цена><+->.\r\n", ch); return false; } break; - case rus::kKaUpper: argument = one_argument(++argument, buf_tmp); + case rus::kKaUpper: argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_weap_class(buf_tmp)) { SendMsgToChar("Неверный класс оружия.\r\n", ch); return false; } break; case rus::kAUpper: { - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); size_t len = strlen(buf_tmp); if (len == 0) { SendMsgToChar("Укажите аффект предмета.\r\n", ch); @@ -882,28 +885,28 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch break; } // case rus::kAUpper case rus::kErUpper:// стоимость ренты - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_rent(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: Р<стоимость><+->.\r\n", ch); return false; } break; case rus::kEmUpper:// количество мортов - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_remorts(buf_tmp)) { SendMsgToChar("Неверный формат в фильтре: М<количество мортов><+->.\r\n", ch); return false; } break; case rus::kUUpper:// умения - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_skill(buf_tmp)) { SendMsgToChar("Неверное умение.\r\n", ch); return false; } break; case rus::kVeUpper:// имя выставившего на базаре - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (filter_type != EXCHANGE) { SendMsgToChar("Только для базара.\r\n", ch); return false; @@ -911,7 +914,7 @@ bool ParseFilter::parse_filter(const CharData *ch, ParseFilter &filter, const ch owner = buf_tmp; break; case rus::kPeUpper:// профессия (отсечь предметы запрещенные данному классу) - argument = one_argument(++argument, buf_tmp); + argument = one_argument(argument + native_text::char_bytes(argument), buf_tmp); if (!filter.init_profession(buf_tmp)) { SendMsgToChar("Неверное название профессии.\r\n", ch); return false; From 2fe86d2fe3069018341553bb4193e03c8270550b Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 20 Aug 2026 11:52:45 +0200 Subject: [PATCH 080/110] fix(utf8): measure wrap width in characters, not bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OutWordsList считал видимую длину слова и разделителя через size(), то есть в байтах. В UTF-8 русская буква занимает два, поэтому перенос по ширине срабатывал примерно вдвое раньше запрошенного. Видно было на syslog у бога с шириной экрана 155: [20-08-26 12:38:56][ Фридроп: упал предмет рунический знак воеводы VNUM 1923 с моба Рысь Сладострастия VNUM 97006 (45 lvl) ] Строка -- 123 символа, в 155 влезала целиком, но в байтах это 163, и её рвало посередине имени моба. Считаем через native_text::char_count. Задет был весь перенос по ширине: syslog богам, почта, теллы, доски, счет, стата -- все они ходят через OutWordsList/WrapText. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/utils/utils_string.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index dec5c04ca..c53e728cb 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -992,7 +992,10 @@ std::string utils::OutWordsList(const std::vector &words, size_t ma // склеивается через separator (first остаётся true -- первое слово идёт // сразу за префиксом без ", "). Видимую длину считаем без цветокодов. std::string result = prefix; - size_t line_length = GetStringWithoutColors(prefix).size(); + // Ширина -- в символах, а не в байтах: в UTF-8 русская буква занимает два, и счёт по + // size() рвал бы строку вдвое раньше запрошенного (issue #3681). + size_t line_length = native_text::char_count(GetStringWithoutColors(prefix)); + const size_t separator_len = native_text::char_count(separator); bool first = true; // separator -- это и есть то, что стоит между словами на одной строке // (", " для списка, " " для обычного переноса по словам). На переносе @@ -1006,14 +1009,14 @@ std::string utils::OutWordsList(const std::vector &words, size_t ma for (const auto &word : words) { // ширину считаем по видимой длине -- цветокоды (&R, &n и т.п.) на экране // места не занимают, иначе строки с цветом переносятся раньше времени - const size_t word_len = GetStringWithoutColors(word).size(); + const size_t word_len = native_text::char_count(GetStringWithoutColors(word)); if (!first) { - if (line_length + separator.size() + word_len > max_length) { + if (line_length + separator_len + word_len > max_length) { result += eol_separator + "\r\n"; line_length = 0; } else { result += separator; - line_length += separator.size(); + line_length += separator_len; } } result += word; From 1efecdfd3b9cc3689954f264d04d6a407512d7b7 Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 20 Aug 2026 11:57:45 +0200 Subject: [PATCH 081/110] fix(utf8): take prefix lengths from the literal, not a KOI8 byte count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "поло все.позн пас" отвечало "Вы не видите ничего похожего на 'е.позн'". find_all_dots сравнивал и отрезал префикс "все." жёсткой четвёркой байт: в KOI8 это ровно четыре символа, в UTF-8 -- семь. Сравнивались первые четыре байта ("вс" и половина "е"), отрезались они же, и от аргумента оставалось "е.<имя>". Длину префикса берём из самого литерала, оба префикса (all. и все.) разбираются одним циклом, так что сравнение и пропуск больше не могут разъехаться. Тем же прогоном по ветке нашлись ещё четыре места, где длина русского литерала задана числом по KOI8. Там не отрезание, а сравнение по обрезанному префиксу -- то есть лишние ложные срабатывания: exchange.cpp:707 "нет" по 3 байтам -- сработает на любое "не..." do_set.cpp:308 "вкл" по 3 байтам do_set.cpp:310 "выкл" по 4 байтам do_put.cpp:161 "кун" по 3 байтам Везде длина заменена на strlen от того же литерала. Проверка: g++ -fsyntax-only по заголовкам ветки проходит на всех четырёх файлах. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/core/target_resolver.cpp | 17 +++++++++++------ src/engine/ui/cmd/do_put.cpp | 2 +- src/engine/ui/cmd_god/do_set.cpp | 4 ++-- src/gameplay/economics/exchange.cpp | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/engine/core/target_resolver.cpp b/src/engine/core/target_resolver.cpp index 03b0fd853..5bf4106bf 100644 --- a/src/engine/core/target_resolver.cpp +++ b/src/engine/core/target_resolver.cpp @@ -872,13 +872,18 @@ int find_all_dots(char *arg) { if (!str_cmp(arg, "all") || !str_cmp(arg, "все")) { return (kFindAll); - } else if (!strn_cmp(arg, "all.", 4) || !strn_cmp(arg, "все.", 4)) { - strl_cpy(tmpname, arg + 4, kMaxInputLength); - strl_cpy(arg, tmpname, kMaxInputLength); - return (kFindAlldot); - } else { - return (kFindIndiv); } + // Длину префикса берём из самого литерала: в UTF-8 "все." -- семь байт, а не четыре, и + // жёсткая четвёрка отрезала полторы буквы, оставляя "е.<имя>" (issue #3681). + for (const char *prefix : {"all.", "все."}) { + const size_t prefix_len = strlen(prefix); + if (!strn_cmp(arg, prefix, prefix_len)) { + strl_cpy(tmpname, arg + prefix_len, kMaxInputLength); + strl_cpy(arg, tmpname, kMaxInputLength); + return (kFindAlldot); + } + } + return (kFindIndiv); } RoomRnum FindRoomRnum(CharData *ch, char *rawroomstr, int trig) { diff --git a/src/engine/ui/cmd/do_put.cpp b/src/engine/ui/cmd/do_put.cpp index eba3c0fc1..13fb5ba72 100644 --- a/src/engine/ui/cmd/do_put.cpp +++ b/src/engine/ui/cmd/do_put.cpp @@ -158,7 +158,7 @@ void do_put(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { else if (isname(theplace, "экипировка equipment")) where_bits = EFind::kObjEquip; - if (theobj && (!strn_cmp("coin", theobj, 4) || !strn_cmp("кун", theobj, 3))) { + if (theobj && (!strn_cmp("coin", theobj, 4) || !strn_cmp("кун", theobj, strlen("кун")))) { money_mode = true; if (howmany <= 0) { SendMsgToChar("Следует указать чиста конкретную сумму.\r\n", ch); diff --git a/src/engine/ui/cmd_god/do_set.cpp b/src/engine/ui/cmd_god/do_set.cpp index 66534863b..2f638f812 100644 --- a/src/engine/ui/cmd_god/do_set.cpp +++ b/src/engine/ui/cmd_god/do_set.cpp @@ -305,9 +305,9 @@ int PerformSet(CharData *ch, CharData *vict, int mode, char *val_arg) { // Find the value of the argument bool on_off_mode{false}; if (set_fields[mode].type == ESetValue::kBinary) { - if (!strn_cmp(val_arg, "on", 2) || !strn_cmp(val_arg, "yes", 3) || !strn_cmp(val_arg, "вкл", 3)) { + if (!strn_cmp(val_arg, "on", 2) || !strn_cmp(val_arg, "yes", 3) || !strn_cmp(val_arg, "вкл", strlen("вкл"))) { on_off_mode = true; - } else if (!strn_cmp(val_arg, "off", 3) || !strn_cmp(val_arg, "no", 2) || !strn_cmp(val_arg, "выкл", 4)) { + } else if (!strn_cmp(val_arg, "off", 3) || !strn_cmp(val_arg, "no", 2) || !strn_cmp(val_arg, "выкл", strlen("выкл"))) { on_off_mode = false; } else { SendMsgToChar("Значение может быть 'on' или 'off'.\r\n", ch); diff --git a/src/gameplay/economics/exchange.cpp b/src/gameplay/economics/exchange.cpp index 2f036dc4f..e51bb04f5 100644 --- a/src/gameplay/economics/exchange.cpp +++ b/src/gameplay/economics/exchange.cpp @@ -704,7 +704,7 @@ bool exchange_setfilter(CharData *ch, char *argument) { if (!correct_filter_length(ch, argument)) { return false; } - if (!strncmp(argument, "нет", 3)) { + if (!strncmp(argument, "нет", strlen("нет"))) { if (EXCHANGE_FILTER(ch)) { free(EXCHANGE_FILTER(ch)); EXCHANGE_FILTER(ch) = nullptr; From 7ea06ba259d95adca2e7b74cc2e6d1f204d9572e Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 20 Aug 2026 12:24:18 +0200 Subject: [PATCH 082/110] fix(utf8): write world files back in the on-disk encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мир на диске -- KOI8-R, движок держит текст в нативной кодировке, но запись эту границу не проходила: все одиннадцать точек сохранения открывали std::ofstream и писали нативные строки как есть. Метод ConvertToKoi8r в том же файле лежал мёртвым кодом, ни одного вызова. Из-за этого каждое сохранение молча переписывало зону в UTF-8, а следующая загрузка прогоняла её через безусловный from_koi8 второй раз -- и мир превращался в кракозябры (olist по такой зоне нечитаем). Трогать OLC для этого не требовалось: ConvertObjValues (db.cpp:453) на каждом бутe помечает на сохранение каждую зону, где нашёлся объект со старыми полями, а очередь сбрасывается на диск при выключении ("OLC: Reboot saving Objects for zone N"). На боевом так переписались зоны 1, 40, 498, 735 и 830, из которых руками не трогали ни одной. Правка: - запись собирается в память и уходит на диск через native_text::write_file, то есть ровно через to_disk -- зеркало к чтению; - чтение переведено с from_koi8 на from_disk_text: он распознаёт уже-нативный текст и не перекодирует повторно, поэтому зона, которую предыдущая сборка успела записать в UTF-8, читается как есть. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/db/yaml_world_data_source.cpp | 147 +++++++++++------------ 1 file changed, 72 insertions(+), 75 deletions(-) diff --git a/src/engine/db/yaml_world_data_source.cpp b/src/engine/db/yaml_world_data_source.cpp index 72b5a4508..a5ce84aad 100644 --- a/src/engine/db/yaml_world_data_source.cpp +++ b/src/engine/db/yaml_world_data_source.cpp @@ -733,7 +733,10 @@ std::string YamlWorldDataSource::GetText(const YAML::Node &node, const std::stri { // Файлы мира лежат на диске в KOI8-R; переводим в нативную кодировку движка // (под KOI8-R это тождество, под UTF-8 - перекодировка). Issue #3681. - std::string text = native_text::from_koi8(node[key].as()); + // from_disk_text, а не from_koi8: он распознаёт уже-нативный текст и не перекодирует + // его повторно, поэтому зона, которую предыдущая сборка успела записать в UTF-8, + // читается как есть, а не превращается в кракозябры. + std::string text = native_text::from_disk_text(node[key].as()); // Convert line endings if configured for DOS format if (m_convert_lf_to_crlf) { @@ -2740,14 +2743,18 @@ bool YamlWorldDataSource::WriteYamlAtomic(const std::string &filepath, const YAM YAML::Emitter emitter; emitter << node; - std::ofstream out(temp_filepath); - if (!out.is_open()) + // Файл мира на диске -- KOI8-R, а движок держит текст в нативной кодировке. Собираем + // в память и перекодируем один раз при записи через native_text::write_file: иначе + // первое же сохранение (OLC или очередь "Reboot saving" из ConvertObjValues) молча + // переписывает зону в UTF-8, а загрузка прогоняет её через from_koi8 второй раз -- + // и мир превращается в кракозябры (issue #3681). + std::ostringstream out; + out << emitter.c_str(); + if (!native_text::write_file(temp_filepath, out.str())) { log("SYSERR: Failed to open temp file for writing: %s", temp_filepath.c_str()); return false; } - out << emitter.c_str(); - out.close(); std::rename(temp_filepath.c_str(), filepath.c_str()); return true; @@ -2770,12 +2777,7 @@ bool YamlWorldDataSource::WriteIndexYaml(const std::string &filepath, fs::create_directories(fs::path(filepath).parent_path()); std::string temp_filepath = filepath + ".tmp"; - std::ofstream out(temp_filepath); - if (!out.is_open()) - { - log("SYSERR: Failed to open temp file for index: %s", temp_filepath.c_str()); - return false; - } + std::ostringstream out; // Match the layout the Python converter emits and the loader expects: // : // - @@ -2792,7 +2794,11 @@ bool YamlWorldDataSource::WriteIndexYaml(const std::string &filepath, out << "- " << v << "\n"; } } - out.close(); + if (!native_text::write_file(temp_filepath, out.str())) + { + log("SYSERR: Failed to open temp file for index: %s", temp_filepath.c_str()); + return false; + } std::rename(temp_filepath.c_str(), filepath.c_str()); return true; } @@ -2878,12 +2884,7 @@ void YamlWorldDataSource::SaveZone(int zone_rnum) fs::create_directories(zone_dir); } - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open temp file for writing: %s", temp_file.c_str()); - return; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); @@ -3190,7 +3191,11 @@ void YamlWorldDataSource::SaveZone(int zone_rnum) yaml.DecreaseIndent(); } - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open temp file for writing: %s", temp_file.c_str()); + return; + } std::rename(temp_file.c_str(), zone_file.c_str()); log("Saved zone %d to YAML file", zone.vnum); @@ -3365,12 +3370,7 @@ bool YamlWorldDataSource::SaveTriggers(int zone_rnum, int specific_vnum, int not // from the in-memory prototypes (which already reflect the edit). const std::string flat_path = m_world_dir + "/zones/" + std::to_string(zone.vnum) + "/triggers.yaml"; const std::string temp_file = flat_path + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - return false; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Triggers for zone " + std::to_string(zone.vnum)); for (const auto &[trig_vnum, trig] : entries) @@ -3382,7 +3382,11 @@ bool YamlWorldDataSource::SaveTriggers(int zone_rnum, int specific_vnum, int not EmitTriggerBody(yaml, trig); yaml.DecreaseIndent(); } - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + return false; + } if (std::rename(temp_file.c_str(), flat_path.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), flat_path.c_str()); @@ -3409,19 +3413,18 @@ bool YamlWorldDataSource::SaveTriggers(int zone_rnum, int specific_vnum, int not trig_file_ss << trig_dir << "/" << std::setfill('0') << std::setw(2) << rel_num << ".yaml"; const std::string trig_file = trig_file_ss.str(); const std::string temp_file = trig_file + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - continue; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Trigger #" + std::to_string(trig_vnum)); yaml.EmptyLine(); EmitTriggerBody(yaml, trig); - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + continue; + } if (std::rename(temp_file.c_str(), trig_file.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), trig_file.c_str()); @@ -3673,12 +3676,7 @@ void YamlWorldDataSource::SaveRooms(int zone_rnum, int specific_vnum) { const std::string flat_path = m_world_dir + "/zones/" + std::to_string(zone.vnum) + "/rooms.yaml"; const std::string temp_file = flat_path + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - return; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Rooms for zone " + std::to_string(zone.vnum)); for (const auto &[vnum, room] : entries) @@ -3690,7 +3688,11 @@ void YamlWorldDataSource::SaveRooms(int zone_rnum, int specific_vnum) EmitRoomBody(yaml, out, room); yaml.DecreaseIndent(); } - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + return; + } if (std::rename(temp_file.c_str(), flat_path.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), flat_path.c_str()); @@ -3715,19 +3717,18 @@ void YamlWorldDataSource::SaveRooms(int zone_rnum, int specific_vnum) int rel_num = vnum % 100; std::string room_file = rooms_dir + "/" + fmt::format("{:02d}", rel_num) + ".yaml"; std::string temp_file = room_file + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - continue; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Room #" + std::to_string(vnum)); yaml.EmptyLine(); EmitRoomBody(yaml, out, room); - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + continue; + } if (std::rename(temp_file.c_str(), room_file.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), room_file.c_str()); @@ -4322,12 +4323,7 @@ void YamlWorldDataSource::SaveMobs(int zone_rnum, int specific_vnum) { const std::string flat_path = m_world_dir + "/zones/" + std::to_string(zone.vnum) + "/mobs.yaml"; const std::string temp_file = flat_path + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - return; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Mobs for zone " + std::to_string(zone.vnum)); for (const auto &[vnum, mob] : entries) @@ -4339,7 +4335,11 @@ void YamlWorldDataSource::SaveMobs(int zone_rnum, int specific_vnum) EmitMobBody(yaml, out, *mob); yaml.DecreaseIndent(); } - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + return; + } if (std::rename(temp_file.c_str(), flat_path.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), flat_path.c_str()); @@ -4366,19 +4366,18 @@ void YamlWorldDataSource::SaveMobs(int zone_rnum, int specific_vnum) mob_file_ss << mobs_dir << "/" << std::setfill('0') << std::setw(2) << rel_num << ".yaml"; std::string mob_file = mob_file_ss.str(); std::string temp_file = mob_file + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - continue; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Mob #" + std::to_string(vnum)); yaml.EmptyLine(); EmitMobBody(yaml, out, *mob); - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + continue; + } if (std::rename(temp_file.c_str(), mob_file.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), mob_file.c_str()); @@ -4799,12 +4798,7 @@ void YamlWorldDataSource::SaveObjects(int zone_rnum, int specific_vnum) { const std::string flat_path = m_world_dir + "/zones/" + std::to_string(zone.vnum) + "/objects.yaml"; const std::string temp_file = flat_path + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - return; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Objects for zone " + std::to_string(zone.vnum)); for (const auto &[vnum, obj] : entries) @@ -4816,7 +4810,11 @@ void YamlWorldDataSource::SaveObjects(int zone_rnum, int specific_vnum) EmitObjectBody(yaml, out, obj); yaml.DecreaseIndent(); } - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + return; + } if (std::rename(temp_file.c_str(), flat_path.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), flat_path.c_str()); @@ -4843,19 +4841,18 @@ void YamlWorldDataSource::SaveObjects(int zone_rnum, int specific_vnum) obj_file_ss << objs_dir << "/" << std::setfill('0') << std::setw(2) << rel_num << ".yaml"; std::string obj_file = obj_file_ss.str(); std::string temp_file = obj_file + ".tmp"; - std::ofstream out(temp_file); - if (!out.is_open()) - { - log("SYSERR: Failed to open %s for writing", temp_file.c_str()); - continue; - } + std::ostringstream out; Koi8rYamlEmitter yaml(out); yaml.Comment("Object #" + std::to_string(vnum)); yaml.EmptyLine(); EmitObjectBody(yaml, out, obj); - out.close(); + if (!native_text::write_file(temp_file, out.str())) + { + log("SYSERR: Failed to open %s for writing", temp_file.c_str()); + continue; + } if (std::rename(temp_file.c_str(), obj_file.c_str()) != 0) { log("SYSERR: Failed to rename %s to %s", temp_file.c_str(), obj_file.c_str()); From 75f12da0522e13ed7fa67c18e426ddc7af8c1479 Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 20 Aug 2026 12:24:18 +0200 Subject: [PATCH 083/110] fix(utf8): pad table columns by characters in colored_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit colored_name считал ширину колонки в байтах: strlen для проверки и printf-ширина для добивки. В UTF-8 русское название длиной 36 символов -- это 65 байт, поле в 63 "не помещалось", добивка не срабатывала вовсе, и на базаре разъезжались все колонки. Ширину считаем в символах (native_text::char_count), цветокоды вычитаются из видимой длины, добивка делается пробелами вручную -- printf-ширина под UTF-8 не годится в принципе. Заодно ширина ограничена размером буфера: часть вызовов передаёт отрицательную len, которая в size_t превращается в астрономическую величину (named_stuff.cpp:401). Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/ui/color.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/engine/ui/color.cpp b/src/engine/ui/color.cpp index 624ac5671..51e9e7649 100644 --- a/src/engine/ui/color.cpp +++ b/src/engine/ui/color.cpp @@ -6,6 +6,11 @@ \details Константы и функции для работы с цветами telnet. */ +#include "utils/native_text.h" + +#include +#include + #include "engine/ui/color.h" #include "utils/utils.h" @@ -228,13 +233,26 @@ size_t count_colors(const char *str, size_t len) { //возвращает строку длины len + кол-во цветов*2 для того чтоб в табличке все было ровненько //left_align выравнивание строки влево char *colored_name(const char *str, size_t len, const bool left_align) { - static char cstr[128]; - static char fmt[7]; - size_t cc = len + count_colors(str) * 2; + static char cstr[256]; + + // Ширину колонки меряем в символах: в UTF-8 буква занимает два байта, и printf-ширина, + // которая считает байты, обрезала бы поле примерно вдвое -- на базаре из-за этого разъезжались + // все колонки (issue #3681). Цветокоды на экране места не занимают, поэтому из видимой длины + // они вычитаются, а не прибавляются к ширине поля, как было в байтовом варианте. + const size_t colors = count_colors(str) * 2; + const size_t chars = native_text::char_count(str); + const size_t visible = chars > colors ? chars - colors : 0; + // ширина не может быть больше буфера: отдельные вызовы передают отрицательную len, + // которая в size_t превращается в астрономическую величину + const size_t width = std::min(len, sizeof(cstr) - 1); - if (strlen(str) < cc) { - snprintf(fmt, sizeof(fmt), "%%%s%ds", (left_align ? "-" : ""), static_cast(cc)); - snprintf(cstr, sizeof(cstr), fmt, str); + if (visible < width) { + const std::string padding(width - visible, ' '); + if (left_align) { + snprintf(cstr, sizeof(cstr), "%s%s", str, padding.c_str()); + } else { + snprintf(cstr, sizeof(cstr), "%s%s", padding.c_str(), str); + } } else { snprintf(cstr, sizeof(cstr), "%s", str); } From 67e669a431e1225c50e717eb3c88a1678a1aa9fc Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 15:28:40 +0200 Subject: [PATCH 084/110] =?UTF-8?q?fix(world):=20=D0=BC=D0=B8=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=D0=B0=20=D0=B7=D0=BE=D0=BD=D1=8B,=20=D0=B3?= =?UTF-8?q?=D0=B4=D0=B5=20=D0=BC=D0=B5=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=87=D0=B5=D0=B3=D0=BE=20(#3749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три конвертера возвращали "зона изменилась" безусловно, даже когда не выставили ни одного ключа. У свитка, зелья или посоха с нулевыми values писать нечего: set_pos пишет только при num > 0, ключей не появляется, guard "уже мигрировано" на следующем буте снова ложен -- и так каждый старт. Зона попадала в очередь сохранения и переписывалась содержимым, которое не менялось. У ёмкостей и фонтанов случай иной: их жидкостное ядро уходит на диск как обычные values (get_val/set_val перенаправлены в те же ключи), а засев ключей по замыслу повторяется на каждой загрузке -- значит помечать зону не нужно вовсе. Воспроизведение: старое правило помечало ровно зоны 1, 40, 498, 735, 830 -- тот самый список из боевого лога, -- с правкой не помечается ни одной. Заодно добавлен тихий лог помеченных зон: он молчит, когда помечать нечего, и называет зоны, если очередь непуста. Повторяющийся из бута в бут список сразу покажет, что миграция снова не персистится. Почему это не ловилось прогоном: SIGTERM ведёт в hupsig -> shutdown_now(), тот ставит circle_reboot = 2, а блок сохранения зон в stop_game стоит под !reboot_is_2() и пропускается целиком. Пометки шли, но очередь не сбрасывалась. --- src/engine/db/db.cpp | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index 6c04ebf8b..7c23d32ee 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -336,9 +336,14 @@ int ConvertPotionToEValueKey(CObjectPrototype *obj, bool proto) { // MUD::Spell().IsValid() here -- ConvertObjValues runs before the spell registry is populated, so // it would reject every spell. It isn't needed either: an out-of-range/undefined spell number is // silently ignored at cast, exactly as the m_vals path already does. - const auto set_spell = [obj](ObjVal::EValueKey key, int num) { + // Помечать зону на сохранение имеет смысл, только если ключи действительно появились: + // у предмета с нулевыми val[] писать нечего, guard выше на следующем буте снова окажется + // ложным, и зона переписывалась бы вхолостую каждый старт (issue #3749). + bool migrated = false; + const auto set_spell = [obj, &migrated](ObjVal::EValueKey key, int num) { if (num > 0) { obj->SetPotionValueKey(key, num); + migrated = true; } }; set_spell(ObjVal::EValueKey::kSpell1Num, v1); @@ -353,10 +358,11 @@ int ConvertPotionToEValueKey(CObjectPrototype *obj, bool proto) { } if (brewed) { obj->SetPotionValueKey(ObjVal::EValueKey::kPotionPotency, v3); + migrated = true; } else { set_spell(ObjVal::EValueKey::kSpell3Num, v3); } - return 1; + return migrated ? 1 : 0; } /// конверт параметров прототипов ПОСЛЕ лоада всех файлов с прототипами @@ -402,8 +408,10 @@ int ConvertSpellItemToEValueKey(CObjectPrototype *obj, bool /*proto*/) { || obj->GetPotionValueKey(ObjVal::EValueKey::kMakerSkill) >= 0) { return 0; // already migrated } - const auto set_pos = [obj](ObjVal::EValueKey key, int num) { - if (num > 0) { obj->SetPotionValueKey(key, num); } + // Как и у зелий: без единого выставленного ключа сохранять нечего (issue #3749). + bool migrated = false; + const auto set_pos = [obj, &migrated](ObjVal::EValueKey key, int num) { + if (num > 0) { obj->SetPotionValueKey(key, num); migrated = true; } }; if (type == EObjType::kScroll) { set_pos(ObjVal::EValueKey::kSpell1Num, obj->get_val(1)); @@ -414,7 +422,7 @@ int ConvertSpellItemToEValueKey(CObjectPrototype *obj, bool /*proto*/) { set_pos(ObjVal::EValueKey::kMaxCharges, obj->get_val(1)); set_pos(ObjVal::EValueKey::kCurCharges, obj->get_val(2)); } - return 1; + return migrated ? 1 : 0; } // issue.magic-items-hotfix: the liquid core is stored in the kLiquid* keys (get_val/set_val redirect @@ -447,11 +455,15 @@ int ConvertDrinkconLiquidCore(CObjectPrototype *obj, bool proto) { obj->SetPotionValueKey(ObjVal::EValueKey::kLiquidCapacity, capacity); obj->SetPotionValueKey(ObjVal::EValueKey::kLiquidCurrent, obj->get_val(1)); obj->SetPotionValueKey(ObjVal::EValueKey::kLiquidType, obj->get_val(2)); - return 1; + // Зону не помечаем. Жидкостное ядро уходит на диск как обычные values (get_val/set_val + // перенаправлены в эти же ключи), нового на диске не появляется, а сам засев по замыслу + // повторяется на каждой загрузке -- иначе зона переписывалась бы вечно (issue #3749). + return 0; } void ConvertObjValues() { int save = 0; + std::set marked; for (const auto &i : obj_proto) { save = std::max(save, ConvertDrinkconSkillField(i.get(), true)); save = std::max(save, ConvertDrinkPoisonField(i.get(), true)); @@ -468,10 +480,21 @@ void ConvertObjValues() { } // ... if (save) { - olc_add_to_save_list(i->get_vnum() / 100, OLC_SAVE_OBJ); + const int zone = i->get_vnum() / 100; + olc_add_to_save_list(zone, OLC_SAVE_OBJ); + marked.insert(zone); save = 0; } } + // Молчит, когда помечать нечего. Если зоны в этом списке повторяются от бута к буту -- + // значит миграция снова не персистится, как было в issue #3749. + if (!marked.empty()) { + std::string zones; + for (const int zone : marked) { + zones += std::to_string(zone) + " "; + } + log("Converted obj values, zones queued for save: %s", zones.c_str()); + } } namespace { From 992b370cc3dff0ab16cb62142697b650085df730 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 15:35:09 +0200 Subject: [PATCH 085/110] =?UTF-8?q?refactor(utf8):=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20=D1=81=D1=80=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20starts=5Fwith,=20=D0=B1=D0=B5=D0=B7=20=D1=80?= =?UTF-8?q?=D1=83=D1=87=D0=BD=D0=BE=D0=B9=20=D0=B4=D0=BB=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit strn_cmp берёт длину третьим аргументом, и её писали числом. Для латиницы это безобидно, а у русских литералов число ставили по KOI8 -- один байт на букву. С UTF-8 такие числа разъехались с длиной литерала, причём молча: strn_cmp("кун", arg, 3) сравнивает полтора символа Первые три байта у 'кун' и 'куртка' совпадают (d0 ba d1), так что 'дать куртка кому-то' уходило в ветку выдачи денег. В посылках то же самое с пятёркой. starts_with берёт длину из самого префикса -- задать её неверно негде. Рядом skip_prefix возвращает число съеденных байт: он нужен там, где префикс после сравнения ещё и отрезают, чтобы сравнение и пропуск брались из одного места (именно их расхождение сломало 'все.' в find_all_dots). Переведён 41 вызов. Полярность сохранена: !strn_cmp(...) и == 0 означали совпадение и стали starts_with(...), != 0 -- отрицанием. Семантику самого strn_cmp не трогал: одиннадцать вызовов передают strlen() от того, что ввёл игрок, -- это сопоставление сокращений, и среди них CMD_IS, то есть диспетчер команд. Считай третий аргумент символами, они бы запрашивали вдвое больше символов, чем в строке, и сокращения перестали бы совпадать. --- src/engine/olc/olc.cpp | 8 ++++---- src/engine/scripting/dg_db_scripts.cpp | 20 ++++++++++---------- src/engine/scripting/dg_scripts.cpp | 2 +- src/engine/scripting/trigger_indenter.cpp | 20 ++++++++++---------- src/engine/ui/cmd/do_give.cpp | 2 +- src/engine/ui/cmd/do_put.cpp | 2 +- src/engine/ui/cmd_god/do_set.cpp | 4 ++-- src/gameplay/communication/parcel.cpp | 2 +- src/utils/utils_string.cpp | 20 ++++++++++++++++++++ src/utils/utils_string.h | 12 ++++++++++++ 10 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/engine/olc/olc.cpp b/src/engine/olc/olc.cpp index c7bbfc076..86015b746 100644 --- a/src/engine/olc/olc.cpp +++ b/src/engine/olc/olc.cpp @@ -185,11 +185,11 @@ void do_olc(CharData *ch, char *argument, int cmd, int subcmd) { return; } } else if (!a_isdigit(*buf1)) { - if (strn_cmp("save", buf1, 4) == 0 - || (lock = !strn_cmp("lock", buf1, 4)) == true - || (unlock = !strn_cmp("unlock", buf1, 6)) == true) { + if (starts_with(buf1, "save") + || (lock = starts_with(buf1, "lock")) == true + || (unlock = starts_with(buf1, "unlock")) == true) { // issue #3582: "save all" -- записать на диск все зоны данного типа. - if (strn_cmp("save", buf1, 4) == 0 && *buf2 && !str_cmp(buf2, "all")) { + if (starts_with(buf1, "save") && *buf2 && !str_cmp(buf2, "all")) { olc_save_all(ch, subcmd); return; } diff --git a/src/engine/scripting/dg_db_scripts.cpp b/src/engine/scripting/dg_db_scripts.cpp index 069160cac..675129ca7 100644 --- a/src/engine/scripting/dg_db_scripts.cpp +++ b/src/engine/scripting/dg_db_scripts.cpp @@ -162,28 +162,28 @@ char *dirty_indent_trigger(char *cmd, int *level) { skip_spaces(&ptr); // ptr содержит строку без первых пробелов. - if (!strn_cmp("case ", ptr, 5) || !strn_cmp("default", ptr, 7)) { + if (starts_with(ptr, "case ") || starts_with(ptr, "default")) { // последовательные case (или default после case) без break if (!indent_stack.empty() - && !strn_cmp("case ", indent_stack.top().c_str(), 5)) { + && starts_with(indent_stack.top().c_str(), "case ")) { --currlev; } else { indent_stack.push(ptr); } nextlev = currlev + 1; - } else if (!strn_cmp("if ", ptr, 3) || !strn_cmp("while ", ptr, 6) - || !strn_cmp("foreach ", ptr, 8) || !strn_cmp("switch ", ptr, 7)) { + } else if (starts_with(ptr, "if ") || starts_with(ptr, "while ") + || starts_with(ptr, "foreach ") || starts_with(ptr, "switch ")) { ++nextlev; indent_stack.push(ptr); - } else if (!strn_cmp("elseif ", ptr, 7) || !strn_cmp("else", ptr, 4)) { + } else if (starts_with(ptr, "elseif ") || starts_with(ptr, "else")) { --currlev; - } else if (!strn_cmp("break", ptr, 5) || !strn_cmp("end", ptr, 3) - || !strn_cmp("done", ptr, 4)) { + } else if (starts_with(ptr, "break") || starts_with(ptr, "end") + || starts_with(ptr, "done")) { // в switch завершающий break можно опускать и сразу писать done|end - if ((!strn_cmp("done", ptr, 4) || !strn_cmp("end", ptr, 3)) + if ((starts_with(ptr, "done") || starts_with(ptr, "end")) && !indent_stack.empty() - && (!strn_cmp("case ", indent_stack.top().c_str(), 5) - || !strn_cmp("default", indent_stack.top().c_str(), 7))) { + && (starts_with(indent_stack.top().c_str(), "case ") + || starts_with(indent_stack.top().c_str(), "default"))) { --currlev; --nextlev; indent_stack.pop(); diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index cf6b4b79c..178428c09 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -4762,7 +4762,7 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el if (!*arg) { snprintf(buf2, sizeof(buf2), "wait w/o an arg: '%s'", cl->cmd.c_str()); trig_log(trig, buf2); - } else if (!strn_cmp(arg, "until ", 6)) // valid forms of time are 14:30 and 1430 + } else if (starts_with(arg, "until ")) // valid forms of time are 14:30 and 1430 { if (sscanf(arg, "until %ld:%ld", &hr, &min) == 2) min += (hr * 60); diff --git a/src/engine/scripting/trigger_indenter.cpp b/src/engine/scripting/trigger_indenter.cpp index a055e352e..7c416c536 100644 --- a/src/engine/scripting/trigger_indenter.cpp +++ b/src/engine/scripting/trigger_indenter.cpp @@ -24,26 +24,26 @@ char *TriggerIndenter::indent(char *cmd, int *level) { char *ptr = cmd; skip_spaces(&ptr); - if (!strn_cmp("case ", ptr, 5) || !strn_cmp("default", ptr, 7)) { + if (starts_with(ptr, "case ") || starts_with(ptr, "default")) { if (!indent_stack_.empty() - && !strn_cmp("case ", indent_stack_.top().c_str(), 5)) { + && starts_with(indent_stack_.top().c_str(), "case ")) { --currlev; } else { indent_stack_.push(ptr); } nextlev = currlev + 1; - } else if (!strn_cmp("if ", ptr, 3) || !strn_cmp("while ", ptr, 6) - || !strn_cmp("foreach ", ptr, 8) || !strn_cmp("switch ", ptr, 7)) { + } else if (starts_with(ptr, "if ") || starts_with(ptr, "while ") + || starts_with(ptr, "foreach ") || starts_with(ptr, "switch ")) { ++nextlev; indent_stack_.push(ptr); - } else if (!strn_cmp("elseif ", ptr, 7) || !strn_cmp("else", ptr, 4)) { + } else if (starts_with(ptr, "elseif ") || starts_with(ptr, "else")) { --currlev; - } else if (!strn_cmp("break", ptr, 5) || !strn_cmp("end", ptr, 3) - || !strn_cmp("done", ptr, 4)) { - if ((!strn_cmp("done", ptr, 4) || !strn_cmp("end", ptr, 3)) + } else if (starts_with(ptr, "break") || starts_with(ptr, "end") + || starts_with(ptr, "done")) { + if ((starts_with(ptr, "done") || starts_with(ptr, "end")) && !indent_stack_.empty() - && (!strn_cmp("case ", indent_stack_.top().c_str(), 5) - || !strn_cmp("default", indent_stack_.top().c_str(), 7))) { + && (starts_with(indent_stack_.top().c_str(), "case ") + || starts_with(indent_stack_.top().c_str(), "default"))) { --currlev; --nextlev; indent_stack_.pop(); diff --git a/src/engine/ui/cmd/do_give.cpp b/src/engine/ui/cmd/do_give.cpp index 46da3faee..a8fcac0f2 100644 --- a/src/engine/ui/cmd/do_give.cpp +++ b/src/engine/ui/cmd/do_give.cpp @@ -173,7 +173,7 @@ void do_give(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { else if (is_number(arg)) { auto amount = std::stoi(arg); argument = one_argument(argument, arg); - if (!strn_cmp("coin", arg, 4) || !strn_cmp("кун", arg, 3) || !str_cmp("денег", arg)) { + if (starts_with(arg, "coin") || starts_with(arg, "кун") || !str_cmp("денег", arg)) { one_argument(argument, arg); if ((vict = give_find_vict(ch, arg)) != nullptr) perform_give_gold(ch, vict, amount); diff --git a/src/engine/ui/cmd/do_put.cpp b/src/engine/ui/cmd/do_put.cpp index 13fb5ba72..f892832ed 100644 --- a/src/engine/ui/cmd/do_put.cpp +++ b/src/engine/ui/cmd/do_put.cpp @@ -158,7 +158,7 @@ void do_put(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { else if (isname(theplace, "экипировка equipment")) where_bits = EFind::kObjEquip; - if (theobj && (!strn_cmp("coin", theobj, 4) || !strn_cmp("кун", theobj, strlen("кун")))) { + if (theobj && (starts_with(theobj, "coin") || starts_with(theobj, "кун"))) { money_mode = true; if (howmany <= 0) { SendMsgToChar("Следует указать чиста конкретную сумму.\r\n", ch); diff --git a/src/engine/ui/cmd_god/do_set.cpp b/src/engine/ui/cmd_god/do_set.cpp index 2f638f812..36fd2c53e 100644 --- a/src/engine/ui/cmd_god/do_set.cpp +++ b/src/engine/ui/cmd_god/do_set.cpp @@ -305,9 +305,9 @@ int PerformSet(CharData *ch, CharData *vict, int mode, char *val_arg) { // Find the value of the argument bool on_off_mode{false}; if (set_fields[mode].type == ESetValue::kBinary) { - if (!strn_cmp(val_arg, "on", 2) || !strn_cmp(val_arg, "yes", 3) || !strn_cmp(val_arg, "вкл", strlen("вкл"))) { + if (starts_with(val_arg, "on") || starts_with(val_arg, "yes") || starts_with(val_arg, "вкл")) { on_off_mode = true; - } else if (!strn_cmp(val_arg, "off", 3) || !strn_cmp(val_arg, "no", 2) || !strn_cmp(val_arg, "выкл", strlen("выкл"))) { + } else if (starts_with(val_arg, "off") || starts_with(val_arg, "no") || starts_with(val_arg, "выкл")) { on_off_mode = false; } else { SendMsgToChar("Значение может быть 'on' или 'off'.\r\n", ch); diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 4d13e4f93..9b7a4c151 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -291,7 +291,7 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (is_number(tmp_arg)) { int amount = atoi(tmp_arg); - if (!strn_cmp("coin", tmp_arg2, 4) || !strn_cmp("кун", tmp_arg2, 5) || !str_cmp("денег", tmp_arg2)) { + if (starts_with(tmp_arg2, "coin") || starts_with(tmp_arg2, "кун") || !str_cmp("денег", tmp_arg2)) { act("$n сказал$g вам : 'Для перевода денег воспользуйтесь услугами банка.'", false, mailman, diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index c53e728cb..6f9136709 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -1,5 +1,7 @@ //#include "utils_string.h" +#include + #include "utils.h" #include "utils/native_text.h" #include "utils/utils_encoding.h" @@ -573,6 +575,24 @@ char *delete_doubledollar(char *string) { // The str_cmp/strn_cmp family folds case per *character*: under KOI8-R that is the original // byte-wise LOWER() loop kept verbatim below, under UTF-8 it is native_text's code-point fold // (issue #3681). The KOI8-R path is untouched so behaviour is bit-identical until the flip. +bool starts_with(const char *str, const char *prefix) { + if (str == nullptr || prefix == nullptr) { + return false; + } + return 0 == strn_cmp(str, prefix, std::strlen(prefix)); +} + +bool starts_with(const std::string &str, const char *prefix) { + return starts_with(str.c_str(), prefix); +} + +std::size_t skip_prefix(const char *str, const char *prefix) { + if (!starts_with(str, prefix)) { + return 0; + } + return std::strlen(prefix); +} + int str_cmp(const char *arg1, const char *arg2) { if (arg1 == nullptr || arg2 == nullptr) { log("SYSERR: str_cmp() passed a nullptr pointer, %p or %p.", arg1, arg2); diff --git a/src/utils/utils_string.h b/src/utils/utils_string.h index e0285367c..eb41a121c 100644 --- a/src/utils/utils_string.h +++ b/src/utils/utils_string.h @@ -333,6 +333,18 @@ int strn_cmp(const std::string &arg1, const char *arg2, size_t n); int strn_cmp(const char *arg1, const std::string &arg2, size_t n); int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n); +/// Начинается ли строка с указанного префикса (без учёта регистра). +/// Длину префикса функция берёт из него самого, поэтому задать её неверно негде -- +/// в отличие от strn_cmp, где её писали числом. С UTF-8 такие числа разъехались с +/// длиной русских литералов: strn_cmp("кун", arg, 3) сравнивает полтора символа и +/// совпадает с «куртка» (issue #3681). +bool starts_with(const char *str, const char *prefix); +bool starts_with(const std::string &str, const char *prefix); + +/// Сколько байт префикса совпало (0 -- не совпало). Нужно там, где префикс после +/// сравнения ещё и отрезают: длина берётся из одного места и разъехаться не может. +std::size_t skip_prefix(const char *str, const char *prefix); + /// Удаление завершающих \r\n из C-строки. /// Дубль: utils::TrimRight - похожий функционал но для пробелов. void PruneCrlf(char *txt); From ce3b834d80eb06dfa387a12c8865008351cc91f7 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 16:01:17 +0200 Subject: [PATCH 086/110] =?UTF-8?q?fix(observability):=20=D1=85=D0=B2?= =?UTF-8?q?=D0=BE=D1=81=D1=82=20=D0=B2=D1=8B=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B5=20=D0=B4=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=20=D0=B4=D0=BE=20Loki=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OtelProvider::Shutdown() стоял в начале stop_game, а он не только дожимает буфер, но и снимает OTLP-отправителя, оставляя один файловый. Всё, что логируется дальше -- сохранение ренты, сброс очереди зон, "Closing all sockets", "Rebooting" -- уходило в никуда. Видно по боевому потоку: за неделю ни одной строки Reboot saving, Closing all sockets или Rebooting, при том что в файловом сислоге они есть. То есть в графане не видно ровно того, что нужно при разборе выключения. Гасим телеметрию последней -- перед exit(52) и перед нормальным возвратом. --- src/engine/core/comm.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/engine/core/comm.cpp b/src/engine/core/comm.cpp index 377ffb71f..0567a84ed 100644 --- a/src/engine/core/comm.cpp +++ b/src/engine/core/comm.cpp @@ -923,9 +923,6 @@ void stop_game(ush_int port) { } #endif - // Shutdown OTEL providers to flush remaining telemetry - observability::OtelProvider::Instance().Shutdown(); - FlushPlayerIndex(); // храны надо сейвить до Crash_save_all_rent(), иначе будем брать бабло у чара при записи @@ -1020,9 +1017,15 @@ void stop_game(ush_int port) { } if (shutdown_parameters.reboot_after_shutdown()) { log("Rebooting."); + // Гасим телеметрию последней: Shutdown() дожимает буфер в коллектор и переводит + // вывод на файл. Стояла она раньше в начале stop_game -- и весь хвост выключения + // (сохранение ренты, сброс очереди зон, "Closing all sockets", "Rebooting") уходил + // в никуда: в файловом сислоге он есть, а в Loki этих строк не было вовсе. + observability::OtelProvider::Instance().Shutdown(); exit(52); // what's so great about HHGTTG, anyhow? } log("Normal termination of game."); + observability::OtelProvider::Instance().Shutdown(); // см. комментарий выше } /* From 7a2fd9dcf1440982416432a264bbb037e0278528 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 16:01:17 +0200 Subject: [PATCH 087/110] =?UTF-8?q?refactor(utf8):=20=D1=81=D0=B2=D0=BE?= =?UTF-8?q?=D0=B9=20starts=5Fwith=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D1=91?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=B0=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20IsAbbr=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В прошлом коммите я завёл starts_with, не заметив, что в проекте уже есть utils::IsAbbr(abbr, text) -- ровно эта проверка, посимвольная, и её зовут из 39 файлов. Второе имя для того же понятия хуже исходной болячки, поэтому свой хелпер убран, все 54 вызова переведены на IsAbbr. Семантика та же: IsAbbr(P, X) -- это "X начинается с P". Порядок аргументов обратный, полярность сохранена. --- src/engine/olc/olc.cpp | 8 ++++---- src/engine/scripting/dg_db_scripts.cpp | 20 ++++++++++---------- src/engine/scripting/dg_scripts.cpp | 2 +- src/engine/scripting/trigger_indenter.cpp | 20 ++++++++++---------- src/engine/ui/cmd/do_give.cpp | 2 +- src/engine/ui/cmd/do_put.cpp | 2 +- src/engine/ui/cmd_god/do_set.cpp | 4 ++-- src/gameplay/communication/parcel.cpp | 2 +- src/utils/utils_string.cpp | 18 ------------------ src/utils/utils_string.h | 12 ------------ 10 files changed, 30 insertions(+), 60 deletions(-) diff --git a/src/engine/olc/olc.cpp b/src/engine/olc/olc.cpp index 86015b746..9709e56b6 100644 --- a/src/engine/olc/olc.cpp +++ b/src/engine/olc/olc.cpp @@ -185,11 +185,11 @@ void do_olc(CharData *ch, char *argument, int cmd, int subcmd) { return; } } else if (!a_isdigit(*buf1)) { - if (starts_with(buf1, "save") - || (lock = starts_with(buf1, "lock")) == true - || (unlock = starts_with(buf1, "unlock")) == true) { + if (utils::IsAbbr("save", buf1) + || (lock = utils::IsAbbr("lock", buf1)) == true + || (unlock = utils::IsAbbr("unlock", buf1)) == true) { // issue #3582: "save all" -- записать на диск все зоны данного типа. - if (starts_with(buf1, "save") && *buf2 && !str_cmp(buf2, "all")) { + if (utils::IsAbbr("save", buf1) && *buf2 && !str_cmp(buf2, "all")) { olc_save_all(ch, subcmd); return; } diff --git a/src/engine/scripting/dg_db_scripts.cpp b/src/engine/scripting/dg_db_scripts.cpp index 675129ca7..42711f084 100644 --- a/src/engine/scripting/dg_db_scripts.cpp +++ b/src/engine/scripting/dg_db_scripts.cpp @@ -162,28 +162,28 @@ char *dirty_indent_trigger(char *cmd, int *level) { skip_spaces(&ptr); // ptr содержит строку без первых пробелов. - if (starts_with(ptr, "case ") || starts_with(ptr, "default")) { + if (utils::IsAbbr("case ", ptr) || utils::IsAbbr("default", ptr)) { // последовательные case (или default после case) без break if (!indent_stack.empty() - && starts_with(indent_stack.top().c_str(), "case ")) { + && utils::IsAbbr("case ", indent_stack.top().c_str())) { --currlev; } else { indent_stack.push(ptr); } nextlev = currlev + 1; - } else if (starts_with(ptr, "if ") || starts_with(ptr, "while ") - || starts_with(ptr, "foreach ") || starts_with(ptr, "switch ")) { + } else if (utils::IsAbbr("if ", ptr) || utils::IsAbbr("while ", ptr) + || utils::IsAbbr("foreach ", ptr) || utils::IsAbbr("switch ", ptr)) { ++nextlev; indent_stack.push(ptr); - } else if (starts_with(ptr, "elseif ") || starts_with(ptr, "else")) { + } else if (utils::IsAbbr("elseif ", ptr) || utils::IsAbbr("else", ptr)) { --currlev; - } else if (starts_with(ptr, "break") || starts_with(ptr, "end") - || starts_with(ptr, "done")) { + } else if (utils::IsAbbr("break", ptr) || utils::IsAbbr("end", ptr) + || utils::IsAbbr("done", ptr)) { // в switch завершающий break можно опускать и сразу писать done|end - if ((starts_with(ptr, "done") || starts_with(ptr, "end")) + if ((utils::IsAbbr("done", ptr) || utils::IsAbbr("end", ptr)) && !indent_stack.empty() - && (starts_with(indent_stack.top().c_str(), "case ") - || starts_with(indent_stack.top().c_str(), "default"))) { + && (utils::IsAbbr("case ", indent_stack.top().c_str()) + || utils::IsAbbr("default", indent_stack.top().c_str()))) { --currlev; --nextlev; indent_stack.pop(); diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 178428c09..745ae18a7 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -4762,7 +4762,7 @@ void process_wait(void *go, Trigger *trig, int type, char *cmd, const cmdlist_el if (!*arg) { snprintf(buf2, sizeof(buf2), "wait w/o an arg: '%s'", cl->cmd.c_str()); trig_log(trig, buf2); - } else if (starts_with(arg, "until ")) // valid forms of time are 14:30 and 1430 + } else if (utils::IsAbbr("until ", arg)) // valid forms of time are 14:30 and 1430 { if (sscanf(arg, "until %ld:%ld", &hr, &min) == 2) min += (hr * 60); diff --git a/src/engine/scripting/trigger_indenter.cpp b/src/engine/scripting/trigger_indenter.cpp index 7c416c536..26f8aaa34 100644 --- a/src/engine/scripting/trigger_indenter.cpp +++ b/src/engine/scripting/trigger_indenter.cpp @@ -24,26 +24,26 @@ char *TriggerIndenter::indent(char *cmd, int *level) { char *ptr = cmd; skip_spaces(&ptr); - if (starts_with(ptr, "case ") || starts_with(ptr, "default")) { + if (utils::IsAbbr("case ", ptr) || utils::IsAbbr("default", ptr)) { if (!indent_stack_.empty() - && starts_with(indent_stack_.top().c_str(), "case ")) { + && utils::IsAbbr("case ", indent_stack_.top().c_str())) { --currlev; } else { indent_stack_.push(ptr); } nextlev = currlev + 1; - } else if (starts_with(ptr, "if ") || starts_with(ptr, "while ") - || starts_with(ptr, "foreach ") || starts_with(ptr, "switch ")) { + } else if (utils::IsAbbr("if ", ptr) || utils::IsAbbr("while ", ptr) + || utils::IsAbbr("foreach ", ptr) || utils::IsAbbr("switch ", ptr)) { ++nextlev; indent_stack_.push(ptr); - } else if (starts_with(ptr, "elseif ") || starts_with(ptr, "else")) { + } else if (utils::IsAbbr("elseif ", ptr) || utils::IsAbbr("else", ptr)) { --currlev; - } else if (starts_with(ptr, "break") || starts_with(ptr, "end") - || starts_with(ptr, "done")) { - if ((starts_with(ptr, "done") || starts_with(ptr, "end")) + } else if (utils::IsAbbr("break", ptr) || utils::IsAbbr("end", ptr) + || utils::IsAbbr("done", ptr)) { + if ((utils::IsAbbr("done", ptr) || utils::IsAbbr("end", ptr)) && !indent_stack_.empty() - && (starts_with(indent_stack_.top().c_str(), "case ") - || starts_with(indent_stack_.top().c_str(), "default"))) { + && (utils::IsAbbr("case ", indent_stack_.top().c_str()) + || utils::IsAbbr("default", indent_stack_.top().c_str()))) { --currlev; --nextlev; indent_stack_.pop(); diff --git a/src/engine/ui/cmd/do_give.cpp b/src/engine/ui/cmd/do_give.cpp index a8fcac0f2..4237c9f71 100644 --- a/src/engine/ui/cmd/do_give.cpp +++ b/src/engine/ui/cmd/do_give.cpp @@ -173,7 +173,7 @@ void do_give(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { else if (is_number(arg)) { auto amount = std::stoi(arg); argument = one_argument(argument, arg); - if (starts_with(arg, "coin") || starts_with(arg, "кун") || !str_cmp("денег", arg)) { + if (utils::IsAbbr("coin", arg) || utils::IsAbbr("кун", arg) || !str_cmp("денег", arg)) { one_argument(argument, arg); if ((vict = give_find_vict(ch, arg)) != nullptr) perform_give_gold(ch, vict, amount); diff --git a/src/engine/ui/cmd/do_put.cpp b/src/engine/ui/cmd/do_put.cpp index f892832ed..263929656 100644 --- a/src/engine/ui/cmd/do_put.cpp +++ b/src/engine/ui/cmd/do_put.cpp @@ -158,7 +158,7 @@ void do_put(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { else if (isname(theplace, "экипировка equipment")) where_bits = EFind::kObjEquip; - if (theobj && (starts_with(theobj, "coin") || starts_with(theobj, "кун"))) { + if (theobj && (utils::IsAbbr("coin", theobj) || utils::IsAbbr("кун", theobj))) { money_mode = true; if (howmany <= 0) { SendMsgToChar("Следует указать чиста конкретную сумму.\r\n", ch); diff --git a/src/engine/ui/cmd_god/do_set.cpp b/src/engine/ui/cmd_god/do_set.cpp index 36fd2c53e..7c0e64819 100644 --- a/src/engine/ui/cmd_god/do_set.cpp +++ b/src/engine/ui/cmd_god/do_set.cpp @@ -305,9 +305,9 @@ int PerformSet(CharData *ch, CharData *vict, int mode, char *val_arg) { // Find the value of the argument bool on_off_mode{false}; if (set_fields[mode].type == ESetValue::kBinary) { - if (starts_with(val_arg, "on") || starts_with(val_arg, "yes") || starts_with(val_arg, "вкл")) { + if (utils::IsAbbr("on", val_arg) || utils::IsAbbr("yes", val_arg) || utils::IsAbbr("вкл", val_arg)) { on_off_mode = true; - } else if (starts_with(val_arg, "off") || starts_with(val_arg, "no") || starts_with(val_arg, "выкл")) { + } else if (utils::IsAbbr("off", val_arg) || utils::IsAbbr("no", val_arg) || utils::IsAbbr("выкл", val_arg)) { on_off_mode = false; } else { SendMsgToChar("Значение может быть 'on' или 'off'.\r\n", ch); diff --git a/src/gameplay/communication/parcel.cpp b/src/gameplay/communication/parcel.cpp index 9b7a4c151..8293d922a 100644 --- a/src/gameplay/communication/parcel.cpp +++ b/src/gameplay/communication/parcel.cpp @@ -291,7 +291,7 @@ void send(CharData *ch, CharData *mailman, long vict_uid, char *arg) { if (is_number(tmp_arg)) { int amount = atoi(tmp_arg); - if (starts_with(tmp_arg2, "coin") || starts_with(tmp_arg2, "кун") || !str_cmp("денег", tmp_arg2)) { + if (utils::IsAbbr("coin", tmp_arg2) || utils::IsAbbr("кун", tmp_arg2) || !str_cmp("денег", tmp_arg2)) { act("$n сказал$g вам : 'Для перевода денег воспользуйтесь услугами банка.'", false, mailman, diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 6f9136709..3a50404d1 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -575,24 +575,6 @@ char *delete_doubledollar(char *string) { // The str_cmp/strn_cmp family folds case per *character*: under KOI8-R that is the original // byte-wise LOWER() loop kept verbatim below, under UTF-8 it is native_text's code-point fold // (issue #3681). The KOI8-R path is untouched so behaviour is bit-identical until the flip. -bool starts_with(const char *str, const char *prefix) { - if (str == nullptr || prefix == nullptr) { - return false; - } - return 0 == strn_cmp(str, prefix, std::strlen(prefix)); -} - -bool starts_with(const std::string &str, const char *prefix) { - return starts_with(str.c_str(), prefix); -} - -std::size_t skip_prefix(const char *str, const char *prefix) { - if (!starts_with(str, prefix)) { - return 0; - } - return std::strlen(prefix); -} - int str_cmp(const char *arg1, const char *arg2) { if (arg1 == nullptr || arg2 == nullptr) { log("SYSERR: str_cmp() passed a nullptr pointer, %p or %p.", arg1, arg2); diff --git a/src/utils/utils_string.h b/src/utils/utils_string.h index eb41a121c..e0285367c 100644 --- a/src/utils/utils_string.h +++ b/src/utils/utils_string.h @@ -333,18 +333,6 @@ int strn_cmp(const std::string &arg1, const char *arg2, size_t n); int strn_cmp(const char *arg1, const std::string &arg2, size_t n); int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n); -/// Начинается ли строка с указанного префикса (без учёта регистра). -/// Длину префикса функция берёт из него самого, поэтому задать её неверно негде -- -/// в отличие от strn_cmp, где её писали числом. С UTF-8 такие числа разъехались с -/// длиной русских литералов: strn_cmp("кун", arg, 3) сравнивает полтора символа и -/// совпадает с «куртка» (issue #3681). -bool starts_with(const char *str, const char *prefix); -bool starts_with(const std::string &str, const char *prefix); - -/// Сколько байт префикса совпало (0 -- не совпало). Нужно там, где префикс после -/// сравнения ещё и отрезают: длина берётся из одного места и разъехаться не может. -std::size_t skip_prefix(const char *str, const char *prefix); - /// Удаление завершающих \r\n из C-строки. /// Дубль: utils::TrimRight - похожий функционал но для пробелов. void PruneCrlf(char *txt); From 066344934916acb5267c61e741a48f1afce0735f Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 16:15:56 +0200 Subject: [PATCH 088/110] =?UTF-8?q?refactor(utf8):=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D1=88=D0=B8=D0=B5=D1=81=D1=8F=20=D1=81=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B5=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D0=BE=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=20IsAbbr=20(#3681?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тридцать три вызова strn_cmp были той же проверкой "одно есть префикс другого", только длину для неё считали руками: strlen(arg), заведённая рядом переменная i, agr_length, len, l. Это ровно utils::IsAbbr, и она посимвольная. Заодно ушли семь переменных, существовавших только ради этой длины, вместе с приёмом "l = 1, чтобы пустая строка не совпала с первой попавшейся": IsAbbr на пустом префиксе и так возвращает false. Отдельно поправлено сравнение падежа в login.cpp: там первые kMinNameLength байт сверялись с именем, а под UTF-8 пятёрка -- это две с половиной русские буквы. Теперь отсчёт по символам. От strn_cmp осталось два вызова, и оба -- не префикс, а минимум двух длин: сверка падежа (login.cpp) и поиск по таблице игроков (player_index.cpp). --- src/engine/core/target_resolver.cpp | 2 +- src/engine/scripting/dg_scripts.cpp | 4 ++-- src/engine/scripting/dg_triggers.cpp | 2 +- src/engine/structs/flag_data.cpp | 6 +----- src/engine/ui/cmd/do_affects.cpp | 5 ++--- src/engine/ui/cmd/do_create.cpp | 13 ++++++------- src/engine/ui/cmd/do_forget.cpp | 11 +++++------ src/engine/ui/interpreter.h | 2 +- src/engine/ui/login.cpp | 6 +++++- src/gameplay/ai/subcmd_resolver.cpp | 2 +- src/gameplay/communication/social.cpp | 3 +-- src/gameplay/crafting/im.cpp | 4 ++-- src/utils/utils_parse.cpp | 11 ++--------- 13 files changed, 30 insertions(+), 41 deletions(-) diff --git a/src/engine/core/target_resolver.cpp b/src/engine/core/target_resolver.cpp index 5bf4106bf..ed57c16d1 100644 --- a/src/engine/core/target_resolver.cpp +++ b/src/engine/core/target_resolver.cpp @@ -877,7 +877,7 @@ int find_all_dots(char *arg) { // жёсткая четвёрка отрезала полторы буквы, оставляя "е.<имя>" (issue #3681). for (const char *prefix : {"all.", "все."}) { const size_t prefix_len = strlen(prefix); - if (!strn_cmp(arg, prefix, prefix_len)) { + if (utils::IsAbbr(prefix, arg)) { strl_cpy(tmpname, arg + prefix_len, kMaxInputLength); strl_cpy(arg, tmpname, kMaxInputLength); return (kFindAlldot); diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 745ae18a7..7a4254ed8 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -3144,7 +3144,7 @@ void find_replacement(void *go, int num; int sum = 0; for (num = 0; num < EApply::kNumberApplies; num++) { - if (!strn_cmp(subfield, apply_types[num], strlen(subfield))) + if (utils::IsAbbr(subfield, apply_types[num])) break; } if (num == EApply::kNumberApplies) { @@ -4393,7 +4393,7 @@ int eval_lhs_op_rhs(const char *expr, char *result, size_t result_size, void *go for (i = 0; *ops[i] != '\n'; i++) for (j = 0; tokens[j]; j++) - if (!strn_cmp(ops[i], tokens[j], strlen(ops[i]))) { + if (utils::IsAbbr(ops[i], tokens[j])) { *tokens[j] = '\0'; p = tokens[j] + strlen(ops[i]); diff --git a/src/engine/scripting/dg_triggers.cpp b/src/engine/scripting/dg_triggers.cpp index 88cfb6cc3..afdfc646d 100644 --- a/src/engine/scripting/dg_triggers.cpp +++ b/src/engine/scripting/dg_triggers.cpp @@ -1013,7 +1013,7 @@ int cmd_otrig(ObjData *obj, CharData *actor, char *cmd, const char *argument, in if (IS_SET(GET_TRIG_NARG(t), type) && (t->arglist[0] == '*' - || 0 == strn_cmp(t->arglist.c_str(), cmd, t->arglist.size()))) { + || utils::IsAbbr(t->arglist.c_str(), cmd))) { if (!actor->IsNpc() && (actor->GetPosition() == EPosition::kSleep)) // command триггер не будет срабатывать если игрок спит { diff --git a/src/engine/structs/flag_data.cpp b/src/engine/structs/flag_data.cpp index 76fb575de..4b56fa310 100644 --- a/src/engine/structs/flag_data.cpp +++ b/src/engine/structs/flag_data.cpp @@ -32,10 +32,6 @@ int ext_search_block(const char *arg, const char *const *const list, int exact) } } } else { - size_t l = strlen(arg); - if (!l) { - l = 1; // Avoid "" to match the first available string - } for (i = j = 0, o = 1; j != 1; i++) { if (**(list + i) == '\n') { o = 1; @@ -50,7 +46,7 @@ int ext_search_block(const char *arg, const char *const *const list, int exact) break; } } else { - if (!strn_cmp(arg, *(list + i), l)) { + if (utils::IsAbbr(arg, *(list + i))) { return j | o; } else { o <<= 1; diff --git a/src/engine/ui/cmd/do_affects.cpp b/src/engine/ui/cmd/do_affects.cpp index b3b992165..3a3696e3e 100644 --- a/src/engine/ui/cmd/do_affects.cpp +++ b/src/engine/ui/cmd/do_affects.cpp @@ -57,9 +57,8 @@ struct SquashRow { void do_affects(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { char sp_name[kMaxStringLength]; - const size_t agr_length = strlen(argument); - if (*argument && !strn_cmp(argument, "краткий", agr_length)) { + if (*argument && utils::IsAbbr(argument, "краткий")) { if (!ch->get_master()) { group::print_one_line(ch, ch, true, 0); } else { @@ -71,7 +70,7 @@ void do_affects(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { // issue.affects-squash: "аффекты все" forces the full, un-collapsed per-source list. Mortals // otherwise get one row per distinct effect (duplicate sources collapsed, longest duration shown); // immortals always see every slot (with modifier/potency detail). - const bool show_all = (*argument && !strn_cmp(argument, "все", agr_length)); + const bool show_all = (*argument && utils::IsAbbr(argument, "все")); const bool squash = !privilege::IsImmortal(ch) && !show_all; // Show the bitset without "hiding" etc. diff --git a/src/engine/ui/cmd/do_create.cpp b/src/engine/ui/cmd/do_create.cpp index 717f020b5..08fd2135a 100644 --- a/src/engine/ui/cmd/do_create.cpp +++ b/src/engine/ui/cmd/do_create.cpp @@ -24,16 +24,15 @@ void do_create(CharData *ch, char *argument, int/* cmd*/, int subcmd) { return; } - size_t i = strlen(arg); ESpellType itemnum; - if (!strn_cmp(arg, "potion", i) || !strn_cmp(arg, "напиток", i)) + if (utils::IsAbbr(arg, "potion") || utils::IsAbbr(arg, "напиток")) itemnum = ESpellType::kPotionCast; - else if (!strn_cmp(arg, "wand", i) || !strn_cmp(arg, "палочка", i)) + else if (utils::IsAbbr(arg, "wand") || utils::IsAbbr(arg, "палочка")) itemnum = ESpellType::kWandCast; - else if (!strn_cmp(arg, "scroll", i) || !strn_cmp(arg, "свиток", i)) + else if (utils::IsAbbr(arg, "scroll") || utils::IsAbbr(arg, "свиток")) itemnum = ESpellType::kScrollCast; - else if (!strn_cmp(arg, "recipe", i) || !strn_cmp(arg, "рецепт", i) || - !strn_cmp(arg, "отвар", i)) { + else if (utils::IsAbbr(arg, "recipe") || utils::IsAbbr(arg, "рецепт") || + utils::IsAbbr(arg, "отвар")) { if (subcmd != SCMD_RECIPE) { SendMsgToChar("Магическую смесь необходимо СМЕШАТЬ.\r\n", ch); return; @@ -41,7 +40,7 @@ void do_create(CharData *ch, char *argument, int/* cmd*/, int subcmd) { // itemnum = SPELL_ITEMS; compose_recipe(ch, argument, 0); return; - } else if (!strn_cmp(arg, "runes", i) || !strn_cmp(arg, "руны", i)) { + } else if (utils::IsAbbr(arg, "runes") || utils::IsAbbr(arg, "руны")) { if (subcmd != SCMD_RECIPE) { SendMsgToChar("Руны требуется сложить.\r\n", ch); return; diff --git a/src/engine/ui/cmd/do_forget.cpp b/src/engine/ui/cmd/do_forget.cpp index 5dfb90523..cc91b9da5 100644 --- a/src/engine/ui/cmd/do_forget.cpp +++ b/src/engine/ui/cmd/do_forget.cpp @@ -13,8 +13,8 @@ inline bool in_mem(char *arg) { return (strlen(arg) != 0) && - (!strn_cmp("часослов", arg, strlen(arg)) || - !strn_cmp("резы", arg, strlen(arg)) || !strn_cmp("book", arg, strlen(arg))); + (utils::IsAbbr(arg, "часослов") || + utils::IsAbbr(arg, "резы") || utils::IsAbbr(arg, "book")); } void do_forget(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { @@ -28,14 +28,13 @@ void do_forget(CharData *ch, char *argument, int/* cmd*/, int/* subcmd*/) { return; } - size_t i = strlen(arg); - if (!strn_cmp(arg, "recipe", i) || !strn_cmp(arg, "рецепт", i) || - !strn_cmp(arg, "отвар", i)) { + if (utils::IsAbbr(arg, "recipe") || utils::IsAbbr(arg, "рецепт") || + utils::IsAbbr(arg, "отвар")) { forget_recipe(ch, argument, 0); return; } - if (!strn_cmp(arg, "все", i) || !strn_cmp(arg, "all", i)) { + if (utils::IsAbbr(arg, "все") || utils::IsAbbr(arg, "all")) { char arg2[kMaxInputLength]; two_arguments(argument, arg, arg2); if (in_mem(arg2)) { diff --git a/src/engine/ui/interpreter.h b/src/engine/ui/interpreter.h index 99fd1ee67..2d1aa891f 100644 --- a/src/engine/ui/interpreter.h +++ b/src/engine/ui/interpreter.h @@ -26,7 +26,7 @@ class CharData; // to avoid inclusion of "char.hpp" void DoMove(CharData *ch, char *, int, int subcmd); -#define CMD_IS(cmd_name) (!strn_cmp(cmd_name, cmd_info[cmd].command, strlen(cmd_name))) +#define CMD_IS(cmd_name) (utils::IsAbbr(cmd_name, cmd_info[cmd].command)) void command_interpreter(CharData *ch, char *argument); // fill_word, half_chop moved to mud_string.h diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index 3d9cc10b7..b9f8beb51 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -1501,9 +1501,13 @@ static void HandleNameCase(DescriptorData *d, char *argument, int step) { if (!_parse_name(argument, tmp_name) && static_cast(native_text::char_count(tmp_name)) >= kMinNameLength && static_cast(native_text::char_count(tmp_name)) <= kMaxNameLength + // Падеж принимается, если первые kMinNameLength символов совпали с именем. Считать + // это в байтах нельзя: под UTF-8 пятёрка -- две с половиной русские буквы (#3681). && !strn_cmp(tmp_name, GET_PC_NAME(d->character), - std::min(kMinNameLength, strlen(GET_PC_NAME(d->character)) - 1))) { + native_text::char_offset(GET_PC_NAME(d->character), + std::min(kMinNameLength, + native_text::char_count(GET_PC_NAME(d->character)) - 1)))) { d->character->player_data.PNames[cur.ecase] = std::string(utils::CAP(tmp_name)); if (step < kLast) { const auto &next = kSteps[step + 1]; diff --git a/src/gameplay/ai/subcmd_resolver.cpp b/src/gameplay/ai/subcmd_resolver.cpp index 59656db55..7062ddc9e 100644 --- a/src/gameplay/ai/subcmd_resolver.cpp +++ b/src/gameplay/ai/subcmd_resolver.cpp @@ -48,7 +48,7 @@ const SubCmdResolver::Row *SubCmdResolver::Resolve(const char *word, bool &ambig const size_t len = std::strlen(word); for (const auto &row : rows_) { for (const auto &name : row.names) { - if (len <= name.size() && !strn_cmp(word, name.c_str(), len)) { + if (len <= name.size() && utils::IsAbbr(word, name.c_str())) { if (match && match != &row) { ambiguous = true; return nullptr; diff --git a/src/gameplay/communication/social.cpp b/src/gameplay/communication/social.cpp index d2ffc8c87..8cfa42e41 100644 --- a/src/gameplay/communication/social.cpp +++ b/src/gameplay/communication/social.cpp @@ -235,9 +235,8 @@ int find_action(char *cmd) { if (!cmd || !*cmd) { return -1; } - const std::size_t len = std::strlen(cmd); for (const auto &entry : g_social_index) { - if (strn_cmp(cmd, entry.first.c_str(), len) == 0) { + if (utils::IsAbbr(cmd, entry.first.c_str())) { return entry.second; } } diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index cf90fb5a2..ee4991cdd 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -68,7 +68,7 @@ int im_get_type_by_name(char *name, int mode) { for (i = 0; i <= top_imtypes; ++i) { if (mode == 0 && imtypes[i].proto_vnum == -1) continue; - if (!strn_cmp(name, imtypes[i].name, strlen(imtypes[i].name))) + if (utils::IsAbbr(imtypes[i].name, name)) return i; } return -1; @@ -1618,7 +1618,7 @@ void forget_recipe(CharData *ch, char *argument, int/* subcmd*/) { size_t i = strlen(name); for (rcpt = top_imrecipes; rcpt >= 0; --rcpt) { - if (!strn_cmp(name, imrecipes[rcpt].name, i)) { + if (utils::IsAbbr(name, imrecipes[rcpt].name)) { break; } } diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index 223b742c6..d3cca5cfc 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -423,7 +423,6 @@ char *fname(const char *namelist) { // issue.interpreter-cleaning: generic argument/token parsing helpers moved from interpreter.cpp. int search_block(const char *target_string, const char **list, int exact) { int i; - size_t l = strlen(target_string); if (exact) { for (i = 0; **(list + i) != '\n'; i++) { @@ -432,11 +431,8 @@ int search_block(const char *target_string, const char **list, int exact) { } } } else { - if (0 == l) { - l = 1; // Avoid "" to match the first available string - } for (i = 0; **(list + i) != '\n'; i++) { - if (!strn_cmp(target_string, *(list + i), l)) { + if (utils::IsAbbr(target_string, *(list + i))) { return i; } } @@ -447,17 +443,14 @@ int search_block(const char *target_string, const char **list, int exact) { int search_block(const std::string &block, const char **list, int exact) { int i; - std::string::size_type l = block.length(); if (exact) { for (i = 0; **(list + i) != '\n'; i++) if (!str_cmp(block, *(list + i))) return (i); } else { - if (!l) - l = 1; // Avoid "" to match the first available string for (i = 0; **(list + i) != '\n'; i++) - if (!strn_cmp(block, *(list + i), l)) + if (utils::IsAbbr(block, *(list + i))) return (i); } From 9f912ec90256455d1675f1919d471027e2ebebb9 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 18:39:20 +0200 Subject: [PATCH 089/110] =?UTF-8?q?refactor(utf8):=20strn=5Fcmp=20=D0=B8?= =?UTF-8?q?=20ncompare=5Fci=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B,?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA=D1=81=20=D1=81=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=B0=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Оба оставшихся вызова strn_cmp оказались одной задачей -- «совпадают ли первые N символов», -- и в обоих N приходил из kMinNameLength, то есть означал буквы, а работал как байты. Под UTF-8 пятёрка -- это две с половиной русские буквы: сверка падежа в login и поиск похожего имени в таблице игроков сравнивали вдвое меньше, чем задумано, и потому срабатывали слишком охотно. utils::IsSamePrefix(a, b, chars) считает символы. Если в какой-то из строк их меньше -- не совпадение, как и раньше. После этого strn_cmp остался без вызовов и удалён, а с ним и native_text:: ncompare_ci -- она существовала только под него и несла тот самый байтовый бюджет. Теперь длину в байтах не задаёт ни одна строковая функция проекта: префиксы -- IsAbbr, первые N символов -- IsSamePrefix, целиком -- str_cmp. Тест на байтовый бюджет заменён тестом на символьный. --- src/engine/db/player_index.cpp | 6 ++-- src/engine/ui/login.cpp | 8 ++--- src/gameplay/ai/subcmd_resolver.cpp | 2 +- src/gameplay/ai/subcmd_resolver.h | 2 +- src/utils/native_text.cpp | 4 --- src/utils/native_text.h | 4 +-- src/utils/utils.cpp | 2 +- src/utils/utils_parse.cpp | 2 +- src/utils/utils_string.cpp | 46 +++++++++++------------------ src/utils/utils_string.h | 12 ++++---- tests/native_text.cpp | 8 ----- tests/text_semantics.cpp | 17 ++++++++--- 12 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/engine/db/player_index.cpp b/src/engine/db/player_index.cpp index 43c1b1d22..0e15d3599 100644 --- a/src/engine/db/player_index.cpp +++ b/src/engine/db/player_index.cpp @@ -130,13 +130,15 @@ bool PlayersIndex::equal_to::operator()(const std::string &left, const std::stri bool IsPlayerExists(const long id) { return player_table.IsPlayerExists(id); } long CmpPtableByName(char *name, int len) { - len = std::min(len, static_cast(strlen(name))); + // len -- в символах: вызывающие передают kMinNameLength, то есть «столько букв». + len = std::min(len, static_cast(native_text::char_count(name))); one_argument(name, arg); /* Anton Gorev (2015/12/29): I am not sure but I guess that linear search is not the best solution here. * TODO: make map helper (MAPHELPER). */ for (std::size_t i = 0; i < player_table.size(); i++) { std::string_view pname = player_table[i].name(); - if (!strn_cmp(pname.data(), arg, std::min(len, static_cast(pname.length())))) { + if (utils::IsSamePrefix(pname.data(), arg, + std::min(len, native_text::char_count(pname)))) { return static_cast(i); } } diff --git a/src/engine/ui/login.cpp b/src/engine/ui/login.cpp index b9f8beb51..fee344916 100644 --- a/src/engine/ui/login.cpp +++ b/src/engine/ui/login.cpp @@ -1503,11 +1503,9 @@ static void HandleNameCase(DescriptorData *d, char *argument, int step) { && static_cast(native_text::char_count(tmp_name)) <= kMaxNameLength // Падеж принимается, если первые kMinNameLength символов совпали с именем. Считать // это в байтах нельзя: под UTF-8 пятёрка -- две с половиной русские буквы (#3681). - && !strn_cmp(tmp_name, - GET_PC_NAME(d->character), - native_text::char_offset(GET_PC_NAME(d->character), - std::min(kMinNameLength, - native_text::char_count(GET_PC_NAME(d->character)) - 1)))) { + && utils::IsSamePrefix(tmp_name, GET_PC_NAME(d->character), + std::min(kMinNameLength, + native_text::char_count(GET_PC_NAME(d->character)) - 1))) { d->character->player_data.PNames[cur.ecase] = std::string(utils::CAP(tmp_name)); if (step < kLast) { const auto &next = kSteps[step + 1]; diff --git a/src/gameplay/ai/subcmd_resolver.cpp b/src/gameplay/ai/subcmd_resolver.cpp index 7062ddc9e..800cda54a 100644 --- a/src/gameplay/ai/subcmd_resolver.cpp +++ b/src/gameplay/ai/subcmd_resolver.cpp @@ -5,7 +5,7 @@ #include "engine/core/comm.h" // SendMsgToChar #include "utils/mud_string.h" // one_argument #include "utils/utils.h" // skip_spaces, kMaxInputLength -#include "utils/utils_string.h" // str_cmp, strn_cmp +#include "utils/utils_string.h" // str_cmp, IsAbbr #include #include diff --git a/src/gameplay/ai/subcmd_resolver.h b/src/gameplay/ai/subcmd_resolver.h index 703185dc2..bd8e78d5b 100644 --- a/src/gameplay/ai/subcmd_resolver.h +++ b/src/gameplay/ai/subcmd_resolver.h @@ -15,7 +15,7 @@ class CharData; // row), so the hint can never drift from the actual subcommands. Strings live in code for now; later // they move to special_msg.xml and this table is built from there -- the API stays the same. // -// Lookup is currently a linear, case-insensitive scan (str_cmp/strn_cmp, KOI8-aware) -- correct and +// Lookup is currently a linear, case-insensitive scan (str_cmp/IsAbbr, KOI8-aware) -- correct and // cheap for the handful of subcommands a spec proc has; the same API can be backed by a shared // prefix-tree later (and reused for socials/command abbreviation). class SubCmdResolver { diff --git a/src/utils/native_text.cpp b/src/utils/native_text.cpp index 5f753685a..74b0f6c9b 100644 --- a/src/utils/native_text.cpp +++ b/src/utils/native_text.cpp @@ -113,7 +113,6 @@ namespace { // Shared driver for the two case-insensitive comparisons. `limit` caps how many bytes of `a` may // be consumed (npos = unlimited): once that budget is spent the strings count as equal, which is -// what the strn_cmp callers -- who pass a prefix length in bytes -- expect. int compare_folded(std::string_view a, std::string_view b, std::size_t limit) { std::size_t pa = 0; std::size_t pb = 0; @@ -150,9 +149,6 @@ int compare_ci(std::string_view a, std::string_view b) { return compare_folded(a, b, std::string_view::npos); } -int ncompare_ci(std::string_view a, std::string_view b, std::size_t n) { - return compare_folded(a, b, n); -} bool is_alnum_char(const char *s) { const unsigned char lead = static_cast(*s); diff --git a/src/utils/native_text.h b/src/utils/native_text.h index 15b9047e5..9fa3a69b4 100644 --- a/src/utils/native_text.h +++ b/src/utils/native_text.h @@ -63,10 +63,8 @@ char32_t first_char_code_upper(const char *s); // the shorter string orders first, returns the signed difference at the first mismatch (0 when // equal). KOI8-R: per byte, via LOWER() -- matches str_cmp/str/str semantics. UTF-8: per code // point, folded via utf8::to_lower (so the sign is meaningful; the magnitude is a code-point -// difference). In ncompare_ci, `n` is a byte budget -- the callers pass strlen()/length() -- and -// the comparison stops (as a match) once that many bytes of equal text have been consumed. +// difference). int compare_ci(std::string_view a, std::string_view b); -int ncompare_ci(std::string_view a, std::string_view b, std::size_t n); // Is the character starting at `s` alphanumeric? KOI8-R: the a_isalnum byte table. UTF-8: ASCII // letters/digits plus the Russian Cyrillic block -- so a multibyte letter is classified as one diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index c9c172754..945654d41 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -91,7 +91,7 @@ return result; -// str_cmp, strn_cmp moved to utils_string.cpp +// str_cmp moved to utils_string.cpp // the "touch" command, essentially. int touch(const char *path) { diff --git a/src/utils/utils_parse.cpp b/src/utils/utils_parse.cpp index d3cca5cfc..8aacfbbaa 100644 --- a/src/utils/utils_parse.cpp +++ b/src/utils/utils_parse.cpp @@ -10,7 +10,7 @@ #include "engine/db/obj_prototypes.h" #include "engine/db/db.h" #include "utils/utils.h" // a_isdigit -#include "utils/utils_string.h" // str_cmp/strn_cmp (search_block) +#include "utils/utils_string.h" // str_cmp/IsAbbr (search_block) //extern ObjRnum GetObjRnum(ObjVnum vnum) { return obj_proto.rnum(vnum); diff --git a/src/utils/utils_string.cpp b/src/utils/utils_string.cpp index 3a50404d1..b9ac3ebf5 100644 --- a/src/utils/utils_string.cpp +++ b/src/utils/utils_string.cpp @@ -114,6 +114,23 @@ void DelegatedStringWriter::clear() { m_delegated_string_ = nullptr; } +bool IsSamePrefix(const char *arg1, const char *arg2, std::size_t chars) { + if (arg1 == nullptr || arg2 == nullptr) { + return false; + } + for (std::size_t i = 0; i < chars; ++i) { + if (!*arg1 || !*arg2) { + return false; // символы кончились раньше, чем набралось chars + } + if (!native_text::chars_equal_ci(arg1, arg2)) { + return false; + } + arg1 += native_text::char_bytes(arg1); + arg2 += native_text::char_bytes(arg2); + } + return true; +} + bool IsAbbr(const char *arg1, const char *arg2) { if (!*arg1) { return false; @@ -572,7 +589,7 @@ char *delete_doubledollar(char *string) { } // Moved from utils.cpp -// The str_cmp/strn_cmp family folds case per *character*: under KOI8-R that is the original +// The str_cmp family folds case per *character*: under KOI8-R that is the original // byte-wise LOWER() loop kept verbatim below, under UTF-8 it is native_text's code-point fold // (issue #3681). The KOI8-R path is untouched so behaviour is bit-identical until the flip. int str_cmp(const char *arg1, const char *arg2) { @@ -603,33 +620,6 @@ int str_cmp(const std::string &arg1, const std::string &arg2) { return native_text::compare_ci(arg1, arg2); } -int strn_cmp(const char *arg1, const char *arg2, size_t n) { - if (arg1 == nullptr || arg2 == nullptr) { - log("SYSERR: strn_cmp() passed a NULL pointer, %p or %p.", arg1, arg2); - return (0); - } - return native_text::ncompare_ci(arg1, arg2, n); -} - -int strn_cmp(const std::string &arg1, const char *arg2, size_t n) { - if (arg2 == nullptr) { - log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg2); - return (0); - } - return native_text::ncompare_ci(arg1, arg2, n); -} - -int strn_cmp(const char *arg1, const std::string &arg2, size_t n) { - if (arg1 == nullptr) { - log("SYSERR: strn_cmp() passed a NULL pointer, %p.", arg1); - return (0); - } - return native_text::ncompare_ci(arg1, arg2, n); -} - -int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n) { - return native_text::ncompare_ci(arg1, arg2, n); -} void StringReplace(std::string &buffer, char s, const std::string &d) { for (size_t index = 0; index = buffer.find(s, index), index != std::string::npos;) { diff --git a/src/utils/utils_string.h b/src/utils/utils_string.h index e0285367c..1071f7f57 100644 --- a/src/utils/utils_string.h +++ b/src/utils/utils_string.h @@ -112,6 +112,11 @@ std::string GetStringWithoutColors(const std::string &string); bool IsEquivalent(const std::string &abbr, const std::string &words); // поиск abbr без пропусков слов в строке words bool IsEqual(const std::string &abbr, const std::string &words); +// Совпадают ли первые chars символов у обеих строк (без учёта регистра). Если символов +// в какой-то из строк меньше -- не совпадают. Единица счёта -- символ, а не байт: пределы +// вроде kMinNameLength означают буквы, и под UTF-8 байтовый счёт давал вдвое короче (#3681). +bool IsSamePrefix(const char *arg1, const char *arg2, std::size_t chars); + // arg1 аббревиатура в arg2 тексте\фразе bool IsAbbr(const char *arg1, const char *arg2); inline int IsAbbr(const std::string &arg1, const char *arg2) { return IsAbbr(arg1.c_str(), arg2); } @@ -326,13 +331,6 @@ int str_cmp(const std::string &arg1, const char *arg2); int str_cmp(const char *arg1, const std::string &arg2); int str_cmp(const std::string &arg1, const std::string &arg2); -/// Сравнение строк без учета регистра с ограничением длины (аналог strncmp). -/// Возвращает: 0 если равны, >0 если arg1 > arg2, <0 если arg1 < arg2. -int strn_cmp(const char *arg1, const char *arg2, size_t n); -int strn_cmp(const std::string &arg1, const char *arg2, size_t n); -int strn_cmp(const char *arg1, const std::string &arg2, size_t n); -int strn_cmp(const std::string &arg1, const std::string &arg2, size_t n); - /// Удаление завершающих \r\n из C-строки. /// Дубль: utils::TrimRight - похожий функционал но для пробелов. void PruneCrlf(char *txt); diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 13b197984..9cee59829 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -86,14 +86,6 @@ TEST(NativeText, CompareCiAscii) { EXPECT_LT(native_text::compare_ci("", "a"), 0); } -TEST(NativeText, NCompareCiAscii) { - EXPECT_EQ(native_text::ncompare_ci("abcdef", "abcXXX", 3), 0); // only first 3 bytes matter - EXPECT_NE(native_text::ncompare_ci("abcdef", "abXXXX", 3), 0); - EXPECT_EQ(native_text::ncompare_ci("ABC", "abc", 3), 0); - EXPECT_EQ(native_text::ncompare_ci("anything", "other", 0), 0); // zero budget: equal - EXPECT_LT(native_text::ncompare_ci("ab", "abc", 10), 0); // budget beyond the strings -} - TEST(NativeText, CompareCiCyrillicIsCaseInsensitive) { // "PRIVET" vs "privet" in Cyrillic: must compare equal in BOTH encodings -- under KOI8-R via // the byte table, under UTF-8 via the code-point fold. This is the property that a naive diff --git a/tests/text_semantics.cpp b/tests/text_semantics.cpp index 35aa6dd78..46fc257a6 100644 --- a/tests/text_semantics.cpp +++ b/tests/text_semantics.cpp @@ -85,10 +85,19 @@ TEST(TextSemantics, StrCmpOrdersConsistently) { EXPECT_GT(str_cmp("abc", "ab"), 0); } -TEST(TextSemantics, StrnCmpComparesPrefixOnly) { - EXPECT_EQ(strn_cmp("abcdef", "abcXXX", 3), 0); - EXPECT_NE(strn_cmp("abcdef", "abXXXX", 3), 0); - EXPECT_EQ(strn_cmp("МЕЧ", "меч", 6), 0); +TEST(TextSemantics, IsSamePrefixCountsCharactersNotBytes) { + EXPECT_TRUE(utils::IsSamePrefix("abcdef", "abcXXX", 3)); + EXPECT_FALSE(utils::IsSamePrefix("abcdef", "abXXXX", 3)); + + // Три буквы -- это три, а не шесть байт: пределы вроде kMinNameLength означают + // именно буквы, и байтовый счёт под UTF-8 давал вдвое короче (issue #3681). + EXPECT_TRUE(utils::IsSamePrefix("МЕЧ", "меч", 3)); + EXPECT_TRUE(utils::IsSamePrefix("мечник", "МЕЧТА", 3)); + EXPECT_FALSE(utils::IsSamePrefix("мечник", "МЕЧТА", 4)); + + // Строка короче запрошенного -- не совпадение, а не «совпало по остатку». + EXPECT_FALSE(utils::IsSamePrefix("ме", "меч", 3)); + EXPECT_TRUE(utils::IsSamePrefix("ме", "меч", 2)); } // ---------------------------------------------------------------------------- argument splitting From 6656b6ad493fd7001f8c6356ccfd02f0fe0a94d1 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 20 Aug 2026 19:14:56 +0200 Subject: [PATCH 090/110] =?UTF-8?q?test(utf8):=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8?= =?UTF-8?q?=20=D1=82=D0=BE,=20=D1=87=D1=82=D0=BE=20=D1=83=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=D0=BE=20=D0=B1=D0=BE=D0=B5=D0=B2=D0=BE=D0=B9?= =?UTF-8?q?=20(#3681,=20#3749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пароль. Хэш обязан считаться по дисковой форме -- на расхождении с байтами в памяти и отвалился вход у всех, чей пароль с кириллицей. Соль случайная, поэтому свойство проверяется через следствие: длинное тире и дефис на диске неразличимы (в KOI8-R тире нет), значит пароли с ними обязаны совпасть. Без фикса этот тест падает, остальные три -- страховка на будущее. Пометка зон. Конвертер обязан отвечать "зона изменилась" только когда ключ действительно выставлен: у предмета с нулевыми values писать нечего, и старое безусловное return 1 метило зону на каждом буте. Три из четырёх тестов падают на старом поведении. Первая версия теста про сосуды оказалась пустой: set_val у жидкостной тары сразу пишет ключи, guard срабатывает, и до засева дело не доходит. Переписан так, чтобы воспроизводить загрузку мимо set_val -- теперь падает как надо. Симметрия границ. Что прочитано через from_disk_text, обязано уйти обратно теми же байтами: нарушение этой пары и съело метки вещей, сундуки и списки имён. --- tests/meson.build | 1 + tests/native_text.cpp | 18 +++++++++++ tests/password.encoding.cpp | 61 ++++++++++++++++++++++++++++++++++++ tests/spell_item_convert.cpp | 49 +++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 tests/password.encoding.cpp diff --git a/tests/meson.build b/tests/meson.build index e2af290a9..7b0427449 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -39,6 +39,7 @@ test_sources = files( 'obj.copy.cpp', 'obj.liquid_core.cpp', 'spell_item_convert.cpp', + 'password.encoding.cpp', 'act.makefood.cpp', 'utils.editor.cpp', 'utils.string.cpp', diff --git a/tests/native_text.cpp b/tests/native_text.cpp index 9cee59829..9cc6ccb01 100644 --- a/tests/native_text.cpp +++ b/tests/native_text.cpp @@ -479,4 +479,22 @@ TEST(NativeText, ToDiskNeverTransliteratesDiskBytes) { EXPECT_EQ(native_text::to_disk("plain ascii"), "plain ascii"); } +TEST(NativeText, DiskRoundTripIsByteIdentical) { + // Правило пары: что прочитано через границу, должно уйти обратно теми же байтами. + // Нарушение этой пары -- одностороннее чтение или одностороння запись -- и съело + // метки вещей, сундуки дружин и списки имён (issue #3681). + const std::string on_disk = "\xD7\xC5\xD2\xC9\xCA.\xD3\xD7\xC5\xD4"; // 'верий.свет' в KOI8-R + + const std::string native = native_text::from_disk_text(on_disk); + EXPECT_EQ(native_text::to_disk(native), on_disk) << "чтение и запись обязаны быть зеркальны"; + + // Повторное чтение уже нативного текста ничего не меняет: именно на этом держится + // идемпотентность цикла загрузка-сохранение. + EXPECT_EQ(native_text::from_disk_text(native), native); + + // И то же самое для чистой латиницы -- она одинакова в обеих кодировках. + const std::string ascii = "plain ascii line"; + EXPECT_EQ(native_text::to_disk(native_text::from_disk_text(ascii)), ascii); +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/password.encoding.cpp b/tests/password.encoding.cpp new file mode 100644 index 000000000..69e6c9ba5 --- /dev/null +++ b/tests/password.encoding.cpp @@ -0,0 +1,61 @@ +// Хэш пароля считается по ДИСКОВЫМ байтам (issue #3681). +// +// Хэш -- сохранённые данные, и посчитан он когда-то по кодировке диска. Когда движок +// перешёл на UTF-8, те же буквы стали давать другие байты, и у всех, чей пароль +// с кириллицей, вход отвалился: 'Bad PW' при верном пароле. Латинские работали, потому +// что в ASCII обе кодировки совпадают. +// +// Проверять хэш напрямую нельзя -- соль случайная, -- поэтому свойство проверяется через +// наблюдаемое следствие: сравнение смотрит на дисковую форму пароля, а не на байты +// в памяти. + +#include "administration/password.h" +#include "utils/native_text.h" + +#include + +#include + +namespace { + +// "пароль" в UTF-8 байтами, чтобы тест не зависел от того, как редактор сохранил файл. +const std::string kCyrillic = "\xD0\xBF\xD0\xB0\xD1\x80\xD0\xBE\xD0\xBB\xD1\x8C"; + +} // namespace + +TEST(PasswordEncoding, CyrillicPasswordMatchesItself) { + const std::string hash = Password::generate_md5_hash(kCyrillic); + EXPECT_TRUE(Password::compare_password(hash, kCyrillic)); +} + +TEST(PasswordEncoding, WrongCyrillicPasswordIsRejected) { + const std::string hash = Password::generate_md5_hash(kCyrillic); + const std::string other = kCyrillic + "\xD1\x8B"; // тот же пароль плюс "ы" + EXPECT_FALSE(Password::compare_password(hash, other)); +} + +TEST(PasswordEncoding, HashIsTakenOverTheOnDiskForm) { + // Ключевое свойство. Два разных набора байт в памяти, у которых ОДНА дисковая форма: + // длинное тире на диске становится обычным дефисом (в KOI8-R его попросту нет, см. + // словарь замен). Если хэш считать по байтам памяти, эти пароли разойдутся; если по + // дисковой форме -- совпадут. Именно на этом расхождении и отвалился вход. + const std::string with_em_dash = "pa\xE2\x80\x94rol"; // pa—rol + const std::string with_hyphen = "pa-rol"; + + ASSERT_NE(with_em_dash, with_hyphen) << "проверка построена на том, что в памяти они разные"; + ASSERT_EQ(native_text::to_disk(with_em_dash), native_text::to_disk(with_hyphen)) + << "...а на диске -- одинаковые"; + + const std::string hash = Password::generate_md5_hash(with_em_dash); + EXPECT_TRUE(Password::compare_password(hash, with_hyphen)) + << "хэш обязан считаться по дисковой форме, иначе старые хэши не сойдутся"; +} + +TEST(PasswordEncoding, AsciiIsUnaffected) { + // Латиница в обеих кодировках одинакова -- на ней баг и не проявлялся. + const std::string hash = Password::generate_md5_hash("parolparol"); + EXPECT_TRUE(Password::compare_password(hash, "parolparol")); + EXPECT_FALSE(Password::compare_password(hash, "parolparo1")); +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/spell_item_convert.cpp b/tests/spell_item_convert.cpp index 6f08a5f8c..d6ca1c63a 100644 --- a/tests/spell_item_convert.cpp +++ b/tests/spell_item_convert.cpp @@ -203,4 +203,53 @@ TEST(PotencyFromProto, LegacyStoredPotencyIsNotProto) { EXPECT_FALSE(IsPotencyFromProto(p.get())); } +// ------------------------------------------------------------- issue #3749: пометка зон +// +// Возврат конвертера -- это ответ на вопрос «надо ли сохранять зону». Раньше он был +// безусловной единицей, и предмет, которому мигрировать нечего, помечал свою зону на +// каждом буте: ключей у него не появляется, guard «уже мигрировано» снова ложен, зона +// снова в очереди. На боевом так вечно переписывались зоны 1, 40, 498, 735 и 830. + +TEST(MigrationMarksZone, ScrollWithNothingToMigrateDoesNotMarkZone) { + auto p = make_proto(EObjType::kScroll, 0, 0, 0, 0); + EXPECT_EQ(0, ConvertSpellItemToEValueKey(p.get(), true)) << "сохранять нечего -- метить зону незачем"; + EXPECT_LT(key(p, ObjVal::EValueKey::kSpell1Num), 0) << "ключей и правда не появилось"; + // И на следующем буте ответ тот же, иначе зона метилась бы вечно. + EXPECT_EQ(0, ConvertSpellItemToEValueKey(p.get(), true)); +} + +TEST(MigrationMarksZone, PotionWithNothingToMigrateDoesNotMarkZone) { + auto p = make_proto(EObjType::kPotion, 0, 0, 0, 0); + EXPECT_EQ(0, ConvertPotionToEValueKey(p.get(), true)); + EXPECT_EQ(0, ConvertPotionToEValueKey(p.get(), true)); +} + +TEST(MigrationMarksZone, RealMigrationStillMarksZoneOnce) { + auto p = make_proto(EObjType::kPotion, 30, 28, -1, -1); + EXPECT_EQ(1, ConvertPotionToEValueKey(p.get(), true)) << "ключ появился -- зону сохранить надо"; + EXPECT_EQ(28, key(p, ObjVal::EValueKey::kSpell1Num)); + EXPECT_EQ(0, ConvertPotionToEValueKey(p.get(), true)) << "но ровно один раз"; +} + +TEST(MigrationMarksZone, LiquidCoreSeedingNeverMarksZone) { + // Ядро сосуда уходит на диск как обычные values (get_val/set_val перенаправлены в эти же + // ключи), а засев по замыслу повторяется на каждой загрузке -- значит помечать зону нельзя + // никогда, иначе она переписывается вечно. + // + // Воспроизводим именно загрузку мимо set_val: значения кладём, пока тип ещё не жидкостный, + // так они попадают в сырой val[], а ключей не появляется. Если задать их сразу сосуду, + // set_val запишет ключи сам, guard сработает, и до засева дело не дойдёт -- тест окажется + // пустым. + auto p = std::make_shared(100601); + p->set_type(EObjType::kOther); + p->set_val(0, 6); + p->set_val(1, 6); + p->set_val(2, 0); + p->set_type(EObjType::kLiquidContainer); + ASSERT_LT(key(p, ObjVal::EValueKey::kLiquidCapacity), 0) << "ключей до засева быть не должно"; + + EXPECT_EQ(0, ConvertDrinkconLiquidCore(p.get(), true)) << "засев есть, а метить зону нельзя"; + EXPECT_EQ(6, key(p, ObjVal::EValueKey::kLiquidCapacity)) << "но ключи при этом засеяны"; +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From d1be60630c011992d280c3422f1d2c0e90f3b4c8 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 21 Aug 2026 00:52:49 +0200 Subject: [PATCH 091/110] =?UTF-8?q?fix(utf8):=20=D0=B2=20=D1=81=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=BA=D0=B5=20=D0=B1=D0=B5=D0=B7=20crypt()=20?= =?UTF-8?q?=D1=85=D1=8D=D1=88=20=D1=82=D0=BE=D0=B6=D0=B5=20=D1=81=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B4=D0=B8=D1=81=D0=BA=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B5=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Фикс пароля был асимметричным: сравнение приводило пароль к дисковой кодировке, а generate_md5_hash под NOCRYPT возвращал нативную строку как есть. То есть в сборке без crypt() кириллический пароль не сходился сам с собой -- ровно та же болячка, от которой фикс и защищал, только в другой ветке #ifdef. Собрано это не на экзотике: crypt() не находится на Windows и macOS, и meson сам включает -DNOCRYPT (meson.build:205). Отсюда и красные Run tests в CI на четырёх джобах при зелёном линуксе -- новый тест поймал настоящий баг, а не разошёлся с платформой. Воспроизведено локально сборкой с -Dnocrypt=true: до правки падают два теста из четырёх, после -- вся сюита зелёная в обеих конфигурациях. --- src/administration/password.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/administration/password.cpp b/src/administration/password.cpp index f5f6097ba..cf6191ee1 100644 --- a/src/administration/password.cpp +++ b/src/administration/password.cpp @@ -44,7 +44,10 @@ const unsigned int MAX_PWD_LENGTH = 50; // * Генерация хэша с более-менее рандомным сальтом std::string generate_md5_hash(const std::string &pwd) { #ifdef NOCRYPT - return pwd; + // И здесь дисковая форма: сравнение всё равно приводит пароль к ней, а хранить + // нативную значило бы, что кириллический пароль не сойдётся сам с собой. Сборки + // без crypt() -- это Windows, macOS и -Dnocrypt=true (issue #3681). + return password_bytes(pwd); #else char key[14]; key[0] = '$'; From df9ad52f80383d6447d9e4658096bb0cba7f87ea Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 21 Aug 2026 01:06:31 +0200 Subject: [PATCH 092/110] =?UTF-8?q?test(utf8):=20=D0=BD=D0=B5=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B2=20=D1=82=D0=B5=D1=81=D1=82=D0=B5=20=D0=BE=D1=82=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=BE=D0=B9=20=D0=B1=D1=83=D0=BA=D0=B2=D0=BE=D0=B9=20(#3?= =?UTF-8?q?681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На macOS crypt() -- классический DES: он смотрит только первые восемь символов пароля. Пара 'parolparol' и 'parolparo1' различается десятым, то есть для DES это один и тот же пароль, и проверка «неверный отвергнут» на macOS превращалась в свою противоположность. Windows и Linux этого не показывали: там либо NOCRYPT, либо MD5-crypt без такого усечения. Различаем первой буквой. Остальные три теста под DES безопасны: их пароли короче восьми байт в дисковой форме и различаются в самом начале. --- tests/password.encoding.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/password.encoding.cpp b/tests/password.encoding.cpp index 69e6c9ba5..0b321c721 100644 --- a/tests/password.encoding.cpp +++ b/tests/password.encoding.cpp @@ -53,9 +53,14 @@ TEST(PasswordEncoding, HashIsTakenOverTheOnDiskForm) { TEST(PasswordEncoding, AsciiIsUnaffected) { // Латиница в обеих кодировках одинакова -- на ней баг и не проявлялся. + // + // Неверный пароль отличается ПЕРВОЙ буквой, а не последней, нарочно: на macOS crypt() -- + // классический DES, он смотрит только первые восемь символов. Пара, различающаяся + // десятым символом, там даёт один и тот же хэш, и проверка «неверный пароль отвергнут» + // молча превращается в свою противоположность. const std::string hash = Password::generate_md5_hash("parolparol"); EXPECT_TRUE(Password::compare_password(hash, "parolparol")); - EXPECT_FALSE(Password::compare_password(hash, "parolparo1")); + EXPECT_FALSE(Password::compare_password(hash, "xarolparol")); } // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 9c9a67f1c99141263f5aed16133734114cece6a7 Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 10:06:48 +0200 Subject: [PATCH 093/110] fix(utf8): cut the liquid separator by its own length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В названии ёмкости после переливания оставался лишний пробел: "древний череп с синим колдовским зельем". name_from_drinkcon срезал разделитель " с " жёсткой тройкой байт -- столько он занимает в KOI8-R. В UTF-8 "с" двухбайтовая, разделитель занимает четыре байта, срезалось три, и первый пробел оставался в названии. При следующей заливке к нему снова клеилось " с " -- пробелов становилось два. То же самое во втором месте, в цикле по падежам. Разделитель вынесен в константу kLiquidSeparator, длина берётся из неё же, и сборка названия (name_to_drinkcon) пользуется той же константой -- разбор и сборка больше не могут разъехаться. Добавлена защита от pos < длины разделителя: иначе substr на коротком имени уходит в underflow size_t. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/gameplay/mechanics/liquid.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/gameplay/mechanics/liquid.cpp b/src/gameplay/mechanics/liquid.cpp index 4b37fdbf0..a8a5413c1 100644 --- a/src/gameplay/mechanics/liquid.cpp +++ b/src/gameplay/mechanics/liquid.cpp @@ -556,6 +556,11 @@ size_t find_liquid_name(const char *name) { return result; } +// Разделитель между названием ёмкости и названием жидкости: "древний череп" + " с " + "зельем". +// Длину берём отсюда же -- в KOI8-R это три байта, в UTF-8 четыре, а зашитая тройка срезала +// разделитель не целиком и оставляла в названии лишний пробел (issue #3681). +static const std::string kLiquidSeparator = " с "; + void name_from_drinkcon(ObjData *obj) { char new_name[kMaxStringLength]; std::string tmp; @@ -568,8 +573,8 @@ void name_from_drinkcon(ObjData *obj) { obj->set_aliases(new_name); pos = find_liquid_name(obj->get_short_description().c_str()); - if (pos == std::string::npos) return; - tmp = obj->get_short_description().substr(0, pos - 3); + if (pos == std::string::npos || pos < kLiquidSeparator.length()) return; + tmp = obj->get_short_description().substr(0, pos - kLiquidSeparator.length()); sprintf(new_name, "%s", tmp.c_str()); obj->set_short_description(new_name); @@ -577,8 +582,8 @@ void name_from_drinkcon(ObjData *obj) { for (int c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; c++) { auto name_case = static_cast(c); pos = find_liquid_name(obj->get_PName(name_case).c_str()); - if (pos == std::string::npos) return; - tmp = obj->get_PName(name_case).substr(0, pos - 3); + if (pos == std::string::npos || pos < kLiquidSeparator.length()) return; + tmp = obj->get_PName(name_case).substr(0, pos - kLiquidSeparator.length()); sprintf(new_name, "%s", tmp.c_str()); obj->set_PName(name_case, new_name); } @@ -595,12 +600,14 @@ void name_to_drinkcon(ObjData *obj, int type) { snprintf(new_name, kMaxInputLength, "%s %s", obj->get_aliases().c_str(), potion_name); obj->set_aliases(new_name); - snprintf(new_name, kMaxInputLength, "%s с %s", obj->get_short_description().c_str(), potion_name); + snprintf(new_name, kMaxInputLength, "%s%s%s", obj->get_short_description().c_str(), + kLiquidSeparator.c_str(), potion_name); obj->set_short_description(new_name); for (c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; c++) { auto name_case = static_cast(c); - snprintf(new_name, kMaxInputLength, "%s с %s", obj->get_PName(name_case).c_str(), potion_name); + snprintf(new_name, kMaxInputLength, "%s%s%s", obj->get_PName(name_case).c_str(), + kLiquidSeparator.c_str(), potion_name); obj->set_PName(name_case, new_name); } // issue #3620: имя сосуда больше не совпадает с прототипом -- без этой пометки olc при правке From bcb7aa81629374c73e1c25a847dd2f96513a2ffb Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 11:20:52 +0200 Subject: [PATCH 094/110] fix(utf8): build the saving-throw trace with fmt, not a 256-byte sprintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрывает #3751. CalcGeneralSaving собирал диагностическую строку через sprintf в буфер на 256 байт. Сама фраза -- сто с лишним русских символов, плюс имя жертвы и название спаса: в KOI8-R это укладывалось впритык, в UTF-8 те же символы занимают вдвое больше, и на длинном имени ("могучий привратник восточных ворот") fortify ловил переполнение и валил процесс. Строка теперь собирается через fmt::format в std::string -- границы у неё нет. Заодно она строится только когда её кто-то увидит: spell_trace::Active для того и выставлен наружу, а CalcGeneralSaving -- горячий путь, до сих пор он форматировал текст на каждый спас-бросок, даже если в игре нет ни одного тестера. Обе ветки сообщения (критудача ДА/НЕТ) слиты в одну: отличались они только цветом, словом и делением save пополам, которое вынесено выше. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/gameplay/mechanics/saving.cpp | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/gameplay/mechanics/saving.cpp b/src/gameplay/mechanics/saving.cpp index 299db95ed..7c9157729 100644 --- a/src/gameplay/mechanics/saving.cpp +++ b/src/gameplay/mechanics/saving.cpp @@ -5,6 +5,7 @@ */ #include "gameplay/mechanics/saving.h" +#include #include "utils/logger.h" #include "gameplay/mechanics/mount.h" @@ -183,19 +184,30 @@ int CalcGeneralSaving(CharData *killer, CharData *victim, ESaving type, int ext_ } int save = CalcSaving(killer, victim, type, true); int rnd = number(-200, 200); - char smallbuf[256]; + // абсолютный фейл + const bool crit_fail = (number(1, 100) <= 5 + || (AFF_FLAGGED(victim, EAffect::kHold) && type == ESaving::kReflex)); + if (crit_fail) { + save /= 2; + } // Saving-throw debug trace: the 3 hardcoded immortal-name // short-circuits (Верий/Кудояр/Рогоза) were removed -- send_to_TC already gates the // message on EPrf::kTester / kCoderinfo / IsImpl, which is the correct way to opt in. - if (number(1, 100) <= 5 || (AFF_FLAGGED(victim, EAffect::kHold) && type == ESaving::kReflex)) { //абсолютный фейл - save /= 2; - sprintf(smallbuf, "&RПротивник %s (%d), ваш бонус: %d, спас '%s' противника: %d, random -200..200: %d, критудача: ДА, шанс успеха: %2.2f%%.\r\n&n", - GET_NAME(victim), GetRealLevel(victim), ext_apply, saving_name.find(type)->second.c_str(), save, rnd, ((std::clamp(save +ext_apply, -200, 200) + 200) / 400.) * 100.); - spell_trace::Line(killer, nullptr, "%s", smallbuf); - } else { - sprintf(smallbuf, "Противник %s (%d), ваш бонус: %d, спас '%s' противника: %d, random -200..200: %d, критудача: НЕТ, шанс успеха: %2.2f%%.\r\n", - GET_NAME(victim), GetRealLevel(victim), ext_apply, saving_name.find(type)->second.c_str(), save, rnd, ((std::clamp(save +ext_apply, -200, 200) + 200) / 400.) * 100.); - spell_trace::Line(killer, nullptr, "%s", smallbuf); + // + // Строка собирается через fmt::format и только если её кто-то увидит: sprintf клал русский + // текст в буфер на 256 байт, а в UTF-8 одна эта фраза с длинным именем моба в него не влезает + // -- fortify ловил переполнение и валил процесс (issue #3751). + if (spell_trace::Active(killer, nullptr)) { + const std::string trace = fmt::format( + "{}Противник {} ({}), ваш бонус: {}, спас '{}' противника: {}, " + "random -200..200: {}, критудача: {}, шанс успеха: {:.2f}%.\r\n{}", + crit_fail ? "&R" : "", + GET_NAME(victim), GetRealLevel(victim), ext_apply, + saving_name.find(type)->second, save, rnd, + crit_fail ? "ДА" : "НЕТ", + ((std::clamp(save + ext_apply, -200, 200) + 200) / 400.) * 100., + crit_fail ? "&n" : ""); + spell_trace::Line(killer, nullptr, "%s", trace.c_str()); } save += ext_apply; // внешний модификатор (обычно +каст) From 7aa9abbf539df09dc87547bd2204ef29030eb05a Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 11:28:18 +0200 Subject: [PATCH 095/110] fix(utf8): log the unknown obj prototype through fmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прочёсывание ветки на предмет буферов фиксированного размера с русским текстом (после #3751) нашло ещё одно место с реальным переполнением: char message[100]; sprintf(message, "неизвестный прототип объекта : %s (VNUM=%d)", имя, внум); Сам текст в UTF-8 занимает 69 байт, на имя предмета остаётся 31 -- то есть пятнадцать русских букв. "курганный оберег" уже не влезает, а sprintf через fortify роняет процесс, как в #3751. Строка собирается через fmt::format, буфер не нужен: у mudlog есть перегрузка под std::string. Проверка: g++ -fsyntax-only по заголовкам ветки проходит. Co-Authored-By: Claude Opus 5 (1M context) --- src/gameplay/handlers/alter_remove_poison.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gameplay/handlers/alter_remove_poison.cpp b/src/gameplay/handlers/alter_remove_poison.cpp index 417ccdf52..769f2b633 100644 --- a/src/gameplay/handlers/alter_remove_poison.cpp +++ b/src/gameplay/handlers/alter_remove_poison.cpp @@ -12,6 +12,7 @@ #include "engine/db/world_objects.h" #include "gameplay/mechanics/liquid.h" #include "utils/logger.h" +#include #include namespace handlers { @@ -19,9 +20,11 @@ namespace handlers { EStageResult AlterRemovePoison(ActionContext &ctx) { ObjData *obj = ctx.ovict; if (obj->get_rnum() < 0) { - char message[100]; - sprintf(message, "неизвестный прототип объекта : %s (VNUM=%d)", obj->get_PName(grammar::ECase::kNom).c_str(), obj->get_vnum()); - mudlog(message, BRF, kLvlBuilder, SYSLOG, 1); + // fmt, а не sprintf в буфер на 100 байт: сам текст занимает 69 байт в UTF-8, на имя + // предмета оставалось 31 -- меньше шестнадцати русских букв (issue #3681, ср. #3751). + mudlog(fmt::format("неизвестный прототип объекта : {} (VNUM={})", + obj->get_PName(grammar::ECase::kNom), obj->get_vnum()), + BRF, kLvlBuilder, SYSLOG, 1); return AlterMsg(ctx, ESpellMsg::kRemovePoisonUnknown); } // issue.potion-hotfix: remove-poison on a drink/food clears its poison LEVEL (kLiquidPoison) and From e529a9463da0bf2a66bb4d006799803a7eb3975d Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 13:07:33 +0200 Subject: [PATCH 096/110] fix(utf8): drop the 128-byte buffer from the drinkcon fill line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрывает #3752. show_obj_to_char собирал описание наполнения ёмкости через промежуточный буфер: char tmp2[128]; sprintf(tmp2, "(%s)", tmp); tmp -- целая фраза: "наполнен меньше, чем на четверть черной вязкой *отравленной* жидкостью". В KOI8-R это 70 байт и в 128 укладывалось с запасом, в UTF-8 те же символы дают больше 128, и fortify валил процесс на обычном "осмотреть суму". Буфер убран, строка собирается через fmt::format сразу в buf2. Заодно прогнал по ветке поиск того же класса: мелкий буфер, в который sprintf кладёт текст, приходящий из функции (а не литералом -- такие литералы я проверял в прошлый раз). Нашлось восемь мест, все с большим запасом: короткая латинская обвязка плюс имя. Проверка: сборка с -D_FORTIFY_SOURCE=3 чистая, 658 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/gameplay/mechanics/sight.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 8140a442d..591edcb4c 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -1343,10 +1343,12 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s sprintf(buf2, " %s ", diag_obj_to_char(object, 1)); if (object->get_type() == EObjType::kLiquidContainer) { char *tmp = drinkcon::daig_filling_drink(object, ch); - char tmp2[128]; native_text::copy_lower_char(tmp, tmp); - sprintf(tmp2, "(%s)", tmp); - strcat(buf2, tmp2); + // Без промежуточного буфера: описание наполнения -- длинная фраза ("наполнен + // меньше, чем на четверть черной вязкой *отравленной* жидкостью"), и в UTF-8 + // она не влезала в 128 байт: fortify ловил переполнение и валил процесс на + // осмотре ёмкости (issue #3752). + strcat(buf2, fmt::format("({})", tmp).c_str()); } } } From 9ce2702540ef6ede8b06eec3a8f5d3503acf3f14 Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 13:15:08 +0200 Subject: [PATCH 097/110] fix(utf8): build the rent report line without a 256-byte buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crash_report_rent_item собирал строку постоя в static char buf[256], а в неё входит название предмета в винительном падеже -- часть переменной длины, которую никто не ограничивает. Запас под UTF-8 съедается почти весь. Самое длинное название прототипа в мире -- 144 байта, строка с ним выходит около 217 байт из 256. Но имена скрафченного собираются в рантайме и бывают длиннее ("плащ из крепкой шкуры жуткого златобрюха с шелковой нитью и золотой цепочкой" -- 141 байт сам по себе), плюс к названию клеится пользовательская метка. В KOI8-R та же строка занимала вдвое меньше и вопроса не возникало. Переполнения на реальных данных я не воспроизвел -- показываю запас, а не факт. Но место ровно того же класса, что #3751 и #3752, и лечится так же: строка собирается через fmt::format в std::string, буферы buf[256], bf[80] и bf2[14] не нужны вовсе. Формат сообщения не изменился. Заодно ушла асимметрия в вычислении цены: она считалась дважды одним и тем же выражением (для числа и для названия валюты) -- теперь один раз в переменную, поведение то же. Проверка: сборка с -D_FORTIFY_SOURCE=3 чистая, 658 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/db/obj_save.cpp | 45 +++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/engine/db/obj_save.cpp b/src/engine/db/obj_save.cpp index 99800f075..a71db18e0 100644 --- a/src/engine/db/obj_save.cpp +++ b/src/engine/db/obj_save.cpp @@ -2271,32 +2271,27 @@ void Crash_report_rent_item(CharData *ch, int factor, int equip, int recursive) { - static char buf[256]; - char bf[80], bf2[14]; - - if (obj) { - if (CAN_WEAR_ANY(obj)) { - if (equip) { - sprintf(bf, " (%d если снять)", obj->get_rent_off() * factor * count); - } else { - sprintf(bf, " (%d если надеть)", obj->get_rent_on() * factor * count); - } - } else { - *bf = '\0'; - } - - if (count > 1) { - sprintf(bf2, " [%d]", count); - } - - sprintf(buf, "%s - %d %s%s за %s%s %s", - recursive ? "" : kColorWht, - (equip ? obj->get_rent_on() * count : obj->get_rent_off()) * - factor * count, - MUD::Currency(currencies::kGoldVnum).GetNameWithAmount((equip ? obj->get_rent_on() * count : obj->get_rent_off()) * factor * count, grammar::ECase::kNom).c_str(), - bf, OBJN(obj, ch, grammar::ECase::kAcc), count > 1 ? bf2 : "", recursive ? "" : kColorNrm); - act(buf, false, recep, 0, ch, kToVict); + if (!obj) { + return; } + // Строка собирается через fmt, а не в буфер на 256 байт: в неё входит название предмета в + // винительном падеже, а в UTF-8 русский текст занимает вдвое больше места -- длинного имени + // вместе с ценой и валютой хватало, чтобы вылезти за край (тот же класс, что #3751 и #3752). + std::string bf; + if (CAN_WEAR_ANY(obj)) { + bf = equip + ? fmt::format(" ({} если снять)", obj->get_rent_off() * factor * count) + : fmt::format(" ({} если надеть)", obj->get_rent_on() * factor * count); + } + const std::string bf2 = count > 1 ? fmt::format(" [{}]", count) : std::string(); + const int cost = (equip ? obj->get_rent_on() * count : obj->get_rent_off()) * factor * count; + + const std::string line = fmt::format("{} - {} {}{} за {}{} {}", + recursive ? "" : kColorWht, + cost, + MUD::Currency(currencies::kGoldVnum).GetNameWithAmount(cost, grammar::ECase::kNom), + bf, OBJN(obj, ch, grammar::ECase::kAcc), bf2, recursive ? "" : kColorNrm); + act(line.c_str(), false, recep, 0, ch, kToVict); } // end by WorM From f923bf70f36a132f53c06bec26ffd167e6dd8324 Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 13:43:08 +0200 Subject: [PATCH 098/110] refactor(dg): build script log lines with fmt instead of 32K stack buffers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit script_log и trig_log держали по char tmpbuf[kMaxStringLength] -- это 32 килобайта на стеке каждый, а trig_log зовёт script_log, так что запись одной строки в лог триггера съедала 64 КБ стека. Переполнения там не было (обе записи шли через snprintf с правильным размером), но буферы не нужны: строка собирается fmt::format, у mudlog есть перегрузка под std::string. Побайтная замена маркеров UID сохранена как есть -- таблица трогает только 0x1C, 0x1D и 0x1E, эти коды меньше 0x80 и внутри многобайтовых UTF-8 последовательностей не встречаются, так что кириллица через цикл проходит нетронутой. Добавил про это комментарий, чтобы цикл не выглядел опасным при следующем чтении. Проверка: сборка с -D_FORTIFY_SOURCE=3 чистая, 658 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/scripting/dg_scripts.cpp | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 7a4254ed8..99f3e8450 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -173,18 +173,16 @@ const char uid_replace_table[] = { }; void script_log(const char *msg, LogMode type) { - char tmpbuf[kMaxStringLength]; - - snprintf(tmpbuf, kMaxStringLength, "SCRIPT LOG %s", msg); - - char *pos = tmpbuf; - while (*pos != '\0') { - *pos = uid_replace_table[static_cast(*pos)]; - ++pos; + std::string text = fmt::format("SCRIPT LOG {}", msg); + // Замена маркеров UID (0x1C/0x1D/0x1E) на пробел -- побайтно, но безопасно: таблица трогает + // только эти три кода, а они меньше 0x80 и внутри многобайтовых UTF-8 последовательностей + // не встречаются. + for (char &c : text) { + c = uid_replace_table[static_cast(c)]; } - log("%s", tmpbuf); - mudlog(tmpbuf, type ? type : NRM, kLvlBuilder, ERRLOG, true); + log("%s", text.c_str()); + mudlog(text, type ? type : NRM, kLvlBuilder, ERRLOG, true); } /* @@ -192,10 +190,9 @@ void script_log(const char *msg, LogMode type) { * Will eventually allow on-line view of script errors. */ void trig_log(Trigger *trig, std::string msg, LogMode type) { - char tmpbuf[kMaxStringLength]; - snprintf(tmpbuf, kMaxStringLength, "(Trigger: %s, VNum: %d) : %s [строка: %d]", GET_TRIG_NAME(trig), - GET_TRIG_VNUM(trig), msg.c_str(), last_trig_line_num); - script_log(tmpbuf, type); + script_log(fmt::format("(Trigger: {}, VNum: {}) : {} [строка: {}]", + GET_TRIG_NAME(trig), GET_TRIG_VNUM(trig), msg, last_trig_line_num).c_str(), + type); } cmdlist_element::shared_ptr find_end(Trigger *trig, cmdlist_element::shared_ptr cl); From ebfec4dc1dbec53aed9e15c9de81f6aad9ad2a8b Mon Sep 17 00:00:00 2001 From: stribog Date: Fri, 21 Aug 2026 13:53:22 +0200 Subject: [PATCH 099/110] refactor(dg): replace the UID table with std::replace_if, add UID_CHAR_ALL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замена меток UID перед записью в лог делалась таблицей на 256 значений, из которых 253 отображали байт сам в себя. Из кода не было видно, что именно происходит, и таблицу забыли обновить, когда появился UID_CHAR_ALL ('\x1f') -- он уезжал в лог сырым управляющим байтом. Теперь std::replace_if по четырём меткам: OBJ, ROOM, CHAR и CHAR_ALL. Таблица (35 строк) удалена, поведение для трёх старых меток прежнее, четвёртая наконец обрабатывается. Проверка: сборка с -D_FORTIFY_SOURCE=3 чистая, 658 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/scripting/dg_scripts.cpp | 51 ++++++----------------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/src/engine/scripting/dg_scripts.cpp b/src/engine/scripting/dg_scripts.cpp index 99f3e8450..38c906ef8 100644 --- a/src/engine/scripting/dg_scripts.cpp +++ b/src/engine/scripting/dg_scripts.cpp @@ -50,6 +50,7 @@ #include "engine/core/utils_char_obj.inl" #include "engine/core/target_resolver.h" #include "gameplay/mechanics/stable_objs.h" +#include #include #include #include "gameplay/mechanics/weather.h" @@ -136,50 +137,18 @@ void do_worldecho(char *msg); */ bool CharacterLinkDrop = false; -//table for replace UID_CHAR, UID_OBJ, UID_ROOM -const char uid_replace_table[] = { - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', - '\x0e', '\x0f', //16 - '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b', '\x20', '\x20', - '\x20', '\x1f', //32 - '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', - '\x2e', '\x2f', //48 - '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', - '\x3e', '\x3f', //64 - '\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', - '\x4e', '\x4f', //80 - '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57', '\x58', '\x59', '\x5a', '\x5b', '\x5c', '\x5d', - '\x5e', '\x5f', //96 - '\x60', '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67', '\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', - '\x6e', '\x6f', //112 - '\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77', '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', - '\x7e', '\x7f', //128 - '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', '\x89', '\x8a', '\x8b', '\x8c', '\x8d', - '\x8e', '\x8f', //144 - '\x90', '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x98', '\x99', '\x9a', '\x9b', '\x9c', '\x9d', - '\x9e', '\x9f', //160 - '\xa0', '\xa1', '\xa2', '\xa3', '\xa4', '\xa5', '\xa6', '\xa7', '\xa8', '\xa9', '\xaa', '\xab', '\xac', '\xad', - '\xae', '\xaf', //176 - '\xb0', '\xb1', '\xb2', '\xb3', '\xb4', '\xb5', '\xb6', '\xb7', '\xb8', '\xb9', '\xba', '\xbb', '\xbc', '\xbd', - '\xbe', '\xbf', //192 - '\xc0', '\xc1', '\xc2', '\xc3', '\xc4', '\xc5', '\xc6', '\xc7', '\xc8', '\xc9', '\xca', '\xcb', '\xcc', '\xcd', - '\xce', '\xcf', //208 - '\xd0', '\xd1', '\xd2', '\xd3', '\xd4', '\xd5', '\xd6', '\xd7', '\xd8', '\xd9', '\xda', '\xdb', '\xdc', '\xdd', - '\xde', '\xdf', //224 - '\xe0', '\xe1', '\xe2', '\xe3', '\xe4', '\xe5', '\xe6', '\xe7', '\xe8', '\xe9', '\xea', '\xeb', '\xec', '\xed', - '\xee', '\xef', //240 - '\xf0', '\xf1', '\xf2', '\xf3', '\xf4', '\xf5', '\xf6', '\xf7', '\xf8', '\xf9', '\xfa', '\xfb', '\xfc', '\xfd', - '\xfe', '\xff' //256 -}; void script_log(const char *msg, LogMode type) { std::string text = fmt::format("SCRIPT LOG {}", msg); - // Замена маркеров UID (0x1C/0x1D/0x1E) на пробел -- побайтно, но безопасно: таблица трогает - // только эти три кода, а они меньше 0x80 и внутри многобайтовых UTF-8 последовательностей - // не встречаются. - for (char &c : text) { - c = uid_replace_table[static_cast(c)]; - } + // Внутри движка ссылка на сущность хранится как <метка UID><номер> (см. dg_scripts.h). + // В лог сырые управляющие байты писать нельзя -- меняем метку на пробел, номер остаётся + // читаемым. Все четыре метки меньше 0x80, внутри многобайтовых UTF-8 последовательностей + // такие значения не встречаются, так что кириллица не задета. + std::replace_if(text.begin(), text.end(), + [](char c) { + return c == UID_OBJ || c == UID_ROOM || c == UID_CHAR || c == UID_CHAR_ALL; + }, + ' '); log("%s", text.c_str()); mudlog(text, type ? type : NRM, kLvlBuilder, ERRLOG, true); From 59b69902391ae41507bfc2d1afd7edf43616ec76 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 21 Aug 2026 17:43:52 +0200 Subject: [PATCH 100/110] =?UTF-8?q?fix(utf8):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=81=D0=B8=D1=80=D0=BE=D1=82=D0=B5=D0=B2?= =?UTF-8?q?=D1=88=D1=83=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=83=D1=8E=20=D0=B8=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=BC=20round-tri?= =?UTF-8?q?p=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=91?= =?UTF-8?q?=D0=BC=D0=BA=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переменная. size_t i = strlen(name) в forget_recipe осталась от перевода на IsAbbr -- единственное предупреждение во всей сборке, и моё. Когда я чистил такие остатки, я считал вхождения имени по всему файлу, а надо было в пределах функции: 'i' там встречается сто раз, но в этой функции больше не нужна. Тест. Заливка и опустошение ёмкости обязаны возвращать название байт в байт. Разделитель ' с ' срезался жёсткой тройкой байт -- столько он занимает в KOI8-R, а в UTF-8 их четыре, -- и в названии оставался лишний пробел, который при следующей заливке удваивался. На старом поведении оба теста падают. Сборка с нуля: ноль предупреждений, ноль ошибок, 657 тестов проходят. --- src/gameplay/crafting/im.cpp | 1 - tests/liquid.name_roundtrip.cpp | 68 +++++++++++++++++++++++++++++++++ tests/meson.build | 1 + 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/liquid.name_roundtrip.cpp diff --git a/src/gameplay/crafting/im.cpp b/src/gameplay/crafting/im.cpp index ee4991cdd..54ab086db 100644 --- a/src/gameplay/crafting/im.cpp +++ b/src/gameplay/crafting/im.cpp @@ -1616,7 +1616,6 @@ void forget_recipe(CharData *ch, char *argument, int/* subcmd*/) { argument += qend + 1; name[qend - 1] = '\0'; - size_t i = strlen(name); for (rcpt = top_imrecipes; rcpt >= 0; --rcpt) { if (utils::IsAbbr(name, imrecipes[rcpt].name)) { break; diff --git a/tests/liquid.name_roundtrip.cpp b/tests/liquid.name_roundtrip.cpp new file mode 100644 index 000000000..ff220562a --- /dev/null +++ b/tests/liquid.name_roundtrip.cpp @@ -0,0 +1,68 @@ +// Название ёмкости: залил жидкость -- вылил -- имя вернулось прежним (issue #3681). +// +// Разделитель " с " между названием ёмкости и названием жидкости срезался жёсткой +// тройкой байт -- столько он занимает в KOI8-R. В UTF-8 буква "с" двухбайтовая, +// разделитель занимает четыре байта, срезалось три, и первый пробел оставался +// в названии: "древний череп с синим колдовским зельем". При следующей заливке +// к нему снова клеился разделитель, и пробелов становилось два. + +#include "gameplay/mechanics/liquid.h" +#include "engine/entities/obj_data.h" +#include "utils/grammar/cases.h" + +#include + +#include +#include + +namespace { + +constexpr ObjVnum kRoundTripJarVnum = 100702; +constexpr int kRoundTripLiquid = 0; // первая жидкость из drinknames[] + +ObjData::shared_ptr MakeJar(const std::string &name) { + auto prototype = std::make_shared(kRoundTripJarVnum); + auto jar = std::make_shared(*prototype); + jar->set_type(EObjType::kLiquidContainer); + jar->set_short_description(name); + jar->set_aliases(name); + for (int c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; ++c) { + jar->set_PName(static_cast(c), name); + } + return jar; +} + +} // namespace + +TEST(LiquidNameRoundTrip, FillAndEmptyRestoresTheName) { + // "древний череп" -- название с кириллицей, на нём баг и был виден. + const std::string original = "\xD0\xB4\xD1\x80\xD0\xB5\xD0\xB2\xD0\xBD\xD0\xB8\xD0\xB9 " + "\xD1\x87\xD0\xB5\xD1\x80\xD0\xB5\xD0\xBF"; + auto jar = MakeJar(original); + + name_to_drinkcon(jar.get(), kRoundTripLiquid); + ASSERT_NE(jar->get_short_description(), original) << "жидкость должна попасть в название"; + + name_from_drinkcon(jar.get()); + EXPECT_EQ(jar->get_short_description(), original) + << "после опустошения название обязано вернуться байт в байт, без хвостового пробела"; + EXPECT_EQ(jar->get_PName(grammar::ECase::kNom), original) << "и в падежах тоже"; +} + +TEST(LiquidNameRoundTrip, RepeatedFillsDoNotAccumulateSpaces) { + // Именно так лишние пробелы и копились: остаток разделителя оставался в названии, + // и следующая заливка приклеивала к нему ещё один. + const std::string original = "\xD1\x81\xD0\xBE\xD1\x81\xD1\x83\xD0\xB4"; // "сосуд" + auto jar = MakeJar(original); + + for (int i = 0; i < 3; ++i) { + name_to_drinkcon(jar.get(), kRoundTripLiquid); + name_from_drinkcon(jar.get()); + } + + EXPECT_EQ(jar->get_short_description(), original); + EXPECT_EQ(jar->get_short_description().find(" "), std::string::npos) + << "двойных пробелов в названии быть не должно"; +} + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/tests/meson.build b/tests/meson.build index 7b0427449..7272ed6f2 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -38,6 +38,7 @@ test_sources = files( 'char.morph.copy.cpp', 'obj.copy.cpp', 'obj.liquid_core.cpp', + 'liquid.name_roundtrip.cpp', 'spell_item_convert.cpp', 'password.encoding.cpp', 'act.makefood.cpp', From 2f52f8932eca544ac675f4af197e247a192c5d95 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 21 Aug 2026 18:37:19 +0200 Subject: [PATCH 101/110] =?UTF-8?q?fix(utf8):=20=C2=AB=D0=B3=D0=B4=D0=B5?= =?UTF-8?q?=C2=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=BB=D0=BE=20=D0=B1=D1=83=D1=84=D0=B5=D1=80=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BD=D0=B0=20=D0=B4=D0=BB=D0=B8?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=D1=85=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поиск богом по частому слову ('где белый кам') отвечал '***ПЕРЕПОЛНЕНИЕ***' вместо списка. Буфер вывода дескриптора ограничен байтами -- kLargeBufSize, 48864, -- а под UTF-8 русский текст занимает вдвое больше, чем занимал в KOI8-R. То есть вместимость в буквах упала вдвое, и списки, которые раньше влезали, перестали. Ограничения на объём у команды нет: PerformImmortWhere собирает все совпадения по миру и шлёт одним куском через SendMsgToChar. Остальные длинные списки в проекте так не делают -- склад, обменник, 'кто' и осмотр давно выводят через page_string, который режет текст по высоте экрана игрока. Переведён и 'где', оба его списка: найденное по слову и перечень игроков без аргумента. Буфер не трогаю: kMaxSockBuf задаёт заодно размер стековых буферов в process_output (i[96K] и o[288K]), и поднимать его ради одной команды -- это 384 КБ стека превратить в 768. page_string при отсутствующем дескрипторе просто выходит, так что для персонажа без связи поведение прежнее. --- src/engine/ui/cmd/do_where.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/engine/ui/cmd/do_where.cpp b/src/engine/ui/cmd/do_where.cpp index 2dd5fc28e..09f9714dc 100644 --- a/src/engine/ui/cmd/do_where.cpp +++ b/src/engine/ui/cmd/do_where.cpp @@ -3,6 +3,7 @@ // #include "engine/entities/char_data.h" +#include "engine/ui/modify.h" #include "utils/native_text.h" #include "administration/privilege.h" #include "engine/db/world_objects.h" @@ -64,7 +65,7 @@ void PerformImmortWhere(CharData *ch, char *arg) { } } } - SendMsgToChar(ss.str(), ch); + page_string(ch->desc, ss.str()); // список игроков тоже бывает длинным } else { std::vector rows; target_resolver::Query q; @@ -87,7 +88,12 @@ void PerformImmortWhere(CharData *ch, char *arg) { found = 1; } if (found) { - SendMsgToChar(where_format::FormatWhere(rows), ch); + // Постранично, а не одним куском: буфер вывода дескриптора ограничен байтами + // (kLargeBufSize, около 48 КБ), а под UTF-8 русский текст занимает вдвое больше, + // чем занимал в KOI8-R. Поиск по частому слову перестал влезать, и игрок получал + // "***ПЕРЕПОЛНЕНИЕ***" вместо списка. Так же выводят свои длинные списки склад, + // обменник и "кто" (issue #3681). + page_string(ch->desc, where_format::FormatWhere(rows)); } else { SendMsgToChar("Нет ничего похожего.\r\n", ch); } From 0e0aa71a1e2f01d83eb3bd4da0ff8da8a011cebd Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 21 Aug 2026 23:39:45 +0200 Subject: [PATCH 102/110] =?UTF-8?q?fix(io):=20=D0=BD=D0=B5=20=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B2=20=D0=BB=D0=BE=D0=B3=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новая диагностика переполнения печатает t->last_input. Эта запись заполняется в process_input на ЛЮБОЙ строке от клиента -- проверки состояния над ней нет, -- поэтому в парольных состояниях там лежит сам пароль в открытом виде. Состояний семь: ввод пароля при входе, задание и подтверждение нового, три шага смены пароля и подтверждение удаления персонажа. Во всех вместо ввода печатается '<ввод скрыт>'. Достижимо это или нет -- вопрос спорный: переполнение требует около 47 КБ невыведенного текста, а сразу после верного пароля сервер как раз шлёт MOTD, новости и уведомления о сообщениях. Но пароль открытым текстом в сислоге -- не та вещь, которую стоит оценивать по вероятности. --- src/engine/core/iosystem.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index c94ecbfbd..b94abf23f 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -153,10 +153,20 @@ void write_to_output(const char *txt, DescriptorData *t) { // bufptr == ~0ull в начале функции), а игрок об этом не узнает. Пишем в сислог, кому и // на какой команде не хватило буфера -- иначе такие случаи видны только счётчиком в // "показать статистика", без единой подробности. + // last_input заполняется на ЛЮБОЙ строке от клиента -- проверки состояния над той + // записью нет, -- так что в парольных состояниях там лежит сам пароль. В лог его + // отдавать нельзя: сислог читают и хранят. + const bool secret_input = t->state == EConState::kPassword + || t->state == EConState::kNewpasswd + || t->state == EConState::kCnfpasswd + || t->state == EConState::kChpwdGetOld + || t->state == EConState::kChpwdGetNew + || t->state == EConState::kChpwdVrfy + || t->state == EConState::kDelcnf1; log("SYSERR: output overflow: %s [%s], команда '%s', в буфере %zu б, отброшено %zu б", t->character ? GET_NAME(t->character) : "<без персонажа>", *t->host ? t->host : "?", - t->last_input, + secret_input ? "<ввод скрыт>" : t->last_input, t->bufspace, size); return; } From a06b1065a9155bf64962f1625c4e54e095049b12 Mon Sep 17 00:00:00 2001 From: stribog Date: Sat, 22 Aug 2026 13:00:57 +0200 Subject: [PATCH 103/110] fix(utf8): convert text files read into a buffer to the native encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Экран справки ("справка" без параметров) приезжал игроку кашей. ReadFileToBuffer читал файл сырыми байтами и отдавал как есть. На диске тексты в KOI8-R, движок держит их в нативной кодировке, и на выходе перекодировщик получал KOI8 вместо UTF-8. Темы справки этой болезнью не страдали -- load_help переводит кодировку сам, -- а экран грузится другим путём, через AllocateBufferForFile, и перевода там не было. Собираем текст в строку и переводим один раз через from_disk_text. Проверка на размер осталась, но теперь считает уже переведённый текст: под UTF-8 он длиннее прочитанного с диска. Тем же путём читается всё, что грузится через AllocateBufferForFile -- экран справки при старте и при "перезагрузить справку". Проверка: сборка ветки с -D_FORTIFY_SOURCE=3 чистая, 660 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/db/db.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/engine/db/db.cpp b/src/engine/db/db.cpp index 19c2b2d1d..886b5ee0c 100644 --- a/src/engine/db/db.cpp +++ b/src/engine/db/db.cpp @@ -3535,6 +3535,7 @@ int ReadFileToBuffer(const char *name, char *destination_buf) { // Цикл идёт по результату fgets. Прежний do/while обрабатывал tmp и после неудачного чтения: // на пустом файле strlen() шёл по неинициализированной памяти, а при нулевой длине // tmp[strlen(tmp) - 1] писал байт ПЕРЕД началом буфера. + std::string text; while (fgets(tmp, READ_SIZE, fl)) { const size_t len = strlen(tmp); // перевод строки снимаем, только если он есть: у строки длиннее READ_SIZE и у последней @@ -3542,19 +3543,22 @@ int ReadFileToBuffer(const char *name, char *destination_buf) { if (len > 0 && tmp[len - 1] == '\n') { tmp[len - 1] = '\0'; } - strcat(tmp, "\r\n"); - - if (strlen(destination_buf) + strlen(tmp) + 1 > kMaxExtendLength) { - log("SYSERR: %s: string too big (%d max)", name, kMaxStringLength); - *destination_buf = '\0'; - fclose(fl); - return (-1); - } - strcat(destination_buf, tmp); + text += tmp; + text += "\r\n"; } fclose(fl); + // Тексты лежат на диске в KOI8-R, движок держит их в нативной кодировке. Без перевода + // экран справки уезжал игроку сырыми байтами и выглядел кашей (issue #3681). + const std::string native = native_text::from_disk_text(text); + if (native.size() + 1 > kMaxExtendLength) { + log("SYSERR: %s: string too big (%d max)", name, kMaxStringLength); + *destination_buf = '\0'; + return (-1); + } + memcpy(destination_buf, native.c_str(), native.size() + 1); + return (0); } From 57db1fa149119a18c58314c3064de20feb4247f4 Mon Sep 17 00:00:00 2001 From: stribog Date: Mon, 24 Aug 2026 07:12:36 +0200 Subject: [PATCH 104/110] fix(utf8): convert alias files at the encoding boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Команда "синоним" показывала русские синонимы кашей: alias.cpp читал и писал файлы сырыми байтами. Файлы лежат на диске в кодировке мира, движок держит текст в нативной, перекодировки на границе не было вовсе. Особенность формата: перед каждой строкой пишется её длина в байтах. Поэтому просто перекодировать при выводе нельзя -- при записи длина считается уже по перекодированной строке, иначе чтение разъедется на первом же русском синониме. На чтении взят from_disk_line, а не from_koi8: он распознаёт уже нативный текст и не перекодирует повторно. Это важно прямо сейчас -- файлы, которые сборка без перекодировки успела записать в UTF-8, прочитаются как есть, а при следующей записи лягут на диск уже правильно. Проверка: сборка ветки с -D_FORTIFY_SOURCE=3 чистая, 660 тестов проходят. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/ui/alias.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/engine/ui/alias.cpp b/src/engine/ui/alias.cpp index 5d43a9113..071c8521e 100644 --- a/src/engine/ui/alias.cpp +++ b/src/engine/ui/alias.cpp @@ -19,6 +19,7 @@ #include "engine/entities/char_data.h" #include "engine/ui/alias.h" +#include "utils/native_text.h" void WriteAliases(CharData *ch) { FILE *file; @@ -39,16 +40,19 @@ void WriteAliases(CharData *ch) { } for (temp = GET_ALIASES(ch); temp; temp = temp->next) { - size_t aliaslen = strlen(temp->alias); - size_t repllen = strlen(temp->replacement); + // Файл синонимов лежит на диске в кодировке мира, а движок держит текст в нативной. + // Длины в формате -- байтовые, поэтому считаем их уже по перекодированным строкам, + // иначе чтение разъедется на первом же русском синониме (issue #3681). + const std::string alias_on_disk = native_text::to_disk(temp->alias); + const std::string repl_on_disk = native_text::to_disk(temp->replacement); fprintf(file, "%d\n%s\n" // Alias "%d\n%s\n" // Replacement "%d\n", // Type - static_cast(aliaslen), - temp->alias, - static_cast(repllen), - temp->replacement, + static_cast(alias_on_disk.size()), + alias_on_disk.c_str(), + static_cast(repl_on_disk.size()), + repl_on_disk.c_str(), temp->type); } @@ -81,11 +85,13 @@ void ReadAliases(CharData *ch) { { dummyi = fscanf(file, "%d\n", &length); dummyc = fgets(xbuf, length + 1, file); - t2->alias = str_dup(xbuf); + // from_disk_line, а не from_koi8: он распознаёт уже нативный текст и не перекодирует + // повторно -- файлы, записанные сборкой без перекодировки, читаются как есть. + t2->alias = str_dup(native_text::from_disk_line(xbuf).c_str()); // Build the replacement. dummyi = fscanf(file, "%d\n", &length); dummyc = fgets(xbuf, length + 1, file); - t2->replacement = str_dup(xbuf); + t2->replacement = str_dup(native_text::from_disk_line(xbuf).c_str()); // Figure out the alias type. dummyi = fscanf(file, "%d\n", &length); t2->type = length; From 6ff7b4db1f7202f9635af6cc453934f30f7deece Mon Sep 17 00:00:00 2001 From: stribog Date: Mon, 24 Aug 2026 10:11:31 +0200 Subject: [PATCH 105/110] fix(utf8): heal drinkcon names left with a stray space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правка разделителя (9c9a67f1c) чинила только новые названия. Старая сборка резала " с " по трем байтам вместо четырех и оставляла в названии хвостовой пробел -- такие названия уже разъехались по файлам вещей. Дальше круг замкнут: при загрузке name_from_drinkcon снимает ровно разделитель и получает "огромная дубовая бочка ", а name_to_drinkcon приклеивает разделитель обратно -- и снова "огромная дубовая бочка с черным колдовским зельем", при каждой загрузке. Срезаем хвостовые пробелы с обеих сторон: и когда название жидкости снимаем, и когда добавляем. Загрузка вещи гоняет эту пару подряд, поэтому испорченные названия чинятся сами, а описки билдеров в прототипах не всплывают в игре. Заодно в name_to_drinkcon ушли sprintf в char[] (теперь fmt::format) и чтение drinknames[] по отрицательному индексу. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XUwDWDnYdXrJdvjDVd36QH --- src/gameplay/mechanics/liquid.cpp | 69 ++++++++++++++++--------------- tests/liquid.name_roundtrip.cpp | 20 +++++++++ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/gameplay/mechanics/liquid.cpp b/src/gameplay/mechanics/liquid.cpp index a8a5413c1..d44a0cd8c 100644 --- a/src/gameplay/mechanics/liquid.cpp +++ b/src/gameplay/mechanics/liquid.cpp @@ -20,6 +20,10 @@ #include "engine/db/global_objects.h" #include "poison.h" +#include "utils/utils_string.h" + +#include + #include #include @@ -561,54 +565,53 @@ size_t find_liquid_name(const char *name) { // разделитель не целиком и оставляла в названии лишний пробел (issue #3681). static const std::string kLiquidSeparator = " с "; -void name_from_drinkcon(ObjData *obj) { - char new_name[kMaxStringLength]; - std::string tmp; +// Имена, которые прежняя обрезка успела испортить, оканчиваются пробелом -- он остался от +// недорезанного разделителя и уехал в файлы вещей. Приписав к такому имени " с ", получаем +// "огромная дубовая бочка с черным колдовским зельем", и так при каждой загрузке. Поэтому +// хвостовые пробелы срезаем с обеих сторон -- и когда название жидкости снимаем, и когда +// добавляем: уже испорченные вещи чинятся сами, а описки билдеров не всплывают в игре. +static std::string CutLiquidName(const std::string &name, size_t pos, size_t separator_len) { + return utils::TrimRightCopy(name.substr(0, pos - separator_len)); +} +void name_from_drinkcon(ObjData *obj) { size_t pos = find_liquid_name(obj->get_aliases().c_str()); - if (pos == std::string::npos) return; - tmp = obj->get_aliases().substr(0, pos - 1); - - sprintf(new_name, "%s", tmp.c_str()); - obj->set_aliases(new_name); + if (pos == std::string::npos || pos < 1) { + return; + } + obj->set_aliases(CutLiquidName(obj->get_aliases(), pos, 1)); pos = find_liquid_name(obj->get_short_description().c_str()); - if (pos == std::string::npos || pos < kLiquidSeparator.length()) return; - tmp = obj->get_short_description().substr(0, pos - kLiquidSeparator.length()); - - sprintf(new_name, "%s", tmp.c_str()); - obj->set_short_description(new_name); + if (pos == std::string::npos || pos < kLiquidSeparator.length()) { + return; + } + obj->set_short_description(CutLiquidName(obj->get_short_description(), pos, kLiquidSeparator.length())); for (int c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; c++) { auto name_case = static_cast(c); pos = find_liquid_name(obj->get_PName(name_case).c_str()); - if (pos == std::string::npos || pos < kLiquidSeparator.length()) return; - tmp = obj->get_PName(name_case).substr(0, pos - kLiquidSeparator.length()); - sprintf(new_name, "%s", tmp.c_str()); - obj->set_PName(name_case, new_name); + if (pos == std::string::npos || pos < kLiquidSeparator.length()) { + return; + } + obj->set_PName(name_case, CutLiquidName(obj->get_PName(name_case), pos, kLiquidSeparator.length())); } } void name_to_drinkcon(ObjData *obj, int type) { - int c; - char new_name[kMaxInputLength], potion_name[kMaxInputLength]; - if (type >= NUM_LIQ_TYPES) { - snprintf(potion_name, kMaxInputLength, "%s", "непонятной бормотухой, сообщите БОГАМ"); - } else { - snprintf(potion_name, kMaxInputLength, "%s", drinknames[type]); - } + const std::string potion_name = (type < 0 || type >= NUM_LIQ_TYPES) + ? "непонятной бормотухой, сообщите БОГАМ" + : drinknames[type]; - snprintf(new_name, kMaxInputLength, "%s %s", obj->get_aliases().c_str(), potion_name); - obj->set_aliases(new_name); - snprintf(new_name, kMaxInputLength, "%s%s%s", obj->get_short_description().c_str(), - kLiquidSeparator.c_str(), potion_name); - obj->set_short_description(new_name); + obj->set_aliases(fmt::format("{} {}", utils::TrimRightCopy(obj->get_aliases()), potion_name)); + obj->set_short_description(fmt::format("{}{}{}", + utils::TrimRightCopy(obj->get_short_description()), + kLiquidSeparator, potion_name)); - for (c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; c++) { + for (int c = grammar::ECase::kFirstCase; c <= grammar::ECase::kLastCase; c++) { auto name_case = static_cast(c); - snprintf(new_name, kMaxInputLength, "%s%s%s", obj->get_PName(name_case).c_str(), - kLiquidSeparator.c_str(), potion_name); - obj->set_PName(name_case, new_name); + obj->set_PName(name_case, fmt::format("{}{}{}", + utils::TrimRightCopy(obj->get_PName(name_case)), + kLiquidSeparator, potion_name)); } // issue #3620: имя сосуда больше не совпадает с прототипом -- без этой пометки olc при правке // прототипа восстановит исходное имя, и сосуд перестанет показывать свое содержимое. diff --git a/tests/liquid.name_roundtrip.cpp b/tests/liquid.name_roundtrip.cpp index ff220562a..2dc86c55b 100644 --- a/tests/liquid.name_roundtrip.cpp +++ b/tests/liquid.name_roundtrip.cpp @@ -65,4 +65,24 @@ TEST(LiquidNameRoundTrip, RepeatedFillsDoNotAccumulateSpaces) { << "двойных пробелов в названии быть не должно"; } +TEST(LiquidNameRoundTrip, StoredExtraSpaceHealsOnReload) { + // Имена, испорченные прежней обрезкой, уже лежат в файлах вещей: "огромная дубовая бочка" + // с хвостовым пробелом перед разделителем. Правка разделителя такое имя не чинила -- снимала + // ровно " с ", получала имя с хвостовым пробелом и приклеивала разделитель обратно, так что + // два пробела всплывали снова при каждой загрузке. Загрузка вещи гоняет ту же пару + // name_from_drinkcon + name_to_drinkcon, поэтому чиниться такое имя должно само. + const std::string original = "огромная дубовая бочка"; + const std::string liquid = drinknames[kRoundTripLiquid]; + auto jar = MakeJar(original + " с " + liquid); + jar->set_aliases(original + " " + liquid); + + name_from_drinkcon(jar.get()); + EXPECT_EQ(jar->get_short_description(), original) << "хвостовой пробел обязан уйти вместе с жидкостью"; + + name_to_drinkcon(jar.get(), kRoundTripLiquid); + EXPECT_EQ(jar->get_short_description(), original + " с " + liquid); + EXPECT_EQ(jar->get_PName(grammar::ECase::kNom), original + " с " + liquid) << "и в падежах тоже"; + EXPECT_EQ(jar->get_aliases().find(" "), std::string::npos) << "в синонимах двойных пробелов тоже быть не должно"; +} + // vim: ts=4 sw=4 tw=0 noet syntax=cpp : From 308404a7c74b78658e28f6ee53e735e830f8a6f1 Mon Sep 17 00:00:00 2001 From: stribog Date: Mon, 24 Aug 2026 10:28:34 +0200 Subject: [PATCH 106/110] fix(sight): drop the extra space before the item state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit diag_obj_to_char сама возвращает строку с ведущим пробелом, а формат вывода добавлял ещё один: предмет показывался с двумя пробелами перед состоянием -- "огромная дубовая бочка <великолепно>". Тот же лишний пробел вылезал и справа, перед всем, что дописывается следом: "сундук <хорошо> (есть содержимое)". Пробел ставит теперь только сама diag_obj_to_char, а описание наполнения ёмкости приносит свой. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XUwDWDnYdXrJdvjDVd36QH --- src/gameplay/mechanics/sight.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 591edcb4c..3606dadd0 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -1340,7 +1340,10 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s sprintf(buf2, " %s*%s%s", kColorGrn, kColorNrm, diag_obj_to_char(object, 1)); } else { - sprintf(buf2, " %s ", diag_obj_to_char(object, 1)); + // diag_obj_to_char сама начинается с пробела, а всё, что дописывается следом, + // свой пробел тоже приносит: лишний тут давал "бочка <великолепно>" и + // "сундук <хорошо> (есть содержимое)". + sprintf(buf2, "%s", diag_obj_to_char(object, 1)); if (object->get_type() == EObjType::kLiquidContainer) { char *tmp = drinkcon::daig_filling_drink(object, ch); native_text::copy_lower_char(tmp, tmp); @@ -1348,7 +1351,7 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s // меньше, чем на четверть черной вязкой *отравленной* жидкостью"), и в UTF-8 // она не влезала в 128 байт: fortify ловил переполнение и валил процесс на // осмотре ёмкости (issue #3752). - strcat(buf2, fmt::format("({})", tmp).c_str()); + strcat(buf2, fmt::format(" ({})", tmp).c_str()); } } } From 32c5696f10de2043bd9928f38f9ffa8edba16ad7 Mon Sep 17 00:00:00 2001 From: stribog Date: Thu, 27 Aug 2026 14:25:25 +0200 Subject: [PATCH 107/110] feat(sight): shorten the drinkcon line in item lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Строка ёмкости в списке разрасталась вдвое: [ 2197] огромная дубовая бочка с черным колдовским зельем <великолепно> (наполнена меньше, чем на четверть черной вязкой жидкостью) Название и так содержит жидкость, а фраза про наполнение занимает полстроки. В списке от неё остаётся только пометка "(пусто)" -- то, чего по названию не видно. Полное описание никуда не делось: "осмотреть <ёмкость>" по-прежнему печатает и наполнение, и качество жидкости. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XUwDWDnYdXrJdvjDVd36QH --- src/gameplay/mechanics/sight.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/gameplay/mechanics/sight.cpp b/src/gameplay/mechanics/sight.cpp index 3606dadd0..077aca654 100644 --- a/src/gameplay/mechanics/sight.cpp +++ b/src/gameplay/mechanics/sight.cpp @@ -1344,13 +1344,15 @@ const char *show_obj_to_char(ObjData *object, CharData *ch, int mode, int show_s // свой пробел тоже приносит: лишний тут давал "бочка <великолепно>" и // "сундук <хорошо> (есть содержимое)". sprintf(buf2, "%s", diag_obj_to_char(object, 1)); - if (object->get_type() == EObjType::kLiquidContainer) { + // В списке предметов от наполнения остаётся только пометка "(пусто)". Полная + // фраза ("наполнена меньше, чем на четверть черной вязкой жидкостью") удлиняла + // строку вдвое, а посмотреть её можно, осмотрев ёмкость. + if (object->get_type() == EObjType::kLiquidContainer + && GET_OBJ_VAL(object, 1) <= 0) { char *tmp = drinkcon::daig_filling_drink(object, ch); native_text::copy_lower_char(tmp, tmp); - // Без промежуточного буфера: описание наполнения -- длинная фраза ("наполнен - // меньше, чем на четверть черной вязкой *отравленной* жидкостью"), и в UTF-8 - // она не влезала в 128 байт: fortify ловил переполнение и валил процесс на - // осмотре ёмкости (issue #3752). + // Без промежуточного буфера: fortify ловил переполнение на длинной фразе + // и валил процесс на осмотре ёмкости (issue #3752). strcat(buf2, fmt::format(" ({})", tmp).c_str()); } } From 41eed8c23f778ab9ad06b6a9e1557f7edfdda951 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 27 Aug 2026 20:15:27 +0200 Subject: [PATCH 108/110] =?UTF-8?q?feat(utf8):=20=D0=BB=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BD=D0=B5-UTF-8=20=D0=BD=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B5=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зеркало предохранителя из to_disk, только на другом конце. Всё нативное -- валидный UTF-8; если в string_to_client_encoding пришло иное, значит файл прочитан мимо границы и игрок увидит кашу. Так уже уезжали экран справки (a06b1065a) и список синонимов (57db1fa14) -- оба нашлись по жалобам, а не проверкой. Место выбрано как единственная воронка: через эту функцию проходит весь текст ко всем клиентам, независимо от кодировки клиента. Проверено обеими сторонами: на чистой ветке прогон по читающим командам (справка, синонимы, доски, почта, склад, титулы, команды, социальные) даёт ноль срабатываний; со снятой нарочно границей чтения экрана справки предохранитель называет первый же кусок -- 'клиенту уходит не-UTF-8 (#1)'. Цена: полный разбор строки, быстрого выхода на латинице в is_valid нет. Замерено 436 нс на 208 байт, около 477 МБ/с -- для легаси-клиентов дешевле того, что тут и так делается (to_koi8 разбирает ту же строку, дальше побайтная таблица). Чего он не поймает: текст, уже прошедший через to_koi8 и превращённый в латиницу -- она валидный UTF-8. То есть ловится пропущенная граница чтения, а не порча, случившаяся раньше по пути. --- src/engine/network/descriptor_data.cpp | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/engine/network/descriptor_data.cpp b/src/engine/network/descriptor_data.cpp index 2b01b4358..454ade423 100644 --- a/src/engine/network/descriptor_data.cpp +++ b/src/engine/network/descriptor_data.cpp @@ -6,6 +6,11 @@ */ #include "descriptor_data.h" + +#include +#include +#include "utils/logger.h" +#include "utils/utf8.h" #include #include #include "utils/native_text.h" @@ -171,6 +176,30 @@ void DescriptorData::string_to_client_encoding(const char *in_str, char *out_str // поэтому перед ними текст надо привести к KOI8-R. Под KOI8-R-рантаймом это тождество, // под UTF-8 - настоящая перекодировка (issue #3681). Для UTF-8-клиента ничего приводить // не нужно: см. case kCodePageUTF8 ниже. + // Зеркало предохранителя из to_disk. Всё нативное -- валидный UTF-8; если сюда пришло + // иное, значит текст прочитан с диска мимо границы и игрок увидит кашу. Так уже уезжали + // экран справки и список синонимов (issue #3681). + // + // Проверка -- полный разбор строки, быстрого выхода на латинице в is_valid нет. Замерено: + // 436 нс на строку в 208 байт, около 477 МБ/с. Для легаси-клиентов это заметно дешевле + // того, что тут и так делается (to_koi8 разбирает ту же строку, дальше побайтная таблица), + // а объёмы вывода мада от такой скорости далеки. + if (!utf8::is_valid(in_str)) { + static std::atomic seen{0}; + const unsigned long n = seen.fetch_add(1); + if (n < 10 || n % 10000 == 0) { + std::string head; + char byte[4]; + for (std::size_t i = 0; in_str[i] && i < 16; ++i) { + std::snprintf(byte, sizeof(byte), "%02x", static_cast(in_str[i])); + head += byte; + head += ' '; + } + log("SYSERR: клиенту уходит не-UTF-8 (#%lu) -- где-то пропущена граница чтения. " + "Первые байты: %s", n + 1, head.c_str()); + } + } + std::string koi8_text; if (keytable != kCodePageUTF8) { koi8_text = native_text::to_koi8(in_str); From 7887137d17bf7dd1015d4e4f3f92339c3713f374 Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Thu, 27 Aug 2026 22:29:05 +0200 Subject: [PATCH 109/110] =?UTF-8?q?build(utf8):=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D1=83?= =?UTF-8?q?=D1=84=D0=B5=D1=80=D0=B0=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B0=20--=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20(#3681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Компилятор считает, сколько байт запишет sprintf, и сравнивает с размером буфера. Предупреждение об этом входит в -Wall, то есть лежало в логе сборки всё время -- пока по нему не начали падать боевые процессы (#3751, #3752): русский текст в UTF-8 занимает вдвое больше, чем занимал в KOI8-R, и фразы, укладывавшиеся впритык, перестали влезать. Класс коварен тем, что числа в месте вызова нет -- растёт сам литерал внутри буфера, объявленного строкой выше. Ни глазами, ни грепом по подозрительным константам такое не находится, а компилятором находится даром. Уровень по умолчанию, не =2: строгий цепляется к теоретической оценке ширины %f в heartbeat.cpp. Флаг вешается через has_argument, так что компиляторы без него (MSVC) просто его не получат. Проверено обеими сторонами: дерево собирается чисто, 666 тестов проходят; заведомое переполнение (32-байтный буфер и 104-байтный литерал) останавливает сборку -- 'directive writing 104 bytes into a region of size 32'. --- meson.build | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/meson.build b/meson.build index 92fb19750..36dae0a9a 100644 --- a/meson.build +++ b/meson.build @@ -58,6 +58,18 @@ if cpp.get_argument_syntax() == 'gcc' project_args += ['-Ofast'] endif project_args += ['-Wno-format-truncation'] + + # Переполнение буфера строкой формата -- ошибка, а не предупреждение. Компилятор считает, + # сколько байт запишет sprintf, и сравнивает с размером буфера; предупреждение об этом + # входит в -Wall и лежало в логе сборки, пока по нему не начали падать боевые процессы + # (#3751, #3752): русский текст в UTF-8 занимает вдвое больше, чем занимал в KOI8-R, + # и фразы, укладывавшиеся впритык, перестали влезать. Числа в коде при этом нет -- растёт + # сам литерал, поэтому глазами и грепом такое не ищется, а компилятором ищется даром. + # + # Уровень по умолчанию, не =2: тот цепляется к теоретической оценке ширины %f. + if cpp.has_argument('-Werror=format-overflow') + project_args += ['-Werror=format-overflow'] + endif if fortify_source # -U на случай, если тулчейн уже определил _FORTIFY_SOURCE (иначе redefine-варнинг) project_args += ['-U_FORTIFY_SOURCE', '-D_FORTIFY_SOURCE=3'] From 3a39a6aed044f4bab90cc212dec4ce158a9c5dfc Mon Sep 17 00:00:00 2001 From: Anton Gorev Date: Fri, 28 Aug 2026 04:59:38 +0200 Subject: [PATCH 110/110] =?UTF-8?q?docs:=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9=20--=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=B1=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,?= =?UTF-8?q?=20=D0=B0=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B6=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20(#3784)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTRIBUTING.md. Было «уделять особое внимание предупреждениям и исправлять при первой же возможности» -- стало требованием: перед коммитом ни одного предупреждения, достаточно своей платформы. И явный запрет заглушать: понижением уровня говорливости, -Wno-, прагмой, приведением типа. Причина названа примером: два падения боевого сервера компилятор назвал заранее, строка лежала в логе сборки, пока по ней не начали падать процессы. Плюс грабли: инкрементальная сборка предупреждения прячет, полный лог даёт только сборка с нуля. CLAUDE.md. Раздел с копией правил заменён указателем на CONTRIBUTING.md. Копия разошлась бы с оригиналом, и было бы непонятно, какая верна -- ровно то, из-за чего правки приходилось повторять дважды. Заодно в CLAUDE.md жила устаревшая строка про сборку с -Wall -Wextra без упоминания, что предупреждения запрещены. --- CLAUDE.md | 44 ++++++-------------------------------------- CONTRIBUTING.md | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c0bb98a36..c90219e68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -213,46 +213,14 @@ Access world state via `MUD::` namespace functions: - Telnet protocol with MSDP (Mud Server Data Protocol) and MCCP compression - Multiple codepage support (Alt, Win, UTF-8, KOI8-R) -## Development Guidelines (from CONTRIBUTING.md) +## Development Guidelines -### Code Style -- **Standard**: C++20 (C++17 minimum) -- **Indentation**: Tabs (size 4), no spaces for indentation -- **Braces**: An opening brace on the same line separated by a space -```cpp -if (condition) { - // code here -} -``` -- **One statement per line** (applies to variable declarations too) -- **Always use braces** for if/else/for/while bodies, even single statements -- **Pointers/References**: `*` and `&` attached to type, space after -```cpp -const char* message = "example"; -const auto& reference = message; -``` - -### File Management -- All new files must be added to `meson.build` (sources go into `main_sources` / `library_sources`, depending on the section) -- Include vim modeline at end of files: -```cpp -// vim: ts=4 sw=4 tw=0 noet syntax=cpp : -``` - -### Memory Management -- Prefer `new`/`delete` over `malloc`/`free` -- Use smart pointers (`CharData::shared_ptr`, `ObjData::shared_ptr`) where possible -- Avoid raw pointers for ownership - -### Testing -- Run unit tests before committing: `./build/tests/tests` -- Write tests for new code using GoogleTest framework -- Tests are in `tests/` directory, named by module (e.g., `char.affects.cpp`, `fight.penalties.cpp`) +**Правила разработки живут в [CONTRIBUTING.md](CONTRIBUTING.md) — читай его перед тем, как +править код, и следуй ему.** Здесь их копии нет намеренно: две копии расходятся, и потом +непонятно, какая верна. -### Compiler Warnings -- Must compile on Windows, Linux, and Cygwin -- Fix all compiler warnings, especially in new code -- Build with `-Wall -Wextra` enabled +Там про стиль (отступы табами, скобки, по одному объявлению в строке), добавление новых файлов +в `meson.build`, работу с памятью, unit-тесты и требование к сборке без предупреждений. ## Common Development Patterns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7029a85f6..50d5a7c9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,10 +22,27 @@ using meat_mapping_t = std::pair; -Код должен быть компилируемым на Windows, Linux. При компиляции вы должны -уделять особое внимание предупреждениям компилятора и исправлять их при первой -же возможности. Особенно если предупреждения появились вследствие добавленного -вами кода. +Код должен быть компилируемым на Windows, Linux. + +**Перед коммитом сборка должна быть без единого предупреждения.** Достаточно +проверить на своей платформе; смотреть сборки CI не обязательно, хотя и полезно. + +Предупреждения надо чинить по существу — понять, от чего оно, и устранить +причину. Заглушать нельзя: ни понижением уровня говорливости компилятора, ни +`-Wno-…`, ни `#pragma diagnostic ignored`, ни приведением типа ради тишины. + +Предупреждение компилятора — это бесплатный статический анализ, который знает +то, чего не найти ни глазами, ни поиском по тексту. Живой пример: два падения +боевого сервера (issue #3751 и #3752) компилятор назвал заранее строкой +`directive writing 170 bytes into a region of size 162` — русский текст в UTF-8 +занимает вдвое больше, чем занимал в KOI8-R, и фраза перестала влезать в буфер. +Предупреждение лежало в логе сборки всё время, пока по нему не начали падать +процессы. С тех пор именно это переполнение — уже ошибка сборки, а не +предупреждение (`-Werror=format-overflow` в meson.build). + +Имейте в виду: **инкрементальная сборка предупреждения прячет** — уже собранные +файлы не перекомпилируются, и лог выглядит чистым. Когда важно увидеть весь лог, +собирайте с нуля в отдельном каталоге. Старайтесь перед коммитом своих изменений запускать unit-тесты. Старайтесь писать свои unit-тесты для написанного вами кода. Часто ошибки можно обнаружить