17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 06:19:19 +02:00
Files
maszyna/utilities/utilities.cpp
pawelw 51521a135a Text displays: accept characters above 127, in windows-1250 or utf-8 (#198)
* Text displays: accept characters above 127, in windows-1250 or utf-8

A text display (sub-model of type "text") looks up the sub-model of each character
in a 256 entry table, and indexed it with a plain char. With char being signed, any
character above 127 gave a negative index: reading the text went outside the table,
and so did filling the table if a character sub-model had such a name. In effect the
displays could only show ASCII, and a text with national characters could crash.

The lookup is now keyed by unicode code point. Both the displayed text and the names
of the character sub-models are decoded as utf-8 if they are valid utf-8, and as
windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8
ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident
(two upper case accented letters in a row will do); when both readings are possible
the one which finds more of its characters in the model is used.

The rest is unchanged: the first character of the text still isn't displayed, the
characters are still drawn in the translucent pass only. A missing text pointer no
longer gets dereferenced.

* Text displays: prepare the characters once for each text

The sequence of character sub-models was cached for the last text only, so
displays of several instances drawn in turn decoded their texts again on
every draw. It is now kept for each text the display shows; the texts of the
instances don't change, so a draw costs a single lookup.

Also the comments added in the renderers are now in English.

* Text displays: move the text decoding to utilities

utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit
next to win1250_to_utf8(). The existing function maps only the Polish letters
and passes other bytes above 127 through, so it can't serve the displays.

* Text displays: address the SonarCloud findings

std::array instead of the C-style arrays, the utf-8 sequence length worked
out in a function of its own instead of nested conditional operators,
std::byte for the bit operations, std::ranges::count_if and contains().
The character loop of the renderers moved to Render_Alpha_text(), which
takes the nesting back to the limit, and the static pasText is accessed
through the class.
2026-10-03 12:05:32 +02:00

672 lines
19 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
/*
MaSzyna EU07 - SPKS
Brakes.
Copyright (C) 2007-2014 Maciej Cierniak
*/
//#include "stdafx.h"
//
//#include <sys/types.h>
//#include <sys/stat.h>
#include <charconv>
#include <ranges>
//#ifndef WIN32
//#include <unistd.h>
//#endif
//
//#ifdef WIN32
//#define stat _stat
//#endif
#include "utilities/utilities.h"
#include "utilities/Globals.h"
#include "utilities/parser.h"
#include "utilities/U8.h"
#include "utilities/Logs.h"
// TODO: This shouldn't be in Globals?
bool DebugModeFlag = false;
bool FreeFlyModeFlag = false;
bool EditorModeFlag = false;
bool DebugCameraFlag = false;
bool DebugTractionFlag = false;
std::string Now()
{
#if defined(__APPLE__)
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm tm{};
localtime_r(&t, &tm);
char buf[64];
std::strftime(buf, sizeof(buf), "%c", &tm);
return std::string(buf);
#else
auto now = std::chrono::system_clock::now();
auto local = std::chrono::current_zone()->to_local(now);
return std::format("{:%c}", local);
#endif
}
std::filesystem::path user_config_path(const std::string &filename)
{
namespace fs = std::filesystem;
#if defined(_WIN32)
if (const char *appdata = std::getenv("APPDATA"))
return fs::path(appdata) / "MaSzyna" / filename;
#elif defined(__APPLE__)
if (const char *home = std::getenv("HOME"))
return fs::path(home) / "Library" / "Application Support" / "MaSzyna" / filename;
#else
if (const char *home = std::getenv("HOME"))
return fs::path(home) / ".config" / "MaSzyna" / filename;
#endif
return {}; // env var missing → caller falls back to CWD-relative filename
}
// zwraca różnicę czasu
// jeśli pierwsza jest aktualna, a druga rozkładowa, to ujemna oznacza opóżnienie
// na dłuższą metę trzeba uwzględnić datę, jakby opóżnienia miały przekraczać 12h (towarowych)
double CompareTime(double t1h, double t1m, double t2h, double t2m)
{
if (t2h < 0)
return 0;
else
{
auto t = (t2h - t1h) * 60 + t2m - t1m; // jeśli t2=00:05, a t1=23:50, to różnica wyjdzie ujemna
if (t < -720) // jeśli różnica przekracza 12h na minus
t = t + 1440; // to dodanie doby minut;else
if (t > 720) // jeśli przekracza 12h na plus
t = t - 1440; // to odjęcie doby minut
return t;
}
}
bool SetFlag(int &Flag, int const Value)
{
if (Value > 0)
{
if (false == TestFlag(Flag, Value))
{
Flag |= Value;
return true; // true, gdy było wcześniej 0 i zostało ustawione
}
}
else if (Value < 0)
{
// Value jest ujemne, czyli zerowanie flagi
return ClearFlag(Flag, -Value);
}
return false;
}
bool ClearFlag(int &Flag, int const Value)
{
if (true == TestFlag(Flag, Value))
{
Flag &= ~Value;
return true;
}
else
{
return false;
}
}
double Random(double min, double max)
{
if (max < min) { std::swap(min, max); } // std::uniform_real_distribution requires min <= max (inverted bounds are UB)
std::uniform_real_distribution<double> dist(min, max);
return dist(Global.random_engine);
}
int Random(int min, int max)
{
if (max < min) { std::swap(min, max); } // std::uniform_int_distribution requires min <= max (inverted bounds are UB)
std::uniform_int_distribution<int> dist(min, max);
return dist(Global.random_engine);
}
std::uint64_t true_random_seed()
{
try
{
std::random_device rd;
if (rd.entropy() > 0.0)
{
// NOTE: both halves have to be 64 bit wide before the shift - shifting a std::uint32_t
// by 32 is undefined behaviour, and drops the high half of the seed
std::uint64_t const high{rd()};
std::uint64_t const low{rd()};
return (high << 32) | low;
}
}
catch (std::exception const &Error)
{
// std::random_device throws when the machine has no usable hardware entropy source;
// the clock below stands in for it, which is worth saying out loud since it is a
// weaker seed than the caller asked for
WriteLog(std::format("random: no hardware entropy source ({}), seeding from the clock instead", Error.what()));
}
return static_cast<std::uint64_t>(std::chrono::high_resolution_clock::now().time_since_epoch().count());
}
std::uint32_t seed_of(std::string const &Text)
{
if (std::uint32_t number{}; std::from_chars(Text.data(), Text.data() + Text.size(), number).ec == std::errc{})
{
return number;
}
std::uint32_t hash{};
for (auto const character : Text)
{
hash = hash * 31u + static_cast<std::uint32_t>(static_cast<unsigned char>(character));
}
return hash;
}
std::string generate_uuid_v4()
{
std::uniform_int_distribution<int> dist(0, 255);
std::array<uint8_t, 16> bytes;
for (auto &b : bytes)
b = static_cast<uint8_t>(dist(Global.random_engine));
// UUID v4 (RFC 4122)
bytes[6] = bytes[6] & 0x0F | 0x40;
bytes[8] = bytes[8] & 0x3F | 0x80;
char buf[37]; // 36 znaków + \0
std::snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
return std::string(buf);
}
double LocalRandom(double a, double b)
{
if (b < a) { std::swap(a, b); } // std::uniform_real_distribution requires a <= b (inverted bounds are UB)
std::uniform_real_distribution<double> dist(a, b);
return dist(Global.local_random_engine);
}
bool FuzzyLogic(double Test, double Threshold, double Probability)
{
if (Test > Threshold && !DebugModeFlag)
return Random() < Probability * Threshold * 1.0 / Test /*im wiekszy Test tym wieksza szansa*/;
else
return false;
}
bool FuzzyLogicAI(double Test, double Threshold, double Probability)
{
if (Test > Threshold)
return Random() < Probability * Threshold * 1.0 / Test /*im wiekszy Test tym wieksza szansa*/;
else
return false;
}
std::vector<std::string> Split(std::string_view s, char delim)
{ // dzieli tekst na wektor tekstow
std::vector<std::string> out;
for (const auto& part : s | std::ranges::views::split(delim))
out.emplace_back(part.begin(), part.end());
return out;
}
std::pair<std::string, int> split_string_and_number(std::string const &Key)
{
auto const indexstart{Key.find_first_of("-1234567890")};
auto const indexend{Key.find_first_not_of("-1234567890", indexstart)};
if (indexstart != std::string::npos)
{
return {Key.substr(0, indexstart), std::stoi(Key.substr(indexstart, indexend - indexstart))};
}
return {Key, 0};
}
std::string to_string(int Value, int width)
{
std::ostringstream o;
o << std::setw(width) << Value;
return std::move(o).str();
};
std::string to_string(double Value, int precision)
{
std::ostringstream o;
o << std::fixed << std::setprecision(precision) << Value;
return std::move(o).str();
};
std::string to_string(double const Value, int const Precision, int const Width)
{
std::ostringstream o;
o << std::setw(Width) << std::fixed << std::setprecision(Precision) << Value;
return std::move(o).str();
};
std::string to_hex_str(int const Value, int const Width)
{
std::ostringstream o;
o << "0x" << std::uppercase << std::setfill('0') << std::setw(Width) << std::hex << Value;
return o.str();
};
std::string const fractionlabels[] = {U8(" "), U8("¹"), U8("²"), U8("³"), U8("⁴"), U8("⁵"), U8("⁶"), U8("⁷"), U8("⁸"), U8("⁹")};
std::string to_minutes_str(float const Minutes, bool const Leadingzero, int const Width)
{
float minutesintegral;
auto const minutesfractional{std::modf(Minutes, &minutesintegral)};
auto const width{Width - 1};
auto minutes = std::string(width - 1, ' ') + (Leadingzero ? std::to_string(100 + minutesintegral).substr(1, 2) : to_string(minutesintegral, 0));
return minutes.substr(minutes.size() - width, width) + fractionlabels[static_cast<int>(std::floor(minutesfractional * 10 + 0.1))];
}
int stol_def(const std::string &str, const int &DefaultValue)
{
int result{DefaultValue};
std::stringstream converter;
converter << str;
converter >> result;
return result;
}
std::string ToLower(std::string const &text)
{
auto lowercase{text};
std::transform(std::begin(text), std::end(text), std::begin(lowercase), [](unsigned char c) { return std::tolower(c); });
return lowercase;
}
std::string ToUpper(std::string const &text)
{
auto uppercase{text};
std::transform(std::begin(text), std::end(text), std::begin(uppercase), [](unsigned char c) { return std::toupper(c); });
return uppercase;
}
// replaces polish letters with basic ascii
void win1250_to_ascii(std::string &Input)
{
std::unordered_map<char, char> const charmap{{165, 'A'}, {198, 'C'}, {202, 'E'}, {163, 'L'}, {209, 'N'}, {211, 'O'}, {140, 'S'}, {143, 'Z'}, {175, 'Z'},
{185, 'a'}, {230, 'c'}, {234, 'e'}, {179, 'l'}, {241, 'n'}, {243, 'o'}, {156, 's'}, {159, 'z'}, {191, 'z'}};
std::unordered_map<char, char>::const_iterator lookup;
for (auto &input : Input)
{
if ((lookup = charmap.find(input)) != charmap.end())
input = lookup->second;
}
}
std::string win1250_to_utf8(const std::string &Input)
{
std::unordered_map<char, std::string> const charmap{{165, "Ą"}, {198, "Ć"}, {202, "Ę"}, {163, "Ł"}, {209, "Ń"}, {211, "Ó"}, {140, "Ś"}, {143, "Ź"}, {175, "Ż"},
{185, "ą"}, {230, "ć"}, {234, "ę"}, {179, "ł"}, {241, "ń"}, {243, "ó"}, {156, "ś"}, {159, "ź"}, {191, "ż"}};
std::string output;
std::unordered_map<char, std::string>::const_iterator lookup;
for (auto &input : Input)
{
if ((lookup = charmap.find(input)) != charmap.end())
output += lookup->second;
else
output += input;
}
return output;
}
namespace
{
// unicode code points of the upper half of the windows-1250 code page; 0 marks the unassigned bytes
std::array<char32_t, 128> const win1250_upperhalf{
0x20AC, 0x0000, 0x201A, 0x0000, 0x201E, 0x2026, 0x2020, 0x2021, 0x0000, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179,
0x0000, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0000, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A,
0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B,
0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C,
0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E,
0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF,
0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F,
0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9};
// returns number of bytes of the utf-8 sequence started by provided byte, or 0 if it can't start one
std::size_t utf8_sequence_length(std::byte const Lead)
{
if ((Lead & std::byte{0x80}) == std::byte{0x00})
{
return 1;
}
if ((Lead & std::byte{0xE0}) == std::byte{0xC0})
{
return 2;
}
if ((Lead & std::byte{0xF0}) == std::byte{0xE0})
{
return 3;
}
if ((Lead & std::byte{0xF8}) == std::byte{0xF0})
{
return 4;
}
return 0; // a continuation byte, or a lead of a sequence utf-8 doesn't allow
}
} // namespace
bool utf8_to_utf32(std::string const &Text, std::u32string &Output)
{
// lowest code point which needs given number of bytes; shorter values are overlong forms
static std::array<char32_t, 5> const minimum{0, 0, 0x80, 0x800, 0x10000};
Output.clear();
auto const size{Text.size()};
for (std::size_t idx = 0; idx < size;)
{
auto const lead{static_cast<std::byte>(Text[idx])};
auto const length{utf8_sequence_length(lead)};
if (length == 0 || idx + length > size)
{
return false;
}
auto codepoint{std::to_integer<char32_t>(lead & (std::byte{0xFF} >> (length > 1 ? length + 1 : 1)))};
for (std::size_t offset = 1; offset < length; ++offset)
{
auto const next{static_cast<std::byte>(Text[idx + offset])};
if ((next & std::byte{0xC0}) != std::byte{0x80})
{
return false;
}
codepoint = (codepoint << 6) | std::to_integer<char32_t>(next & std::byte{0x3F});
}
if (codepoint < minimum[length] || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF))
{
return false;
}
Output += codepoint;
idx += length;
}
return true;
}
void win1250_to_utf32(std::string const &Text, std::u32string &Output)
{
Output.clear();
for (auto const character : Text)
{
auto const value{std::to_integer<std::size_t>(static_cast<std::byte>(character))};
Output += (value < 0x80 ? static_cast<char32_t>(value) : win1250_upperhalf[value - 0x80]);
}
}
// Ra: tymczasowe rozwiązanie kwestii zagranicznych (czeskich) napisów
char charsetconversiontable[] = "E?,?\"_++?%S<STZZ?`'\"\".--??s>stzz"
" ^^L$A|S^CS<--RZo±,l'uP.,as>L\"lz"
"RAAAALCCCEEEEIIDDNNOOOOxRUUUUYTB"
"raaaalccceeeeiiddnnoooo-ruuuuyt?";
// wycięcie liter z ogonkami
std::string Bezogonkow(std::string Input, bool const Underscorestospaces)
{
char const extendedcharsetbit{static_cast<char>(0x80)};
char const space{' '};
char const underscore{'_'};
for (auto &input : Input)
{
if (input & extendedcharsetbit)
{
input = charsetconversiontable[input ^ extendedcharsetbit];
}
else if (input < space)
{
input = space;
}
else if (Underscorestospaces && input == underscore)
{
input = space;
}
}
return Input;
}
template <> bool extract_value(bool &Variable, std::string const &Key, std::string const &Input, std::string const &Default)
{
auto value = extract_value(Key, Input);
if (false == value.empty())
{
// set the specified variable to retrieved value
Variable = ToLower(value) == "yes";
return true; // located the variable
}
else
{
// set the variable to provided default value
if (false == Default.empty())
{
Variable = ToLower(Default) == "yes";
}
return false; // couldn't locate the variable in provided input
}
}
bool FileExists(std::string const &Filename)
{
return std::filesystem::exists(Filename);
}
std::pair<std::string, std::string> FileExists(std::vector<std::string> const &Names, std::vector<std::string> const &Extensions)
{
for (auto const &name : Names)
{
for (auto const &extension : Extensions)
{
if (FileExists(name + extension))
{
return {name, extension};
}
}
}
// nothing found
return {{}, {}};
}
// returns time of last modification for specified file
std::time_t last_modified(std::string const &Filename)
{
std::string fn = Filename;
struct stat filestat;
if (::stat(fn.c_str(), &filestat) == 0)
return filestat.st_mtime;
else
return 0;
}
std::tm local_tm(std::time_t Time)
{
std::tm tm{};
#ifdef _WIN32
localtime_s(&tm, &Time);
#else
localtime_r(&Time, &tm);
#endif
return tm;
}
std::tm utc_tm(std::time_t Time)
{
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &Time);
#else
gmtime_r(&Time, &tm);
#endif
return tm;
}
// potentially erases file extension from provided file name. returns: true if extension was removed, false otherwise
bool erase_extension(std::string &Filename)
{
auto const extensionpos{Filename.rfind('.')};
if (extensionpos == std::string::npos)
{
return false;
}
if (extensionpos != Filename.rfind("..") + 1)
{
// we can get extension for .mat or, in legacy files, some image format. just trim it and set it to material file extension
Filename.erase(extensionpos);
return true;
}
return false;
}
void erase_leading_slashes(std::string &Filename)
{
auto pos = Filename.find_first_not_of('/');
Filename.erase(0, pos);
}
// potentially replaces backward slashes in provided file path with unix-compatible forward slashes
void replace_slashes(std::string &Filename)
{
std::ranges::replace(Filename, '\\', '/');
}
// returns potential path part from provided file name
std::string substr_path(std::string const &Filename)
{
// String::substr returns new string so substr_path has to return std::string
if (auto pos = Filename.rfind('/'); pos != std::string::npos)
return Filename.substr(0, pos + 1);
return {};
}
// returns length of common prefix between two provided strings
std::ptrdiff_t len_common_prefix(std::string_view a, std::string_view b)
{
auto [it1, it2] = std::ranges::mismatch(a, b);
return std::distance(a.begin(), it1);
}
// returns true if provided string contains another provided string
bool contains(std::string_view const String, std::string_view Substring)
{
// To be replaced with string::contains in C++ 23
return String.find(Substring) != std::string::npos;
}
bool contains(std::string_view const String, char Character)
{
// To be replaced with string::contains in C++ 23
return String.find(Character) != std::string::npos;
}
// helper, restores content of a 3d vector from provided input stream
// TODO: review and clean up the helper routines, there's likely some redundant ones
glm::dvec3 LoadPoint(cParser &Input)
{
// pobranie współrzędnych punktu
glm::dvec3 point;
Input.getTokens(3);
Input >> point.x >> point.y >> point.z;
return point;
}
// extracts a group of tokens from provided data stream, returns one of them picked randomly
std::string deserialize_random_set(cParser &Input, char const *Break)
{
auto token{Input.getToken<std::string>(true, Break)};
std::replace(token.begin(), token.end(), '\\', '/');
if (token != "[")
{
// simple case, single token
return token;
}
// if instead of a single token we've encountered '[' this marks a beginning of a random set
// we retrieve all entries, then return a random one
std::vector<std::string> tokens;
while ((token = deserialize_random_set(Input, Break)) != "" && token != "]")
{
tokens.emplace_back(token);
}
if (false == tokens.empty())
{
std::shuffle(std::begin(tokens), std::end(tokens), Global.random_engine);
return tokens.front();
}
else
{
// shouldn't ever get here but, eh
return "";
}
}
// returns hash of provided string, combined with provided seed
std::uint32_t hash_string(std::string const &String, std::uint32_t const Seed)
{
// FNV-1a, 32 bit, with the seed mixed into the standard offset basis
std::uint32_t hash{2166136261u ^ Seed};
for (auto const character : String)
{
hash ^= static_cast<std::uint32_t>(static_cast<unsigned char>(character));
hash *= 16777619u;
}
return hash;
}
// extracts a group of tokens from provided data stream, returns all of them
std::vector<std::string> deserialize_set(cParser &Input, char const *Break)
{
std::vector<std::string> tokens;
auto token{Input.getToken<std::string>(true, Break)};
std::ranges::replace(token, '\\', '/');
if (token != "[")
{
// simple case, single token
if (false == token.empty())
{
tokens.emplace_back(token);
}
return tokens;
}
// '[' marks a beginning of a set, retrieve all entries until it's closed
while ((token = deserialize_random_set(Input, Break)) != "" && token != "]")
{
tokens.emplace_back(token);
}
return tokens;
}