Nickname Generator 5.2.2
Gamer-style nickname generation for C++23
Loading...
Searching...
No Matches
nicknamegen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_NICKNAMEGEN_HPP
5#define DASMIG_NICKNAMEGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <cmath>
10#include <array>
11#include <cstddef>
12#include <cstdint>
13#include <filesystem>
14#include <map>
15#include <optional>
16#include <ostream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <string_view>
21#include <system_error>
22#include <utility>
23#include <vector>
24
25#ifdef DASMIG_NICKNAME_EMBED
26#include "nicknamegen_data.hpp"
27#endif
28
29/// @file nicknamegen.hpp
30/// @brief Nickname generator library — gamer-style nickname generation for C++23.
31/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
32/// @see See doc/usage.md for the narrative tutorial.
33
34DASMIG_CORE_REQUIRE(1, 1);
35
36struct nng_test_access;
37
38namespace dasmig
39{
40
41/// @brief The kind of nickname a request asks for.
42enum class nickname_style : std::uint8_t
43{
44 gaming, ///< A gaming handle: decorated and cased freely ("xX_Nioi_Xx", "dR4g0n").
45 username, ///< A username: lowercase letters and digits, with `.` or `_` ("john.smith92").
46};
47
48/// @brief Per-call choices for nickname generation.
49///
50/// Word lists are loaded on nng; deterministic calls take a separate seed.
52{
53 /// @brief Full name to optionally derive the nickname from.
54 ///
55 /// Empty (or only spaces) selects from the loaded word lists. With a
56 /// name, one in four nicknames is derived from it; without word lists,
57 /// the name is always used.
58 std::wstring name{};
59
60 /// @brief Themes the nickname may be drawn from, as plain strings.
61 ///
62 /// Typically the tags of a character's traits and job (`"combat"`,
63 /// `"leadership"`, `"manual"`, ...), as trait-generator and
64 /// occupation-generator give them; no dependency on either. A tag that
65 /// has a `tag_<tag>.words` list is a theme, and with at least one theme
66 /// one nickname in three is a word of a theme (a theme chosen uniformly,
67 /// then a word), unless the name takes the nickname first. Tags without a
68 /// list are ignored. The default is no themes.
69 std::vector<std::string> tags{};
70
71 /// @brief The kind of nickname: a gaming handle (the default) or a username.
72 ///
73 /// A gaming handle may be decorated, leetified and cased freely. A
74 /// username is lowercase ASCII letters and digits, at most 24 characters,
75 /// with `.` or `_` between parts: from a name it is the name in a common
76 /// shape ("john.smith", "jsmith", "smith_john", "johns92"), always
77 /// derived from the name; without a name it is one or two words from the
78 /// lists ("swiftfox", "quiet_otter"), with digits about half the time.
79 /// Accents are folded to ASCII ("jose" for "José") and characters
80 /// that have no ASCII form are dropped; a name with no letters left is
81 /// treated as no name. A username sometimes gets a prefix ("real", "the",
82 /// "its") or a suffix ("official", "hq", "irl"), about one in thirteen.
83 nickname_style style{nickname_style::gaming};
84
85 /// @brief Birth year, for the digits of a username.
86 ///
87 /// With a year from 1900 to 2100, the digits of a username are often the
88 /// year, as two or four digits ("john.smith92", "jsmith1992"). Without it
89 /// they are a made-up year, a small or lucky number, or a pattern.
90 /// Ignored by the gaming style.
91 std::optional<int> birth_year{};
92};
93
94/// @brief Return type for nickname generation, holding both the transformed
95/// nickname and the original source string.
96///
97/// Supports implicit conversion to std::wstring, streaming via operator<<
98/// and JSON output via to_json().
100{
101 public:
102 /// @brief Return the original word or name used to generate this nickname.
103 /// @return Original source string before any transforms.
104 [[nodiscard]] std::wstring plain() const
105 {
106 return _original_string;
107 }
108
109 /// @brief Retrieve the random seed used to generate this nickname.
110 /// @return The per-call seed for replay.
111 /// @see nng::get_nickname(const nickname_request&, std::uint64_t)
112 [[nodiscard]] std::uint64_t seed() const
113 {
114 return _signature.seed;
115 }
116
117 /// @brief The seed and the data this nickname was generated from.
118 /// @return The replay signature; `data` is nng::data_hash() at the time
119 /// of generation.
120 [[nodiscard]] const core::seed_signature& signature() const
121 {
122 return _signature;
123 }
124
125 /// @brief Write this nickname as a JSON object
126 /// `{"nickname":…,"source":…,"signature":{…}}`.
127 /// @param w Writer positioned where a value is allowed.
128 void to_json(core::json_writer& w) const
129 {
130 w.begin_object()
131 .field("nickname", core::to_utf8(_internal_string))
132 .field("source", core::to_utf8(_original_string))
133 .key("signature");
134 _signature.to_json(w);
135 w.end_object();
136 }
137
138 /// @brief This nickname as a JSON document.
139 /// @return Compact JSON.
140 [[nodiscard]] std::string to_json() const
141 {
142 core::json_writer w;
143 to_json(w);
144 return w.str();
145 }
146
147 /// @brief Implicit conversion to std::wstring.
148 /// @return The transformed nickname string.
149 operator std::wstring() const // NOLINT(hicpp-explicit-conversions)
150 {
151 return _internal_string;
152 }
153
154 /// @brief Stream the nickname to a wide output stream.
155 /// @param wos Output stream.
156 /// @param nickname Nickname to stream.
157 /// @return Reference to the output stream.
158 friend std::wostream& operator<<(std::wostream& wos,
159 const nickname& nickname)
160 {
161 wos << nickname._internal_string;
162 return wos;
163 }
164
165 private:
166 /// @brief Private constructor — nicknames are created only by nng.
167 /// @param nickname_str The transformed nickname text.
168 /// @param original_str The original source text.
169 nickname(std::wstring nickname_str, std::wstring original_str)
170 : _internal_string(std::move(nickname_str)),
171 _original_string(std::move(original_str))
172 {
173 }
174
175 std::wstring _internal_string; ///< Current nickname after transforms.
176 std::wstring _original_string; ///< Original source string.
177 core::seed_signature _signature; ///< Seed and data hash.
178
179 friend class nng; ///< Allows nng to construct nicknames.
180};
181
182/// @brief Nickname generator that produces gamer-style nicknames.
183///
184/// Generates nicknames either from a person's name (first name, initials,
185/// mixed parts, ...) or from word lists, then decorates them (leet speak,
186/// suffixes, reversal, ...) and formats their case.
187///
188/// Output is identical on every compiler and platform (dasmig-core): the
189/// case changes use a fixed mapping rather than the C locale, and word
190/// lists are read as UTF-8 in name order.
191///
192/// Can be used as a singleton via instance() or constructed independently.
193///
194/// @par Thread safety
195/// Each instance is independent. Concurrent calls to get_nickname(request)
196/// on the **same** instance require external synchronisation. Seeded calls
197/// are read-only and can run concurrently, provided no word lists are changed.
198class nng
199{
200 public:
201 /// @brief Default constructor — creates an empty generator with no word
202 /// lists.
203 nng() = default;
204
205 nng(const nng&) = delete; ///< Not copyable.
206 nng& operator=(const nng&) = delete; ///< Not copyable.
207 nng(nng&&) noexcept = default; ///< Move constructor.
208 nng& operator=(nng&&) noexcept = default; ///< Move assignment.
209 ~nng() = default; ///< Default destructor.
210
211 /// @brief Access the global singleton instance.
212 ///
213 /// Loads the shipped word lists on first access, from compiled-in data
214 /// or an auto-probed `resources/`.
215 /// @return Reference to the global nng instance.
216 static nng& instance()
217 {
218 static nng inst{auto_probe_tag{}};
219 return inst;
220 }
221
222 /// @brief Generate a random nickname, optionally based on a name.
223 ///
224 /// With a name, one in four nicknames is derived from it (always, when
225 /// no word lists are loaded); the others come from the word lists.
226 ///
227 /// @param request Per-call choices; use {} for a word-list nickname.
228 /// @return The generated nickname.
229 /// @throws std::invalid_argument If request.name has no name parts and
230 /// no word lists are loaded.
231 [[nodiscard]] nickname get_nickname(const nickname_request& request)
232 {
233 return get_nickname(request, _engine());
234 }
235
236 /// @brief Generate a deterministic nickname.
237 /// @param request Per-call choices; use {} for a word-list nickname.
238 /// @param call_seed Seed; the same seed, request and word lists give the
239 /// same nickname.
240 /// @return The generated nickname.
241 /// @throws std::invalid_argument If request.name has no name parts and
242 /// no word lists are loaded.
243 [[nodiscard]] nickname get_nickname(const nickname_request& request,
244 std::uint64_t call_seed) const
245 {
246 auto r = core::rng{call_seed}.split("nickname");
247 auto result = solver(request, r);
248 result._signature = {.seed = call_seed, .data = data_hash()};
249 return result;
250 }
251
252 // -- Batch generation -------------------------------------------------
253
254 /// @brief Generate a batch of nickname results from a seed drawn like
255 /// get_nickname() draws its own.
256 ///
257 /// Item `i` is `get_nickname(request, core::item_seed(batch_seed, options.first + i))`
258 /// for the drawn batch seed; see the seeded overload.
259 ///
260 /// @param request What every item is generated from.
261 /// @param options Count, first index, threads and uniqueness.
262 /// @return The results, in index order.
263 [[nodiscard]] std::vector<nickname> get_nickname_batch(const nickname_request& request,
264 const core::batch_options& options)
265 {
266 return get_nickname_batch(request, options, _engine());
267 }
268
269 /// @brief Generate a batch of nickname results, deterministically.
270 ///
271 /// Item `i` is exactly `get_nickname(request, core::item_seed(batch_seed,
272 /// options.first + i))`, so an item replays on its own, a range can be
273 /// generated directly, two batches that follow each other equal one, and
274 /// the output does not depend on `options.threads`.
275 ///
276 /// @param request What every item is generated from.
277 /// @param options Count, first index, threads and uniqueness (see
278 /// core::unique_rule; the rule must be made for `nickname`).
279 /// @param batch_seed Seed of the whole batch.
280 /// @return The results, in index order.
281 /// @throws std::invalid_argument If the request or options are invalid.
282 /// @throws std::runtime_error If no data has been loaded, or a unique
283 /// batch runs out of attempts.
284 [[nodiscard]] std::vector<nickname> get_nickname_batch(const nickname_request& request,
285 const core::batch_options& options,
286 std::uint64_t batch_seed) const
287 {
288 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
289 { return get_nickname(request, item_seed); });
290 }
291
292 /// @brief Stream a batch of nickname results to a sink without holding the
293 /// whole batch.
294 ///
295 /// Generates the same items as get_nickname_batch() and calls
296 /// `sink(index, item)` in index order on the calling thread; `index`
297 /// includes `options.first`.
298 ///
299 /// @tparam Sink Callable taking `(std::uint64_t, nickname&)`; for example
300 /// core::jsonl_writer or core::csv_writer.
301 /// @param request What every item is generated from.
302 /// @param options Count, first index, threads and uniqueness.
303 /// @param batch_seed Seed of the whole batch.
304 /// @param sink Receives the items.
305 template <typename Sink>
306 void for_each_nickname(const nickname_request& request, const core::batch_options& options,
307 std::uint64_t batch_seed, Sink&& sink) const
308 {
309 core::for_each_batch(options, batch_seed,
310 [&](std::uint64_t item_seed) { return get_nickname(request, item_seed); },
311 std::forward<Sink>(sink));
312 }
313
314 /// @brief Stream a batch of nickname results with a batch seed drawn like
315 /// get_nickname() draws its own.
316 /// @tparam Sink Callable taking `(std::uint64_t, nickname&)`.
317 /// @param request What every item is generated from.
318 /// @param options Count, first index, threads and uniqueness.
319 /// @param sink Receives the items.
320 template <typename Sink>
321 void for_each_nickname(const nickname_request& request, const core::batch_options& options,
322 Sink&& sink)
323 {
324 for_each_nickname(request, options, _engine(), std::forward<Sink>(sink));
325 }
326
327
328 /// @brief Check whether any word list is loaded.
329 /// @return True when at least one non-empty word list is loaded.
330 [[nodiscard]] bool has_wordlists() const
331 {
332 return !_wordlists.empty();
333 }
334
335 /// @brief Load word lists from a directory, replacing all lists and
336 /// layers loaded before.
337 ///
338 /// Reads every `.words` file (one word per line) and `.tsv` file (a
339 /// `word` column) below the directory, in path order. The list name is
340 /// the file name without extension; files with the same name extend the
341 /// same list. A path that is not a directory is ignored.
342 ///
343 /// @param resource_path Directory such as `resources/lite`.
344 /// @throws core::tsv_error If a file is malformed; the lists are then
345 /// unchanged.
346 void load(const std::filesystem::path& resource_path)
347 {
348 std::error_code ec;
349 if (!std::filesystem::is_directory(resource_path, ec)) return;
350 apply_(read_directory_(resource_path), core::layer_mode::replace, true);
351 }
352
353 /// @brief Load the shipped word lists, replacing all lists and layers
354 /// loaded before.
355 ///
356 /// The library ships one tier: `dataset::full` loads the same lists as
357 /// `dataset::lite`. Uses the compiled-in copy when built with
358 /// `DASMIG_NICKNAME_EMBED`, otherwise searches `resources/`,
359 /// `../resources`, `nickname-generator/resources` and
360 /// `../nickname-generator/resources`.
361 ///
362 /// @param tier Ignored (one tier).
363 /// @return False when no word list was found.
364 /// @throws core::tsv_error If a file is malformed.
365 [[nodiscard]] bool load([[maybe_unused]] dataset tier)
366 {
367 std::vector<std::pair<std::string, core::tsv_document>> docs;
368 const auto root = core::find_resources(library_name_, std::string{shipped_tier_} + "/" +
369 std::string{marker_list_} + ".words");
370 for (const auto list : shipped_lists_)
371 {
372 const auto path = std::string{shipped_tier_} + "/" + std::string{list} + ".words";
373 if (auto doc = core::load_resource(path, embedded_files_(), root, {"word"}))
374 {
375 docs.emplace_back(std::string{list}, std::move(*doc));
376 }
377 }
378 if (docs.empty()) return false;
379 apply_(std::move(docs), core::layer_mode::replace, true);
380 return true;
381 }
382
383 /// @brief Add word list layers from a directory or a file.
384 ///
385 /// A directory is read like load() and all its files are applied
386 /// together, or none if any is invalid. A `.words` file has one word
387 /// per line; a `.tsv` file has a `word` column and may have `disabled`
388 /// to remove words. The list name is the file name without extension;
389 /// a new name adds a list.
390 ///
391 /// @param path Directory or file.
392 /// @param mode How the layers combine with the words loaded before.
393 /// @throws core::tsv_error If a file cannot be read or is invalid; the
394 /// lists are then unchanged.
395 /// @throws std::invalid_argument If @p path is a file that is neither
396 /// `.words` nor `.tsv`.
397 void extend(const std::filesystem::path& path,
398 core::layer_mode mode = core::layer_mode::override)
399 {
400 std::error_code ec;
401 if (std::filesystem::is_directory(path, ec))
402 {
403 apply_(read_directory_(path), mode, false);
404 return;
405 }
406 auto doc = read_file_(path);
407 if (!doc)
408 {
409 throw std::invalid_argument("extend: expected a .words or .tsv file, got " +
410 path.filename().string());
411 }
412 std::vector<std::pair<std::string, core::tsv_document>> docs;
413 docs.emplace_back(core::to_utf8(path.stem().wstring()), std::move(*doc));
414 apply_(std::move(docs), mode, false);
415 }
416
417 /// @brief Add a layer to a word list from TSV text with a `word`
418 /// column (and optionally `disabled`).
419 /// @param list Name of the list (a new name adds a list).
420 /// @param text TSV text.
421 /// @param source Name used in errors.
422 /// @param mode How the layer combines with the words loaded before.
423 /// @return What the layer did.
424 /// @throws core::tsv_error If the text is invalid; the lists are
425 /// unchanged.
426 const core::layer_info& extend_from_string(std::string_view list, std::string_view text,
427 std::string source,
428 core::layer_mode mode = core::layer_mode::override)
429 {
430 std::vector<std::pair<std::string, core::tsv_document>> docs;
431 docs.emplace_back(std::string{list}, core::tsv_document::parse(text, std::move(source)));
432 apply_(std::move(docs), mode, false);
433 return _lists.at(std::string{list}).layers().back();
434 }
435
436 /// @brief Names of the loaded word lists, in the order they are used.
437 /// @return The list names (UTF-8).
438 [[nodiscard]] std::vector<std::string> list_names() const
439 {
440 std::vector<std::string> names;
441 for (const auto& [name, table] : _lists)
442 {
443 if (!table.empty()) names.push_back(name);
444 }
445 return names;
446 }
447
448 /// @brief The layers in effect for one word list.
449 /// @param list The list name.
450 /// @return The layer records.
451 /// @throws std::out_of_range If there is no such list.
452 [[nodiscard]] const std::vector<core::layer_info>& layers(const std::string& list) const
453 {
454 return _lists.at(list).layers();
455 }
456
457 /// @brief Hash of the loaded word lists, recorded in each result's
458 /// signature().
459 /// @return The hash; 0 when nothing is loaded.
460 [[nodiscard]] std::uint64_t data_hash() const
461 {
462 std::uint64_t h = 0;
463 for (const auto& [name, table] : _lists)
464 {
465 h = core::hash_combine(core::hash_combine(h, core::fnv1a64(name)), table.data_hash());
466 }
467 return h;
468 }
469
470 private:
471 static constexpr std::string_view library_name_ = "nickname-generator";
472 static constexpr std::string_view shipped_tier_ = "lite";
473 static constexpr std::string_view marker_list_ = "animals";
474 // Lists named tag_<tag> are themes, drawn only for a request's tags.
475 static constexpr std::string_view tag_prefix_ = "tag_";
476 static constexpr std::array<std::string_view, 52> shipped_lists_{"adjectives", "animals", "colors", "food", "gems", "japanese", "myth", "nature", "space", "tag_academic", "tag_activity", "tag_art", "tag_care", "tag_caring", "tag_chaos", "tag_combat", "tag_crafting", "tag_creative", "tag_desk", "tag_discipline", "tag_dreaming", "tag_duty", "tag_exploration", "tag_faith", "tag_flight", "tag_haste", "tag_knowledge", "tag_labour", "tag_leadership", "tag_manual", "tag_medicine", "tag_morale", "tag_outdoor", "tag_physical", "tag_planning", "tag_rest", "tag_risk", "tag_routine", "tag_safety", "tag_social", "tag_solitude", "tag_solo", "tag_stealth", "tag_survival", "tag_team", "tag_technical", "tag_trade", "tag_training", "tag_violence", "tag_waiting", "tag_wealth", "tag_willpower"};
477
478 // Function pointer type for nickname transform methods.
479 using generator_fn = std::wstring (*)(const std::wstring&, core::rng&);
480
481 // Container of methods used to modify the nickname.
482 using generators = std::vector<generator_fn>;
483
484 // Characters treated as vowels (escaped so the source encoding does not
485 // matter): aeiou, then á à â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ø
486 // š ù ú û ü ý ÿ.
487 // NOLINTNEXTLINE(bugprone-throwing-static-initialization)
488 static const inline std::wstring _vowels{
489 L"aeiou\u00E1\u00E0\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB"
490 L"\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F8"
491 L"\u0161\u00F9\u00FA\u00FB\u00FC\u00FD\u00FF"};
492
493 // Maps letters to their leet-speak digit equivalents.
494 // NOLINTNEXTLINE(bugprone-throwing-static-initialization)
495 static const inline std::map<wchar_t, wchar_t> _leet_map{
496 {L'o', L'0'}, {L'O', L'0'}, {L'i', L'1'}, {L'I', L'1'}, {L's', L'2'},
497 {L'S', L'2'}, {L'e', L'3'}, {L'E', L'3'}, {L'a', L'4'}, {L'A', L'4'},
498 {L'g', L'6'}, {L'G', L'6'}, {L't', L'7'}, {L'T', L'7'}, {L'b', L'8'},
499 {L'B', L'8'}, {L'q', L'9'}, {L'Q', L'9'}};
500
501 static core::table_schema schema_()
502 {
503 return {.key = {"word"}, .required = {}, .optional = {}};
504 }
505
506 // Word lists by name (sorted, so the order does not depend on the
507 // filesystem), and the words in use, rebuilt after every change.
508 std::map<std::string, core::layered_table, std::less<>> _lists;
509 std::vector<std::vector<std::wstring>> _wordlists;
510 core::discrete _list_choice{1.0}; // over _wordlists, by the square root of their sizes
511 std::map<std::string, std::vector<std::wstring>, std::less<>> _themes; // by tag
512
513 core::rng _engine{core::rng::from_entropy()};
514
515 struct auto_probe_tag {};
516
517 explicit nng(auto_probe_tag /*tag*/)
518 {
519 (void)load(dataset::lite);
520 }
521
522 static std::span<const core::embedded_file> embedded_files_()
523 {
524#ifdef DASMIG_NICKNAME_EMBED
525 return embedded::nicknamegen::files;
526#else
527 return {};
528#endif
529 }
530
531 // -- Data ---------------------------------------------------------------
532
533 static std::optional<core::tsv_document> read_file_(const std::filesystem::path& file)
534 {
535 const auto ext = file.extension();
536 if (ext == ".words") return core::tsv_document::load(file, {"word"});
537 if (ext == ".tsv") return core::tsv_document::load(file);
538 return std::nullopt;
539 }
540
541 static std::vector<std::pair<std::string, core::tsv_document>> read_directory_(
542 const std::filesystem::path& dir)
543 {
544 std::vector<std::filesystem::path> files;
545 for (const auto& entry : std::filesystem::recursive_directory_iterator(dir))
546 {
547 if (entry.is_regular_file() &&
548 (entry.path().extension() == ".words" || entry.path().extension() == ".tsv"))
549 {
550 files.push_back(entry.path());
551 }
552 }
553 std::ranges::sort(files); // the iteration order is unspecified
554 std::vector<std::pair<std::string, core::tsv_document>> docs;
555 docs.reserve(files.size());
556 for (const auto& file : files)
557 {
558 docs.emplace_back(core::to_utf8(file.stem().wstring()), *read_file_(file));
559 }
560 return docs;
561 }
562
563 // Apply layers to copies of the lists, then commit: all or nothing.
564 // With @p reset (load), lists not in @p docs are dropped.
565 void apply_(std::vector<std::pair<std::string, core::tsv_document>> docs, core::layer_mode mode,
566 bool reset)
567 {
568 auto next = reset ? decltype(_lists){} : _lists;
569 // On reset each list's first file replaces; later files with the
570 // same name extend it.
571 std::map<std::string, bool, std::less<>> started;
572 for (auto& [name, doc] : docs)
573 {
574 auto it = next.try_emplace(name, schema_()).first;
575 auto list_mode = mode;
576 if (reset)
577 {
578 list_mode = started[name] ? core::layer_mode::override : core::layer_mode::replace;
579 }
580 started[name] = true;
581 (void)it->second.extend(std::move(doc), list_mode, [](const core::tsv_row& row) {
582 (void)core::from_utf8(row["word"]);
583 });
584 }
585 std::vector<std::vector<std::wstring>> wordlists;
586 std::map<std::string, std::vector<std::wstring>, std::less<>> themes;
587 for (const auto& [name, table] : next)
588 {
589 std::vector<std::wstring> words;
590 words.reserve(table.size());
591 for (const auto& row : table.rows()) words.push_back(core::from_utf8(row["word"]));
592 if (words.empty()) continue;
593 if (name.starts_with(tag_prefix_))
594 {
595 themes.emplace(name.substr(tag_prefix_.size()), std::move(words));
596 }
597 else
598 {
599 wordlists.push_back(std::move(words));
600 }
601 }
602 _lists = std::move(next);
603 // A list's chance grows with its size, but only by the square root, so
604 // a long list does not drown a short one and a short one does not
605 // take over from a long one.
606 if (!wordlists.empty())
607 {
608 std::vector<double> weights;
609 weights.reserve(wordlists.size());
610 for (const auto& list : wordlists) weights.push_back(std::sqrt(static_cast<double>(list.size())));
611 _list_choice = core::discrete{weights};
612 }
613 _wordlists = std::move(wordlists);
614 _themes = std::move(themes);
615 }
616
617 // -- Case mapping -----------------------------------------------------
618 //
619 // A fixed mapping for Basic Latin, Latin-1 Supplement and Latin
620 // Extended-A, so the result does not depend on the C locale. Other
621 // characters are left as they are.
622
623 static wchar_t to_upper_(wchar_t c)
624 {
625 const auto u = static_cast<std::uint32_t>(c);
626 if (u >= 0x61U && u <= 0x7AU) return static_cast<wchar_t>(u - 0x20U);
627 if (u >= 0xE0U && u <= 0xFEU && u != 0xF7U) return static_cast<wchar_t>(u - 0x20U);
628 if (u == 0xFFU) return static_cast<wchar_t>(0x178U);
629 if ((u >= 0x100U && u <= 0x12FU) || (u >= 0x132U && u <= 0x137U) ||
630 (u >= 0x14AU && u <= 0x177U))
631 {
632 return static_cast<wchar_t>(u & ~1U); // pairs: upper even, lower odd
633 }
634 if ((u >= 0x139U && u <= 0x148U) || (u >= 0x179U && u <= 0x17EU))
635 {
636 return (u % 2U == 0U) ? static_cast<wchar_t>(u - 1U) : c; // upper odd, lower even
637 }
638 return c;
639 }
640
641 static wchar_t to_lower_(wchar_t c)
642 {
643 const auto u = static_cast<std::uint32_t>(c);
644 if (u >= 0x41U && u <= 0x5AU) return static_cast<wchar_t>(u + 0x20U);
645 if (u >= 0xC0U && u <= 0xDEU && u != 0xD7U) return static_cast<wchar_t>(u + 0x20U);
646 if (u == 0x178U) return static_cast<wchar_t>(0xFFU);
647 if ((u >= 0x100U && u <= 0x12FU) || (u >= 0x132U && u <= 0x137U) ||
648 (u >= 0x14AU && u <= 0x177U))
649 {
650 return static_cast<wchar_t>(u | 1U);
651 }
652 if ((u >= 0x139U && u <= 0x148U) || (u >= 0x179U && u <= 0x17EU))
653 {
654 return (u % 2U == 1U) ? static_cast<wchar_t>(u + 1U) : c;
655 }
656 return c;
657 }
658
659 static bool is_upper_(wchar_t c) { return to_lower_(c) != c; }
660
661 // -- Decorations --------------------------------------------------------
662
663 // Add an x to either the front or back of the nickname - or both.
664 static std::wstring xfy(const std::wstring& nickname, core::rng& engine)
665 {
666 std::wstring x_nickname{nickname};
667 switch (core::uniform_int(engine, 0, 2))
668 {
669 case 0:
670 x_nickname.push_back(L'X');
671 break;
672 case 1:
673 x_nickname.insert(0, 1, L'X');
674 break;
675 default: // case 2
676 x_nickname.push_back(L'X');
677 x_nickname.insert(0, 1, L'X');
678 break;
679 }
680 return x_nickname;
681 }
682
683 // Writes the nickname backwards such as emankcin.
684 static std::wstring reverse(const std::wstring& nickname,
685 [[maybe_unused]] core::rng& engine)
686 {
687 std::wstring reverse_nickname{nickname};
688 std::ranges::reverse(reverse_nickname);
689 return reverse_nickname;
690 }
691
692 // Adds an y to the end of nickname or replace the last character if it's a
693 // vowel.
694 static std::wstring yfy(const std::wstring& nickname,
695 [[maybe_unused]] core::rng& engine)
696 {
697 std::wstring nicknamy{nickname};
698 if (_vowels.contains(nickname.back()))
699 {
700 nicknamy.back() = L'y';
701 }
702 else
703 {
704 nicknamy.push_back(L'y');
705 }
706 return nicknamy;
707 }
708
709 // Adds a number to the end of nickname.
710 static std::wstring numify(const std::wstring& nickname, core::rng& engine)
711 {
712 std::wstring nickname_with_number{nickname};
713 static constexpr int max_digit{9};
714 nickname_with_number.push_back(static_cast<wchar_t>(L'0' + core::uniform_int(engine, 1, max_digit)));
715 for (auto i = core::uniform_int(engine, 0, 3); i > 0; i--)
716 {
717 nickname_with_number.push_back(L'0');
718 }
719 return nickname_with_number;
720 }
721
722 // Adds a trace to the end of the nickname.
723 static std::wstring tracefy(const std::wstring& nickname,
724 [[maybe_unused]] core::rng& engine)
725 {
726 return nickname + L"-";
727 }
728
729 // Adds an ing to the end of nickname or replace the last character if it's
730 // a vowel.
731 static std::wstring ingify(const std::wstring& nickname,
732 [[maybe_unused]] core::rng& engine)
733 {
734 std::wstring nicknaming{nickname};
735 if (_vowels.contains(nickname.back()))
736 {
737 nicknaming.back() = L'i';
738 }
739 else
740 {
741 nicknaming.push_back(L'i');
742 }
743 nicknaming.append(L"ng");
744 return nicknaming;
745 }
746
747 // Finds an aeio vowel and duplicates it, returns same nickname if no
748 // available vowel.
749 static std::wstring duovowel(const std::wstring& nickname,
750 [[maybe_unused]] core::rng& engine)
751 {
752 static constexpr std::wstring_view simple_vowels = L"aeio";
753 std::wstring nicknamee{nickname};
754 for (const auto& vowel : simple_vowels)
755 {
756 if (auto it = std::ranges::find(nicknamee, vowel); it != nicknamee.end())
757 {
758 nicknamee.insert(it, *it);
759 break;
760 }
761 }
762 return nicknamee;
763 }
764
765 // Positions of characters that have a leet-speak digit.
766 static std::vector<std::size_t> leet_positions_(const std::wstring& text)
767 {
768 std::vector<std::size_t> positions;
769 for (std::size_t i = 0; i < text.size(); ++i)
770 {
771 if (_leet_map.contains(text[i])) positions.push_back(i);
772 }
773 return positions;
774 }
775
776 // Replaces a letter by a numerical character.
777 static std::wstring oneleet(const std::wstring& nickname, core::rng& engine)
778 {
779 std::wstring leet_nickname{nickname};
780 const auto candidates = leet_positions_(leet_nickname);
781 if (!candidates.empty())
782 {
783 const auto pos = core::pick(engine, candidates);
784 leet_nickname[pos] = _leet_map.at(leet_nickname[pos]);
785 }
786 return leet_nickname;
787 }
788
789 // Replaces up to two random letters in the nickname by their numerical
790 // leet-speak equivalents.
791 static std::wstring allleet(const std::wstring& nickname, core::rng& engine)
792 {
793 std::wstring leet_nickname{nickname};
794 static constexpr std::size_t max_replacements{2};
795 auto candidates = leet_positions_(leet_nickname);
796 core::shuffle(engine, candidates);
797 for (std::size_t n = 0; n < std::min(max_replacements, candidates.size()); ++n)
798 {
799 const auto pos = candidates[n];
800 leet_nickname[pos] = _leet_map.at(leet_nickname[pos]);
801 }
802 return leet_nickname;
803 }
804
805 // Slightly modify the nickname to add some flavor.
806 // NOLINTNEXTLINE(misc-no-recursion)
807 static nickname leetify(nickname nickname, core::rng& engine, bool force = false,
808 int depth = 0)
809 {
810 // Maximum recursion depth to prevent stack overflow on unmappable
811 // inputs.
812 static constexpr int max_depth{7};
813 // We have 1/2 chance of leetifying, force parameter overrides this.
814 if (force || core::bernoulli(engine, 0.5))
815 {
816 // When leetifying, there's 1/2 chance of using a finalizer or a
817 // random leetifier.
818 if (core::bernoulli(engine, 0.5))
819 {
820 static const generators possible_generators{
821 reverse, // emanckin
822 duovowel, // nicknamee
823 oneleet, // n1ckname
824 allleet // n1ckn4m3
825 };
826 auto before = nickname._internal_string;
827 nickname._internal_string =
828 core::pick(engine, possible_generators)(nickname, engine);
829 // If the new nickname didn't change and we haven't exceeded
830 // the depth limit, force leetify again.
831 if (depth < max_depth)
832 {
833 return leetify(nickname, engine, nickname._internal_string == before,
834 depth + 1);
835 }
836 }
837 else
838 {
839 static const generators possible_generators{
840 xfy, // nicknameX
841 reverse, // emanckin
842 yfy, // nicknamy
843 numify, // nickname2000
844 tracefy, // nickname-
845 ingify, // nicknaming
846 };
847 nickname._internal_string =
848 core::pick(engine, possible_generators)(nickname, engine);
849 }
850 }
851 return nickname;
852 }
853
854 // -- Case formats -------------------------------------------------------
855
856 // Returns the nickname with an underscore separating its original parts.
857 static std::wstring snake_case(const std::wstring& name)
858 {
859 std::wstring result;
860 result.reserve(name.size() + 4);
861 for (std::size_t i = 0; i < name.size(); ++i)
862 {
863 if (i > 0 && is_upper_(name[i])) result.push_back(L'_');
864 result.push_back(name[i]);
865 }
866 return result;
867 }
868
869 // Returns the nickname in all uppercase.
870 static std::wstring upper_case(const std::wstring& name,
871 [[maybe_unused]] core::rng& engine)
872 {
873 std::wstring upper_name{name};
874 std::ranges::for_each(upper_name, [](wchar_t& character) { character = to_upper_(character); });
875 return upper_name;
876 }
877
878 // Returns the nickname in all lowercase.
879 static std::wstring lower_case(const std::wstring& name,
880 [[maybe_unused]] core::rng& engine)
881 {
882 std::wstring lower_name{name};
883 std::ranges::for_each(lower_name, [](wchar_t& character) { character = to_lower_(character); });
884 return lower_name;
885 }
886
887 // Returns the nickname in title case.
888 static std::wstring title_case(const std::wstring& name,
889 [[maybe_unused]] core::rng& engine)
890 {
891 // Well, titlecase is actually the default.
892 return name;
893 }
894
895 // Returns the nickname in sentence case.
896 static std::wstring sentence_case(const std::wstring& name, core::rng& engine)
897 {
898 if (name.empty()) return name;
899 std::wstring sentence_name{lower_case(name, engine)};
900 sentence_name.at(0) = to_upper_(sentence_name.at(0));
901 return sentence_name;
902 }
903
904 // Returns the nickname in camel case.
905 static std::wstring camel_case(const std::wstring& name,
906 [[maybe_unused]] core::rng& engine)
907 {
908 if (name.empty()) return name;
909 std::wstring camel_name{name};
910 camel_name.at(0) = to_lower_(camel_name.at(0));
911 return camel_name;
912 }
913
914 // Returns the nickname in reverse sentence case.
915 static std::wstring reverse_sentence_case(const std::wstring& name, core::rng& engine)
916 {
917 if (name.empty()) return name;
918 std::wstring rsentence_name{lower_case(name, engine)};
919 rsentence_name.back() = to_upper_(rsentence_name.back());
920 return rsentence_name;
921 }
922
923 // Returns the nickname in bathtub case.
924 static std::wstring bathtub_case(const std::wstring& name, core::rng& engine)
925 {
926 if (name.empty()) return name;
927 std::wstring bathtub_name{lower_case(name, engine)};
928 bathtub_name.at(0) = to_upper_(bathtub_name.at(0));
929 bathtub_name.back() = to_upper_(bathtub_name.back());
930 return bathtub_name;
931 }
932
933 // Returns the nickname in winding case.
934 static std::wstring winding_case(const std::wstring& name, core::rng& engine)
935 {
936 std::wstring winding_name{lower_case(name, engine)};
937 for (std::size_t i = 0; i < winding_name.size(); i += 2)
938 {
939 winding_name[i] = to_upper_(winding_name[i]);
940 }
941 return winding_name;
942 }
943
944 // Returns the nickname with case in a random fashion.
945 static std::wstring random_case(const std::wstring& name, core::rng& engine)
946 {
947 std::wstring random_name{lower_case(name, engine)};
948 std::ranges::for_each(random_name, [&engine](wchar_t& character) {
949 if (core::bernoulli(engine, 0.5)) character = to_upper_(character);
950 });
951 return random_name;
952 }
953
954 // Returns the nickname all lower case with a single random character
955 // uppercase.
956 static std::wstring random_single_case(const std::wstring& name, core::rng& engine)
957 {
958 std::wstring random_name{lower_case(name, engine)};
959 if (random_name.empty()) return random_name;
960 auto& random_char = core::pick(engine, random_name);
961 random_char = to_upper_(random_char);
962 return random_name;
963 }
964
965 // Format nickname utilizing one of the possible cases.
966 static nickname format(nickname nickname, core::rng& engine)
967 {
968 // 1% chance of snake case. nick_name
969 static constexpr double snake_case_probability{0.01};
970 if (core::bernoulli(engine, snake_case_probability))
971 {
972 nickname._internal_string = snake_case(nickname);
973 }
974 // Repeat functions to enforce a distribution.
975 static const generators possible_generators = {
976 upper_case,
977 upper_case,
978 upper_case,
979 upper_case, // NICKNAME
980 lower_case,
981 lower_case,
982 lower_case,
983 lower_case,
984 lower_case,
985 lower_case,
986 lower_case,
987 lower_case, // nickname
988 title_case,
989 title_case, // NickName
990 sentence_case,
991 sentence_case,
992 sentence_case,
993 sentence_case,
994 sentence_case, // Nickname
995 camel_case,
996 camel_case, // nickName
997 reverse_sentence_case,
998 reverse_sentence_case, // nicknamE
999 bathtub_case,
1000 bathtub_case,
1001 bathtub_case, // NicknamE
1002 winding_case, // nIcKnAmE
1003 random_case, // niCKnaMe
1004 random_single_case // nicknaMe
1005 };
1006 nickname._internal_string = core::pick(engine, possible_generators)(nickname, engine);
1007 return nickname;
1008 }
1009
1010 // -- Name-based nicknames -------------------------------------------------
1011
1012 // Split a full name into a vector containing each name/surname.
1013 // Empty parts (from consecutive spaces) are filtered out.
1014 [[nodiscard]] static std::vector<std::wstring> split_name(const std::wstring& name)
1015 {
1016 std::vector<std::wstring> parts;
1017 std::wstring current;
1018 for (const wchar_t ch : name)
1019 {
1020 if (ch == L' ')
1021 {
1022 if (!current.empty()) parts.push_back(std::move(current));
1023 current.clear();
1024 }
1025 else
1026 {
1027 current.push_back(ch);
1028 }
1029 }
1030 if (!current.empty()) parts.push_back(std::move(current));
1031 return parts;
1032 }
1033
1034 // Returns the first name.
1035 static std::wstring first_name(const std::wstring& name,
1036 [[maybe_unused]] core::rng& engine)
1037 {
1038 return split_name(name).front();
1039 }
1040
1041 // Returns the last surname.
1042 static std::wstring last_name(const std::wstring& name,
1043 [[maybe_unused]] core::rng& engine)
1044 {
1045 return split_name(name).back();
1046 }
1047
1048 // Returns any name (until it hits a space character).
1049 static std::wstring any_name(const std::wstring& name, core::rng& engine)
1050 {
1051 const auto names_list = split_name(name);
1052 return core::pick(engine, names_list);
1053 }
1054
1055 // Returns only the name initials.
1056 static std::wstring initials(const std::wstring& name,
1057 [[maybe_unused]] core::rng& engine)
1058 {
1059 std::wstring nickname;
1060 for (const auto& part : split_name(name))
1061 {
1062 nickname.push_back(part.front());
1063 }
1064 return nickname;
1065 }
1066
1067 // Mix the last two names.
1068 static std::wstring mix_two(const std::wstring& name, core::rng& engine)
1069 {
1070 std::wstring nickname;
1071 auto names_list{split_name(name)};
1072 if (names_list.size() > 2)
1073 {
1074 names_list.erase(names_list.begin(), names_list.end() - 2);
1075 }
1076 for (const auto& part : names_list)
1077 {
1078 if (part.size() < 2)
1079 {
1080 nickname.append(part);
1081 }
1082 else
1083 {
1084 nickname.append(part.substr(0, core::uniform_int<std::size_t>(engine, 2, part.size())));
1085 }
1086 }
1087 return nickname;
1088 }
1089
1090 // Mix first name with last name initial.
1091 static std::wstring first_plus_initial(const std::wstring& name,
1092 [[maybe_unused]] core::rng& engine)
1093 {
1094 const auto names_list{split_name(name)};
1095 if (names_list.size() < 2) return names_list.front();
1096 return names_list.front() + names_list.back().front();
1097 }
1098
1099 // Mix last name with first name initial.
1100 static std::wstring initial_plus_last(const std::wstring& name,
1101 [[maybe_unused]] core::rng& engine)
1102 {
1103 const auto names_list{split_name(name)};
1104 if (names_list.size() < 2) return names_list.front();
1105 return names_list.front().front() + names_list.back();
1106 }
1107
1108 // Reduce a random part of the name.
1109 static std::wstring reduce_single_name(const std::wstring& name, core::rng& engine)
1110 {
1111 std::wstring single_name{any_name(name, engine)};
1112 if (single_name.size() > 3)
1113 {
1114 // Remove all vowel characters from the name unless it's already
1115 // small enough.
1116 single_name.erase(std::remove_if(single_name.begin() + 1, single_name.end() - 1,
1117 [&single_name](const wchar_t& character) {
1118 return (_vowels.contains(character) &&
1119 (single_name.size() > 3));
1120 }),
1121 single_name.end() - 1);
1122 }
1123 return single_name;
1124 }
1125
1126 // -- Usernames ----------------------------------------------------------
1127
1128 static constexpr std::size_t username_max_length_ = 24;
1129
1130 // Lowercase ASCII letters and digits only: accents are folded (the same
1131 // Latin ranges as the case mapping), anything else is dropped.
1132 [[nodiscard]] static std::wstring fold_ascii_(const std::wstring& text)
1133 {
1134 // Latin Extended-A (U+0100 to U+017F), one letter per code point; the
1135 // ligatures (U+0132, U+0133, U+0152, U+0153) are handled apart.
1136 static constexpr std::string_view extended_a =
1137 "aaaaaacc" "ccccccdd" "ddeeeeee" "eeeegggg" "gggghhhh" "iiiiiiii" "iiiijjkk" "klllllll"
1138 "lllnnnnn" "nnnnoooo" "oooorrrr" "rrssssss" "sstttttt" "uuuuuuuu" "uuuuwwyy" "yzzzzzzs";
1139 std::wstring folded;
1140 for (const wchar_t original : text)
1141 {
1142 const wchar_t c = to_lower_(original);
1143 const auto u = static_cast<std::uint32_t>(c);
1144 if ((u >= 0x61U && u <= 0x7AU) || (u >= 0x30U && u <= 0x39U))
1145 {
1146 folded.push_back(c);
1147 }
1148 else if (u >= 0xE0U && u <= 0xFFU)
1149 {
1150 if (u <= 0xE5U) folded.push_back(L'a');
1151 else if (u == 0xE6U) folded.append(L"ae");
1152 else if (u == 0xE7U) folded.push_back(L'c');
1153 else if (u <= 0xEBU) folded.push_back(L'e');
1154 else if (u <= 0xEFU) folded.push_back(L'i');
1155 else if (u == 0xF0U) folded.push_back(L'd');
1156 else if (u == 0xF1U) folded.push_back(L'n');
1157 else if (u <= 0xF6U || u == 0xF8U) folded.push_back(L'o');
1158 else if (u >= 0xF9U && u <= 0xFCU) folded.push_back(L'u');
1159 else if (u == 0xFDU || u == 0xFFU) folded.push_back(L'y');
1160 else if (u == 0xFEU) folded.append(L"th");
1161 }
1162 else if (u == 0xDFU)
1163 {
1164 folded.append(L"ss");
1165 }
1166 else if (u >= 0x100U && u <= 0x17FU)
1167 {
1168 if (u == 0x132U || u == 0x133U) folded.append(L"ij");
1169 else if (u == 0x152U || u == 0x153U) folded.append(L"oe");
1170 else folded.push_back(static_cast<wchar_t>(extended_a[u - 0x100U]));
1171 }
1172 }
1173 return folded;
1174 }
1175
1176 // The parts of a name that survive folding.
1177 [[nodiscard]] static std::vector<std::wstring> username_parts_(const std::wstring& name)
1178 {
1179 std::vector<std::wstring> parts;
1180 for (const auto& part : split_name(name))
1181 {
1182 if (auto folded = fold_ascii_(part); !folded.empty()) parts.push_back(std::move(folded));
1183 }
1184 return parts;
1185 }
1186
1187 // A word of a theme or a list, as a username part: short when it can be.
1188 [[nodiscard]] std::wstring username_word_(const std::vector<const std::vector<std::wstring>*>& themes,
1189 bool allow_theme, core::rng& engine) const
1190 {
1191 static constexpr double theme_probability{1.0 / 3.0};
1192 static constexpr std::size_t preferred_length = 8;
1193 static constexpr int attempts = 8;
1194 std::wstring word;
1195 for (int attempt = 0; attempt < attempts; ++attempt)
1196 {
1197 const std::vector<std::wstring>* list = nullptr;
1198 if (allow_theme && !themes.empty() &&
1199 (_wordlists.empty() || core::bernoulli(engine, theme_probability)))
1200 {
1201 list = core::pick(engine, themes);
1202 }
1203 else if (!_wordlists.empty())
1204 {
1205 list = &_wordlists[_list_choice(engine)];
1206 }
1207 else
1208 {
1209 throw std::invalid_argument("Received no name and word lists are empty");
1210 }
1211 word = fold_ascii_(core::pick(engine, *list));
1212 if (!word.empty() && word.size() <= preferred_length) return word;
1213 }
1214 return word.empty() ? L"user" : word;
1215 }
1216
1217 // Picks an index by weights that add up to 100.
1218 template <std::size_t N>
1219 [[nodiscard]] static std::size_t weighted_index_(core::rng& engine, const std::array<int, N>& weights)
1220 {
1221 auto r = core::uniform_int(engine, 0, 99);
1222 for (std::size_t i = 0; i < N; ++i)
1223 {
1224 if (r < weights[i]) return i;
1225 r -= weights[i];
1226 }
1227 return N - 1;
1228 }
1229
1230 // Two digits of a year, or four.
1231 [[nodiscard]] static std::wstring year_digits_(int year, bool two)
1232 {
1233 if (!two) return std::to_wstring(year);
1234 const int yy = year % 100;
1235 return (yy < 10 ? L"0" : L"") + std::to_wstring(yy);
1236 }
1237
1238 // Digits at the end of a username: a year, a small or lucky number, a
1239 // pattern, or none.
1240 [[nodiscard]] static std::wstring username_digits_(const std::optional<int>& birth_year,
1241 core::rng& engine)
1242 {
1243 static constexpr std::array<std::wstring_view, 9> lucky{L"7", L"13", L"21", L"23", L"42",
1244 L"77", L"88", L"99", L"100"};
1245 static constexpr std::array<std::wstring_view, 8> patterns{L"01", L"007", L"123", L"1234",
1246 L"11", L"12", L"00", L"10"};
1247 const bool known = birth_year && *birth_year >= 1900 && *birth_year <= 2100;
1248 // none, two-digit year, four-digit year, small, lucky, pattern, long
1249 static constexpr std::array<int, 7> unknown_weights{40, 20, 4, 14, 6, 6, 10};
1250 static constexpr std::array<int, 7> known_weights{30, 28, 14, 8, 5, 5, 10};
1251 std::wstring digits;
1252 switch (weighted_index_(engine, known ? known_weights : unknown_weights))
1253 {
1254 case 0:
1255 return {};
1256 case 1:
1257 digits = year_digits_(known ? *birth_year : 1970 + core::uniform_int(engine, 0, 39), true);
1258 break;
1259 case 2:
1260 digits = year_digits_(known ? *birth_year : core::uniform_int(engine, 1970, 2008), false);
1261 break;
1262 case 3:
1263 digits = std::to_wstring(core::uniform_int(engine, 1, 99));
1264 break;
1265 case 4:
1266 digits = std::wstring{core::pick(engine, lucky)};
1267 break;
1268 case 5:
1269 digits = std::wstring{core::pick(engine, patterns)};
1270 break;
1271 default:
1272 digits = std::to_wstring(core::uniform_int(engine, 100, 9999));
1273 break;
1274 }
1275 if (core::bernoulli(engine, 0.12)) digits.insert(0, 1, L'_');
1276 return digits;
1277 }
1278
1279 // Words that sit between the parts of a name and are not a name of their own.
1280 [[nodiscard]] static bool is_particle_(const std::wstring& part)
1281 {
1282 static const std::array<std::wstring_view, 22> particles{
1283 L"da", L"das", L"de", L"del", L"della", L"di", L"do", L"dos", L"du", L"la", L"le",
1284 L"van", L"von", L"der", L"den", L"ten", L"ter", L"bin", L"ibn", L"al", L"el", L"y"};
1285 return std::ranges::find(particles, part) != particles.end();
1286 }
1287
1288 // The name in one of the shapes usernames take.
1289 [[nodiscard]] static std::wstring username_from_name_(const std::vector<std::wstring>& parts,
1290 core::rng& engine)
1291 {
1292 // The name's parts without the particles between them ("da", "von", ...).
1293 std::vector<std::wstring> names;
1294 for (std::size_t i = 0; i < parts.size(); ++i)
1295 {
1296 const bool inner = i > 0 && i + 1 < parts.size();
1297 if (!(inner && is_particle_(parts[i]))) names.push_back(parts[i]);
1298 }
1299 const std::wstring& given = names.front();
1300 if (names.size() == 1) return given;
1301
1302 // With three or more names, the middle one is a middle name or the first
1303 // of two family names, and the family name may be either of the last two.
1304 std::wstring family = names.back();
1305 std::wstring middle;
1306 if (names.size() >= 3)
1307 {
1308 const bool middle_name = core::bernoulli(engine, 0.5);
1309 if (middle_name)
1310 {
1311 middle = names[1];
1312 }
1313 else if (core::bernoulli(engine, 0.4))
1314 {
1315 family = names[names.size() - 2];
1316 }
1317 }
1318 const std::wstring g = given.substr(0, 1);
1319 const std::wstring f = family.substr(0, 1);
1320
1321 if (!middle.empty() && core::bernoulli(engine, 0.4))
1322 {
1323 const std::wstring m = middle.substr(0, 1);
1324 switch (core::uniform_int(engine, 0, 5))
1325 {
1326 case 0: return given + L"." + m + L"." + family; // john.m.smith
1327 case 1: return given + m + family; // johnmsmith
1328 case 2: return g + m + family; // jmsmith
1329 case 3: return given + L"_" + m + L"_" + family; // john_m_smith
1330 case 4: return g + m + L"." + family; // jm.smith
1331 default: return given + middle; // johnmichael
1332 }
1333 }
1334
1335 // given.family, givenfamily, given_family, gfamily, g.family, given.f,
1336 // givenf, family.given, family_given, familyg
1337 static constexpr std::array<int, 10> weights{22, 17, 9, 14, 5, 5, 6, 8, 5, 9};
1338 switch (weighted_index_(engine, weights))
1339 {
1340 case 0: return given + L"." + family;
1341 case 1: return given + family;
1342 case 2: return given + L"_" + family;
1343 case 3: return g + family;
1344 case 4: return g + L"." + family;
1345 case 5: return given + L"." + f;
1346 case 6: return given + f;
1347 case 7: return family + L"." + given;
1348 case 8: return family + L"_" + given;
1349 default: return family + g;
1350 }
1351 }
1352
1353 // A prefix or a suffix, about one username in thirteen.
1354 static void username_affix_(std::wstring& base, core::rng& engine)
1355 {
1356 static constexpr std::array<std::wstring_view, 6> prefixes{L"the", L"real", L"its", L"iam", L"hey", L"just"};
1357 static constexpr std::array<std::wstring_view, 6> suffixes{L"official", L"real", L"hq", L"irl", L"online", L"hi"};
1358 if (!core::bernoulli(engine, 1.0 / 13.0)) return;
1359 if (core::bernoulli(engine, 0.5))
1360 {
1361 const std::wstring prefix{core::pick(engine, prefixes)};
1362 base = prefix + (core::bernoulli(engine, 0.3) ? L"_" : L"") + base;
1363 }
1364 else
1365 {
1366 const std::wstring suffix{core::pick(engine, suffixes)};
1367 base += (core::bernoulli(engine, 0.6) ? L"_" : L"") + suffix;
1368 }
1369 }
1370
1371 // A username: the name in a common shape, or one or two words.
1372 [[nodiscard]] nickname username_(const nickname_request& request, core::rng& engine) const
1373 {
1374 std::wstring original;
1375 std::wstring base;
1376 if (const auto parts = username_parts_(request.name); !parts.empty())
1377 {
1378 original = request.name;
1379 base = username_from_name_(parts, engine);
1380 }
1381 else
1382 {
1383 const auto themes = themes_of_(request.tags);
1384 std::wstring first = username_word_(themes, true, engine);
1385 original = first;
1386 base = first;
1387 if (core::bernoulli(engine, 0.5))
1388 {
1389 const std::wstring second = username_word_(themes, false, engine);
1390 original += L" " + second;
1391 switch (core::uniform_int(engine, 0, 4))
1392 {
1393 case 0: case 1: base += second; break;
1394 case 2: case 3: base += L"_" + second; break;
1395 default: base += L"." + second; break;
1396 }
1397 }
1398 }
1399 username_affix_(base, engine);
1400
1401 const std::wstring digits = username_digits_(request.birth_year, engine);
1402 if (base.size() + digits.size() > username_max_length_)
1403 {
1404 base.resize(username_max_length_ - std::min(digits.size(), username_max_length_ - 1));
1405 while (base.size() > 1 && (base.back() == L'.' || base.back() == L'_')) base.pop_back();
1406 }
1407 return {base + digits, original};
1408 }
1409
1410 // The theme lists for a request's tags, each once, in the request's order;
1411 // tags without a list are left out.
1412 [[nodiscard]] std::vector<const std::vector<std::wstring>*> themes_of_(
1413 const std::vector<std::string>& tags) const
1414 {
1415 std::vector<const std::vector<std::wstring>*> themes;
1416 for (const auto& tag : tags)
1417 {
1418 const auto it = _themes.find(tag);
1419 if (it == _themes.end()) continue;
1420 if (std::ranges::find(themes, &it->second) == themes.end()) themes.push_back(&it->second);
1421 }
1422 return themes;
1423 }
1424
1425 // Contains logic to generate a random nickname optionally based on the
1426 // player full name.
1427 [[nodiscard]] nickname solver(const nickname_request& request, core::rng& engine) const
1428 {
1429 if (request.style == nickname_style::username) return username_(request, engine);
1430 const std::wstring& name = request.name;
1431 std::wstring original;
1432 std::wstring nick;
1433 // 1/4 chance of nickname being name related.
1434 static constexpr double name_related_probability{0.25};
1435 // 1/3 chance of a themed word when the request has a theme (always, if no
1436 // plain list is loaded).
1437 static constexpr double theme_probability{1.0 / 3.0};
1438 // Always use the name if there are no word lists to fall back to.
1439 const bool has_name = !split_name(name).empty();
1440 const bool use_name =
1441 has_name && (_wordlists.empty() || core::bernoulli(engine, name_related_probability));
1442 if (use_name)
1443 {
1444 // Purposefully adds redundancy to first and last name with any name
1445 // to add double weight to them.
1446 static const generators possible_generators{
1447 first_name, // John
1448 last_name, // Doe
1449 any_name, // Smith
1450 initials, // JSD
1451 mix_two, // DoSmi
1452 initial_plus_last, // JSmith
1453 first_plus_initial, // JohnS
1454 reduce_single_name // Jhn
1455 };
1456 original = name;
1457 nick = core::pick(engine, possible_generators)(name, engine);
1458 }
1459 else if (const auto themes = themes_of_(request.tags);
1460 !themes.empty() && (_wordlists.empty() || core::bernoulli(engine, theme_probability)))
1461 {
1462 const auto* drawn_theme = core::pick(engine, themes);
1463 nick = original = core::pick(engine, *drawn_theme);
1464 }
1465 else if (!_wordlists.empty())
1466 {
1467 const auto& drawn_wordlist = _wordlists[_list_choice(engine)];
1468 nick = original = core::pick(engine, drawn_wordlist);
1469 }
1470 else
1471 {
1472 throw std::invalid_argument("Received no name and word lists are empty");
1473 }
1474 return format(leetify({nick, original}, engine), engine);
1475 }
1476
1477 friend struct ::nng_test_access;
1478};
1479
1480} // namespace dasmig
1481
1482#endif // DASMIG_NICKNAMEGEN_HPP
Return type for nickname generation, holding both the transformed nickname and the original source st...
const core::seed_signature & signature() const
The seed and the data this nickname was generated from.
friend std::wostream & operator<<(std::wostream &wos, const nickname &nickname)
Stream the nickname to a wide output stream.
void to_json(core::json_writer &w) const
Write this nickname as a JSON object {"nickname":…,"source":…,"signature":{…}}.
std::wstring plain() const
Return the original word or name used to generate this nickname.
std::string to_json() const
This nickname as a JSON document.
std::uint64_t seed() const
Retrieve the random seed used to generate this nickname.
Nickname generator that produces gamer-style nicknames.
static nng & instance()
Access the global singleton instance.
const core::layer_info & extend_from_string(std::string_view list, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a layer to a word list from TSV text with a word column (and optionally disabled).
nng & operator=(const nng &)=delete
Not copyable.
std::vector< nickname > get_nickname_batch(const nickname_request &request, const core::batch_options &options)
Generate a batch of nickname results from a seed drawn like get_nickname() draws its own.
std::uint64_t data_hash() const
Hash of the loaded word lists, recorded in each result's signature().
bool has_wordlists() const
Check whether any word list is loaded.
bool load(dataset tier)
Load the shipped word lists, replacing all lists and layers loaded before.
void load(const std::filesystem::path &resource_path)
Load word lists from a directory, replacing all lists and layers loaded before.
nng(const nng &)=delete
Not copyable.
nickname get_nickname(const nickname_request &request)
Generate a random nickname, optionally based on a name.
const std::vector< core::layer_info > & layers(const std::string &list) const
The layers in effect for one word list.
void for_each_nickname(const nickname_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of nickname results with a batch seed drawn like get_nickname() draws its own.
std::vector< std::string > list_names() const
Names of the loaded word lists, in the order they are used.
std::vector< nickname > get_nickname_batch(const nickname_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of nickname results, deterministically.
nng()=default
Default constructor — creates an empty generator with no word lists.
nickname get_nickname(const nickname_request &request, std::uint64_t call_seed) const
Generate a deterministic nickname.
nng(nng &&) noexcept=default
Move constructor.
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add word list layers from a directory or a file.
void for_each_nickname(const nickname_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of nickname results to a sink without holding the whole batch.
nickname_style
The kind of nickname a request asks for.
@ username
A username: lowercase letters and digits, with . or _ ("john.smith92").
@ gaming
A gaming handle: decorated and cased freely ("xX_Nioi_Xx", "dR4g0n").
Per-call choices for nickname generation.
nickname_style style
The kind of nickname: a gaming handle (the default) or a username.
std::optional< int > birth_year
Birth year, for the digits of a username.
std::wstring name
Full name to optionally derive the nickname from.
std::vector< std::string > tags
Themes the nickname may be drawn from, as plain strings.