├── README.md ├── examples ├── Report.txt ├── other_stuff │ ├── read-test.sp │ └── read-test2.sp ├── performance-test.sp ├── usage-dump.sp └── usage-sample.sp └── localizer.inc /README.md: -------------------------------------------------------------------------------- 1 | # SM-Localizer 2 | **Using intrinsic Valve translation #phrases in SourceMod** 3 | 4 | **Readme** 5 | - Available at AlliedMods: https://forums.alliedmods.net/showthread.php?t=339741 6 | 7 | **Examples** 8 | - See "**examples**" folder and **localizer.inc** for details. 9 | - [L4D Map Changer](https://forums.alliedmods.net/showthread.php?t=311161) (by Dragokas) 10 | - [L4D Weapon and melees](https://github.com/dragokas/l4d2_weapons) (by raziEiL [disawar1], fork by Dragokas) 11 | -------------------------------------------------------------------------------- /examples/Report.txt: -------------------------------------------------------------------------------- 1 | >>>> Test #1: First run (Database) 2 | >>> Start processing of 174 files... 3 | Installation mode: DATABASE 4 | Connection to Steam servers successful. 5 | VAC secure mode is activated. 6 | >> Profiler report for: Decoding (174 items, 105.4 MB), max file: 3.38 MB 7 | avg speed: 14.83 MB/sec. 8 | max time per file: 0.25 sec. 9 | total time elapsed: 7.10 sec. 10 | Performance - OK. 11 | >> Profiler report for: Caching (174 items, 56.0 MB), max file: 1.95 MB 12 | avg speed: 9.25 MB/sec. 13 | max time per file: 0.19 sec. 14 | total time elapsed: 6.05 sec. 15 | Performance - OK. 16 | >>> Phrases processing is completed. 17 | [Server] Loc: Multiplayer 18 | 19 | >>>> Test #2: Second run (Database) 20 | >>> Start processing of 174 files... 21 | Installation mode: DATABASE 22 | >> Profiler report for: Decoding (0 items, 0.0 MB), max file: 0.00 MB 23 | avg speed: Na MB/sec. 24 | max time per file: 0.00 sec. 25 | total time elapsed: 0.00 sec. 26 | >> Profiler report for: Caching (0 items, 0.0 MB), max file: 0.00 MB 27 | avg speed: Na MB/sec. 28 | max time per file: 0.00 sec. 29 | total time elapsed: 0.00 sec. 30 | >>> Phrases processing is completed. 31 | [Server] Loc: Multiplayer 32 | 33 | >>>> Test #3: First run (Full cache) 34 | >>> Start processing of 174 files... 35 | Installation mode: FULLCACHE 36 | >> Profiler report for: Decoding (174 items, 105.4 MB), max file: 3.38 MB 37 | avg speed: 15.07 MB/sec. 38 | max time per file: 0.29 sec. 39 | total time elapsed: 6.99 sec. 40 | Performance - OK. 41 | >> Profiler report for: Caching (174 items, 56.0 MB), max file: 1.95 MB 42 | avg speed: 14.73 MB/sec. 43 | max time per file: 0.14 sec. 44 | total time elapsed: 3.80 sec. 45 | Performance - OK. 46 | >>> Phrases processing is completed. 47 | [Server] Loc: Multiplayer 48 | 49 | >>>> Test #4: Second run (Full cache) 50 | >>> Start processing of 174 files... 51 | Installation mode: FULLCACHE 52 | >> Profiler report for: Decoding (0 items, 0.0 MB), max file: 0.00 MB 53 | avg speed: Na MB/sec. 54 | max time per file: 0.00 sec. 55 | total time elapsed: 0.00 sec. 56 | >> Profiler report for: Caching (174 items, 56.0 MB), max file: 1.95 MB 57 | avg speed: 14.96 MB/sec. 58 | max time per file: 0.18 sec. 59 | total time elapsed: 3.74 sec. 60 | Performance - OK. 61 | >>> Phrases processing is completed. 62 | [Server] Loc: Multiplayer 63 | 64 | >>>> Test #5: Load Translation file 65 | >> Profiler report for: Dumping (289795 phrases): 0.20 sec. 66 | >> Profiler report for: LoadTranslations: 0.32 sec. 67 | [Server] LoadTranslations: Multiplayer -------------------------------------------------------------------------------- /examples/other_stuff/read-test.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #include 5 | 6 | public void OnPluginStart() 7 | { 8 | int bytesRead, buff[1024]; 9 | File hFile = OpenFile("resource/csgo_english.txt", "rb", true); 10 | if( hFile ) 11 | { 12 | PrintToServer("File size: %i", GetFileSize(hFile)); 13 | 14 | while( !hFile.EndOfFile() ) 15 | { 16 | bytesRead += hFile.Read(buff, sizeof(buff), 1); 17 | } 18 | delete hFile; 19 | } 20 | PrintToServer("Total bytes read: %i", bytesRead); 21 | } 22 | 23 | int GetFileSize(File hFile) 24 | { 25 | int save_pos = hFile.Position; 26 | hFile.Seek(0, SEEK_END); 27 | int size = hFile.Position; 28 | hFile.Seek(save_pos, SEEK_SET); 29 | return size; 30 | } 31 | -------------------------------------------------------------------------------- /examples/other_stuff/read-test2.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #include 5 | #include 6 | 7 | public void OnPluginStart() 8 | { 9 | RegAdminCmd("sm_test_perf", CmdPerformance, ADMFLAG_ROOT); 10 | } 11 | 12 | public Action CmdPerformance(int client, int argc) 13 | { 14 | float time; 15 | char code[1], name[32], srcFile[PLATFORM_MAX_PATH]; 16 | 17 | Profiler prof = new Profiler(); 18 | 19 | for( int i = 0; i < GetLanguageCount(); i++ ) 20 | { 21 | GetLanguageInfo(i, code, sizeof(code), name, sizeof(name)); 22 | FormatEx(srcFile, sizeof(srcFile), "resource/csgo_%s.txt", name); 23 | PrintToServer("Reading (%s)...", srcFile); 24 | 25 | File hr = OpenFile(srcFile, "rb", true); 26 | if( hr ) 27 | { 28 | prof.Start(); 29 | // Note: it seems various buffer sizes doesn't affect performance too much 30 | // 31 | int bytesRead, buff[512/* MUST MOD 4*/]; 32 | 33 | while( !hr.EndOfFile() ) 34 | { 35 | bytesRead = hr.Read(buff, sizeof(buff), 2); 36 | } 37 | delete hr; 38 | prof.Stop(); 39 | time += prof.Time; 40 | PrintToServer("Read (%s) -> %.2f sec.", srcFile, prof.Time); 41 | } 42 | } 43 | delete prof; 44 | PrintToServer("Total time: %.2f sec.", time); 45 | return Plugin_Handled; 46 | } 47 | -------------------------------------------------------------------------------- /examples/performance-test.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #define LC_PROFILER // required to measure the performance (see server console) 5 | #include 6 | 7 | Localizer loc; 8 | 9 | public void OnPluginStart() 10 | { 11 | if( GetEngineVersion() != Engine_TF2 ) 12 | { 13 | FindConVar("sv_hibernate_when_empty").SetInt(0); 14 | } 15 | //CmdPerformance(0, 0); 16 | RegAdminCmd("sm_localizer_perf", CmdPerformance, ADMFLAG_ROOT); 17 | } 18 | 19 | public Action CmdPerformance(int client, int argc) 20 | { 21 | loc = new Localizer(LC_INSTALL_MODE_CUSTOM); 22 | loc.Uninstall(); 23 | PrintToServer("\n>>>> Test #1: First run (Database)"); 24 | loc = new Localizer(LC_INSTALL_MODE_DATABASE); 25 | loc.Delegate_InitCompleted(SampleTranslation); 26 | return Plugin_Handled; 27 | } 28 | 29 | public void SampleTranslation() 30 | { 31 | static int times; 32 | times += 1; 33 | 34 | if( GetEngineVersion() == Engine_TF2 ) 35 | { 36 | loc.PrintToServer("[Server] Loc: #TF_Spy"); 37 | } 38 | else { 39 | loc.PrintToServer("[Server] Loc: #GameUI_Multiplayer"); 40 | } 41 | 42 | if( loc.InstallMode == LC_INSTALL_MODE_DATABASE ) 43 | { 44 | if( times % 2 == 1 ) 45 | { 46 | PrintToServer("\n>>>> Test #2: Second run (Database)"); 47 | loc.Close(); 48 | loc = new Localizer(LC_INSTALL_MODE_DATABASE); 49 | loc.Delegate_InitCompleted(SampleTranslation); 50 | } 51 | else { 52 | PrintToServer("\n>>>> Test #3: First run (Full cache)"); 53 | loc.Uninstall(); 54 | loc.Close(); 55 | loc = new Localizer(LC_INSTALL_MODE_FULLCACHE); 56 | loc.Delegate_InitCompleted(SampleTranslation); 57 | } 58 | } 59 | else if( loc.InstallMode == LC_INSTALL_MODE_FULLCACHE ) 60 | { 61 | if( times % 2 == 1 ) 62 | { 63 | PrintToServer("\n>>>> Test #4: Second run (Full cache)"); 64 | loc.Close(); 65 | loc = new Localizer(LC_INSTALL_MODE_FULLCACHE); 66 | loc.Delegate_InitCompleted(SampleTranslation); 67 | } 68 | else { 69 | PrintToServer("\n>>>> Test #5: Load Translation file"); 70 | loc.DumpAll(); 71 | loc.LoadTranslations(); 72 | PrintToServer("[Server] LoadTranslations: %T", "#GameUI_Multiplayer", LANG_SERVER); 73 | 74 | //loc.Uninstall(); 75 | //loc.Close(); 76 | //loc = new Localizer(LC_INSTALL_MODE_TRANSLATIONFILE); 77 | //loc.Delegate_InitCompleted(SampleTranslation); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/usage-dump.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #include 5 | 6 | Localizer loc; 7 | 8 | public void OnPluginStart() 9 | { 10 | loc = new Localizer(LC_INSTALL_MODE_FULLCACHE); 11 | loc.Delegate_InitCompleted(OnPhrasesReady); 12 | } 13 | 14 | public void OnPhrasesReady() 15 | { 16 | loc.DumpAll(); 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/usage-sample.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #include 5 | 6 | Localizer loc; 7 | 8 | public void OnPluginStart() 9 | { 10 | loc = new Localizer(); 11 | loc.Delegate_InitCompleted(OnPhrasesReady); 12 | 13 | RegConsoleCmd("sm_t", CmdTest); 14 | } 15 | 16 | public void OnPhrasesReady() 17 | { 18 | SampleTranslation(); 19 | } 20 | 21 | public Action CmdTest(int client, int argc) 22 | { 23 | if( loc.IsReady() ) 24 | { 25 | SampleTranslation(client); 26 | } 27 | else { 28 | ReplyToCommand(client, "Localizer is not ready yet."); 29 | } 30 | return Plugin_Handled; 31 | } 32 | 33 | void SampleTranslation(int client = LANG_SERVER) 34 | { 35 | if( GetEngineVersion() == Engine_TF2 ) 36 | { 37 | loc.ReplyToCommand(client, "[Reply] Loc: #TF_Spy"); 38 | return; 39 | } 40 | 41 | loc.ReplyToCommand(client, "[Reply] Loc: #GameUI_Multiplayer"); 42 | 43 | if( client ) 44 | { 45 | loc.PrintToChat(client, "%t", "[Chat] Loc: #GameUI_Multiplayer"); 46 | } 47 | 48 | PrintToServer("[Server] Loc {to default lang}: %s", Loc_Translate(LANG_SERVER, ">>>:#GameUI_Multiplayer")); 49 | PrintToServer("[Server] Loc {to Ukrainian}: %s", Loc_TranslateToLang("#GameUI_Multiplayer", _, "ukrainian")); 50 | PrintToServer("[Server] Loc {to Ukrainian (code)}: %s", Loc_TranslateToLang("#GameUI_Multiplayer", _, _, "ua")); 51 | } 52 | 53 | /* 54 | Console output: 55 | 56 | [Reply] Loc: Multiplayer 57 | [Server] Loc {to default lang}: >>>:Multiplayer 58 | [Server] Loc {to Ukrainian}: Мережева гра 59 | [Server] Loc {to Ukrainian (code)}: Мережева гра 60 | */ 61 | -------------------------------------------------------------------------------- /localizer.inc: -------------------------------------------------------------------------------- 1 | /** 2 | * Localizer.inc | API | Using instrinsic in-game translation #phrases 3 | * 4 | * Copyright (C) 2022 Stanislav "Dragokas" Polshyn 5 | * 6 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 7 | * 8 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 9 | * 10 | * You should have received a copy of the GNU General Public License along with this program. If not, see . 11 | **/ 12 | 13 | //{ #region Declares 14 | #if defined _localizer_included 15 | #endinput 16 | #endif 17 | #define _localizer_included 18 | 19 | #define LOCALIZER_VERSION "0.94" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define LC_USE_SQLITE 1 27 | 28 | enum LC_INSTALL_MODE { 29 | LC_INSTALL_MODE_NONE = 0, 30 | LC_INSTALL_MODE_DATABASE = 1, 31 | LC_INSTALL_MODE_FULLCACHE = 2, 32 | LC_INSTALL_MODE_TRANSLATIONFILE = 4, 33 | LC_INSTALL_MODE_CUSTOM = 8 34 | } 35 | 36 | enum LC_OP_STATE 37 | { 38 | LC_OP_STATE_WAIT, 39 | LC_OP_STATE_SIGNAL 40 | } 41 | 42 | enum LC_OP_TYPE 43 | { 44 | LC_OP_TYPE_DECODE, 45 | LC_OP_TYPE_CACHE 46 | } 47 | 48 | enum LC_OP_FLAG 49 | { 50 | LC_OP_FLAG_NONE = 0, 51 | LC_OP_FLAG_RESET = 1, 52 | LC_OP_FLAG_DELAY = 2 53 | } 54 | 55 | enum LC_CACHE 56 | { 57 | LC_CACHE_DATABASE = 1, // L1 58 | LC_CACHE_RAM = 2 // L2 - StringMap 59 | } 60 | 61 | //#define LC_PROFILER // uncomment to measure the performance 62 | 63 | char LC_LANGUAGE_KEY[] = "Language"; 64 | char LC_RESOURCE_ENCODED_DIR[] = "resource"; 65 | char LC_RESOURCE_ENCODED_PL_DIR[] = "../platform/resource"; 66 | char LC_RESOURCE_DECODED_DIR[] = "resource/utf8"; 67 | char LC_RESOURCE_INDEX_FILE[] = "resource/utf8/_index.txt"; 68 | char LC_TRANSLATION_FILE[] = "localizer.phrases.txt"; 69 | #define LC_DATABASE_NAME "localizer" 70 | #define LC_DATABASE_TABLE "localizer_phrases" 71 | #define LC_CONVAR_NAME "sm_localizer_inc" 72 | #define LC_MAX_LANG_COUNT 64 73 | #define LC_MAX_LANG_FILE_LENGTH 64 74 | #define LC_MAX_PHRASE_LENGTH 128 75 | #define LC_MAX_TRANSLATION_LENGTH 3072 76 | #define LC_THREAD_EXECUTION_TIME 0.3 77 | #define LC_THREAD_WAIT_TIME 0.1 78 | #define LC_MAX_SIGNAL_WAIT_TIME 60.0 79 | #define LC_MAX_PATH_LANG_ENCODED LC_MAX_LANG_FILE_LENGTH+sizeof(LC_RESOURCE_ENCODED_DIR)+1 80 | #define LC_MAX_PATH_LANG_DECODED LC_MAX_LANG_FILE_LENGTH+sizeof(LC_RESOURCE_DECODED_DIR)+1 81 | 82 | Database g_hLcDB; 83 | StringMap g_hMapLcPhrase[LC_MAX_LANG_COUNT], // weird, can't declare dynamic array statically 84 | g_hMapLcStamp, 85 | g_hMapLcEnglishFile; // for english ghosts (in CSGO) 86 | ArrayStack g_hLcStackEncoded, 87 | g_hLcStackCache; 88 | PrivateForward g_fwdLcOnPhrasesProcessingCompleted; 89 | Profiler g_hLcProf; 90 | Regex g_rLcCaptures; 91 | ConVar g_hCvarLcState; 92 | Handle g_hTimerLcState; 93 | bool g_bLcDecodeInProgress, 94 | g_bLcIndexChanged, 95 | g_bLcReady, 96 | g_bLcProfiler; 97 | int g_iLcServerLanguage, 98 | g_iLcQueueTx, 99 | g_iTotalFilesToProcess; 100 | LC_INSTALL_MODE g_iLcInstallMode; 101 | EngineVersion g_iLcEngine; 102 | 103 | typedef g_typLcNotifier = function void(); 104 | // #endregion Declares } 105 | 106 | //{ #region Stocks 107 | /* ============================================================================== 108 | Stocks 109 | ================================================================================*/ 110 | 111 | methodmap Localizer < Handle 112 | { 113 | /** 114 | * Called whenever new instance of Localizer methodmap is created. 115 | * This will make an installation of Localizer. 116 | * 117 | * @param install_mode Optional method of installation: 118 | * 119 | * LC_INSTALL_MODE_DATABASE (default) 120 | * - saves phrases to a database. 121 | * LC_INSTALL_MODE_FULLCACHE (experimental) 122 | * - pre-caches all phrases making the fastest query, but consuming a lot of memory. 123 | * LC_INSTALL_MODE_TRANSLATIONFILE (experimental, not fully implemented) 124 | * - save phrases to translation file. 125 | * - phrases are available via %T %t specifiers only. 126 | * - do not use own's Localizer Print* methods within this installation mode. 127 | * LC_INSTALL_MODE_CUSTOM 128 | * - no installation is performed, no objects are initialized at startup. 129 | * - it is intended to use with Localizer.Uninstall() method only. 130 | * 131 | * @return New instance of this methodmap. 132 | * 133 | * @note For LC_INSTALL_MODE_DATABASE and LC_INSTALL_MODE_TRANSLATIONFILE, installation is shared between plugins. 134 | * To access the translation functionality as soon as possible, register a hook via Localizer.Delegate_InitCompleted() method. 135 | * To free the resources use Localizer.Close() method. 136 | * To uninstall Localizer completely, use Localizer.Uninstall() method. 137 | */ 138 | public Localizer(LC_INSTALL_MODE install_mode = LC_INSTALL_MODE_DATABASE) 139 | { 140 | Loc_Init(install_mode); 141 | return view_as(g_hLcProf); 142 | } 143 | 144 | /** 145 | * Returns the installation mode used 146 | */ 147 | property LC_INSTALL_MODE InstallMode 148 | { 149 | public get() { return g_iLcInstallMode; } 150 | } 151 | 152 | /** 153 | * Creates a single use hook, notifying that Localizer is fully initialized 154 | * 155 | * @note Translation can only be retrieved after this hook is raised 156 | * 157 | * @param func Callback name having the following prototype: public void Foo() 158 | */ 159 | public void Delegate_InitCompleted(g_typLcNotifier func) 160 | { 161 | g_fwdLcOnPhrasesProcessingCompleted.AddFunction(GetMyHandle(), func); 162 | } 163 | 164 | /** 165 | * Informs if Localizer is fully initialized. 166 | * 167 | * @return True if Localizer is ready to translate phrases, false if initialization has still proceeded. 168 | */ 169 | public bool IsReady() 170 | { 171 | return g_bLcReady; 172 | } 173 | 174 | /** 175 | * Sends a message to the server console. 176 | * 177 | * @param format Formatting rules (allows to accept #phrase). 178 | * @param ... Variable number of format parameters. 179 | * 180 | * @note If phrase translation doesn't exist, it defaults to server language translation. 181 | */ 182 | public void PrintToServer(char[] format, any ...) 183 | { 184 | char translation[LC_MAX_TRANSLATION_LENGTH]; 185 | SetGlobalTransTarget(LANG_SERVER); 186 | VFormat(translation, sizeof(translation), format, 2 +1); 187 | Loc_ReplacePhrases(translation, sizeof(translation), g_iLcServerLanguage); 188 | PrintToServer("%s", translation); 189 | } 190 | 191 | /** 192 | * Replies to a message in a command. 193 | * 194 | * A client index of 0 will use PrintToServer(). 195 | * If the command was from the console, PrintToConsole() is used. 196 | * If the command was from chat, PrintToChat() is used. 197 | * 198 | * @param client Client index, or 0 for server. 199 | * @param format Formatting rules (allows to accept #phrase). 200 | * @param ... Variable number of format parameters. 201 | * 202 | * @note If phrase translation doesn't exist, it defaults to server language translation. 203 | * @error If the client is not connected or invalid. 204 | */ 205 | public void ReplyToCommand(int client, const char[] format, any ...) 206 | { 207 | char translation[LC_MAX_TRANSLATION_LENGTH]; 208 | SetGlobalTransTarget(client); 209 | VFormat(translation, sizeof(translation), format, 3 +1); 210 | Loc_ReplacePhrases(translation, sizeof(translation), client == 0 ? g_iLcServerLanguage : GetClientLanguage(client)); 211 | ReplyToCommand(client, "%s", translation); 212 | } 213 | 214 | /** 215 | * Prints a message to a specific client in the chat area. 216 | * 217 | * @param client Client index. 218 | * @param format Formatting rules (allows to accept #phrase). 219 | * @param ... Variable number of format parameters. 220 | * 221 | * @note If phrase translation doesn't exist, it defaults to server language translation. 222 | * @error Invalid client index, or client not in game. 223 | */ 224 | public void PrintToChat(int client, const char[] format, any ...) 225 | { 226 | char translation[LC_MAX_TRANSLATION_LENGTH]; 227 | SetGlobalTransTarget(client); 228 | VFormat(translation, sizeof(translation), format, 3 +1); 229 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(client)); 230 | PrintToChat(client, "%s", translation); 231 | } 232 | 233 | /** 234 | * Prints a message to all clients in the chat area. 235 | * 236 | * @param format Formatting rules (allows to accept #phrase). 237 | * @param ... Variable number of format parameters. 238 | * 239 | * @note If phrase translation doesn't exist, it defaults to server language translation. 240 | */ 241 | public void PrintToChatAll(char[] format, any ...) 242 | { 243 | char translation[LC_MAX_TRANSLATION_LENGTH]; 244 | for( int i = 1; i <= MaxClients; i++ ) 245 | { 246 | if( IsClientInGame(i) && !IsFakeClient(i) ) 247 | { 248 | SetGlobalTransTarget(i); 249 | VFormat(translation, sizeof(translation), format, 2 +1); 250 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(i)); 251 | PrintToChat(i, "%s", translation); 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Prints a message to a specific client with a hint box. 258 | * 259 | * @param client Client index. 260 | * @param format Formatting rules (allows to accept #phrase). 261 | * @param ... Variable number of format parameters. 262 | * 263 | * @note If phrase translation doesn't exist, it defaults to server language translation. 264 | * @error Invalid client index, or client not in game. 265 | */ 266 | public void PrintHintText(int client, const char[] format, any ...) 267 | { 268 | char translation[LC_MAX_TRANSLATION_LENGTH]; 269 | SetGlobalTransTarget(client); 270 | VFormat(translation, sizeof(translation), format, 3 +1); 271 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(client)); 272 | PrintHintText(client, "%s", translation); 273 | } 274 | 275 | /** 276 | * Prints a message to all clients with a hint box. 277 | * 278 | * @param format Formatting rules (allows to accept #phrase). 279 | * @param ... Variable number of format parameters. 280 | * 281 | * @note If phrase translation doesn't exist, it defaults to server language translation. 282 | */ 283 | public void PrintHintTextToAll(char[] format, any ...) 284 | { 285 | char translation[LC_MAX_TRANSLATION_LENGTH]; 286 | for( int i = 1; i <= MaxClients; i++ ) 287 | { 288 | if( IsClientInGame(i) && !IsFakeClient(i) ) 289 | { 290 | SetGlobalTransTarget(i); 291 | VFormat(translation, sizeof(translation), format, 2 +1); 292 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(i)); 293 | PrintHintText(i, "%s", translation); 294 | } 295 | } 296 | } 297 | 298 | /** 299 | * Prints a message to a specific client in the center of the screen. 300 | * 301 | * @param client Client index. 302 | * @param format Formatting rules (allows to accept #phrase). 303 | * @param ... Variable number of format parameters. 304 | * 305 | * @note If phrase translation doesn't exist, it defaults to server language translation. 306 | * @error Invalid client index, or client not in game. 307 | */ 308 | public void PrintCenterText(int client, const char[] format, any ...) 309 | { 310 | char translation[LC_MAX_TRANSLATION_LENGTH]; 311 | SetGlobalTransTarget(client); 312 | VFormat(translation, sizeof(translation), format, 3 +1); 313 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(client)); 314 | PrintCenterText(client, "%s", translation); 315 | } 316 | 317 | /** 318 | * Prints a message to all clients in the center of the screen. 319 | * 320 | * @param format Formatting rules (allows to accept #phrase). 321 | * @param ... Variable number of format parameters. 322 | * 323 | * @note If phrase translation doesn't exist, it defaults to server language translation. 324 | */ 325 | public void PrintCenterTextAll(char[] format, any ...) 326 | { 327 | char translation[LC_MAX_TRANSLATION_LENGTH]; 328 | for( int i = 1; i <= MaxClients; i++ ) 329 | { 330 | if( IsClientInGame(i) && !IsFakeClient(i) ) 331 | { 332 | SetGlobalTransTarget(i); 333 | VFormat(translation, sizeof(translation), format, 2 +1); 334 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(i)); 335 | PrintCenterText(i, "%s", translation); 336 | } 337 | } 338 | } 339 | 340 | /** 341 | * Sends a message to a client's console. 342 | * 343 | * @param client Client index. 344 | * @param format Formatting rules (allows to accept #phrase). 345 | * @param ... Variable number of format parameters. 346 | * 347 | * @note If phrase translation doesn't exist, it defaults to server language translation. 348 | * @error If the client is not connected an error will be thrown. 349 | */ 350 | public void PrintToConsole(int client, const char[] format, any ...) 351 | { 352 | char translation[LC_MAX_TRANSLATION_LENGTH]; 353 | SetGlobalTransTarget(client); 354 | VFormat(translation, sizeof(translation), format, 3 +1); 355 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(client)); 356 | PrintToConsole(client, "%s", translation); 357 | } 358 | 359 | /** 360 | * Sends a message to every client's console. 361 | * 362 | * @param format Formatting rules (allows to accept #phrase). 363 | * @param ... Variable number of format parameters. 364 | * 365 | * @note If phrase translation doesn't exist, it defaults to server language translation. 366 | */ 367 | public void PrintToConsoleAll(char[] format, any ...) 368 | { 369 | char translation[LC_MAX_TRANSLATION_LENGTH]; 370 | for( int i = 1; i <= MaxClients; i++ ) 371 | { 372 | if( IsClientInGame(i) && !IsFakeClient(i) ) 373 | { 374 | SetGlobalTransTarget(i); 375 | VFormat(translation, sizeof(translation), format, 2 +1); 376 | Loc_ReplacePhrases(translation, sizeof(translation), GetClientLanguage(i)); 377 | PrintToConsole(i, "%s", translation); 378 | } 379 | } 380 | } 381 | 382 | /** 383 | * Formats a string according to the SourceMod format rules (see documentation). 384 | * 385 | * @param buffer Destination string buffer. 386 | * @param maxlength Maximum length of output string buffer. 387 | * @param format Formatting rules (allows to accept #phrase). 388 | * @param ... Variable number of format parameters. 389 | * 390 | * @return Number of cells written. 391 | * 392 | * @note If phrase translation doesn't exist, it defaults to server language translation. 393 | */ 394 | public int Format(char[] buffer, int maxlength, const char[] format, any...) 395 | { 396 | VFormat(buffer, maxlength, format, 4 +1); 397 | Loc_ReplacePhrases(buffer, maxlength, g_iLcServerLanguage); 398 | return strlen(buffer); 399 | } 400 | 401 | /** 402 | * Translates a single #phrase according to client's or spicified language. 403 | * 404 | * @param phrase A single #phrase to be translated. 405 | * @param buffer Destination string buffer. Input and output buffers cannot be the same! 406 | * @param maxlength Maximum length of output string buffer. 407 | * @param client Optional client index which language to translate to (default: LANG_SERVER). 408 | * @param lang_name Optional full language name to translate to (default: empty). 409 | * @param lang_code Optional alphabetic language code to translate to (default: empty). 410 | * @param default_text Optional default string to use if phrase doesn't exist (default: empty). 411 | * 412 | * @return True if phrase is translated, false otherwise. 413 | * 414 | * @note If phrase doesn't found, the empty string is returned in buffer. 415 | * @error If #phrase length is <= 1. 416 | */ 417 | public bool PhraseTranslateToLang(char[] phrase, char[] buffer, int maxlength, int client = LANG_SERVER, char[] lang_name = NULL_STRING, char[] lang_code = NULL_STRING, char[] default_text = NULL_STRING ) 418 | { 419 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 420 | if( Loc_GetPhrase(lang_num, phrase[1], buffer, maxlength, false, false) ) 421 | { 422 | return true; 423 | } 424 | else { 425 | strcopy(buffer, maxlength, default_text); 426 | return false; 427 | } 428 | } 429 | 430 | /** 431 | * Translates a single #phrase to client's or spicified language. 432 | * 433 | * @param phrase A single #phrase. 434 | * @param buffer Destination string buffer. 435 | * @param maxlength Maximum length of output string buffer. 436 | * @param client Optional client index which language to translate to (default: LANG_SERVER). 437 | * @param lang_name Optional full language name to translate to (default: empty). 438 | * @param lang_code Optional alphabetic language code to translate to (default: empty). 439 | * 440 | * @return True if phrase is translated, false otherwise. 441 | * 442 | * @note If phrase doesn't found, the empty string is returned in buffer. 443 | * @error If #phrase length is <= 1. 444 | */ 445 | public bool PhraseExists(char[] phrase, int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "") 446 | { 447 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 448 | char translation[LC_MAX_TRANSLATION_LENGTH]; 449 | return Loc_GetPhrase(lang_num, phrase[1], translation, sizeof(translation), false, false); 450 | } 451 | 452 | /** 453 | * Checks if a #phrase had precache in RAM (Cache L2). 454 | * 455 | * @param phrase A single #phrase. 456 | * @param client Optional client index of phrase language (default: LANG_SERVER). 457 | * @param lang_name Optional full language name of phrase (default: empty). 458 | * @param lang_code Optional alphabetic language code of phrase (default: empty). 459 | * 460 | * @return True if phrase is precached, false otherwise. 461 | * 462 | * @error If #phrase length is <= 1. 463 | */ 464 | public bool PhrasePrecached(char[] phrase, int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "") 465 | { 466 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 467 | char translation[LC_MAX_TRANSLATION_LENGTH]; 468 | return g_hMapLcPhrase[lang_num].GetString(phrase[1], translation, sizeof(translation)); 469 | } 470 | 471 | /** 472 | * Precaches a #phrase to RAM (Cache L2) for faster access later. 473 | * 474 | * @param phrase A single #phrase. 475 | * 476 | * @return True if phrase is precached, false if phrase doesn't found. 477 | * 478 | * @note This precache is only actual for installation mode 'LC_INSTALL_MODE_DATABASE' (default). 479 | * @noerror If #phrase length is <= 1. 480 | */ 481 | public bool PrecachePhrase(char[] phrase) 482 | { 483 | int lang_num = Loc_GetLanguageNum(LANG_SERVER, "", ""); 484 | char translation[LC_MAX_TRANSLATION_LENGTH]; 485 | return Loc_GetPhrase(lang_num, phrase[1], translation, sizeof(translation), true, true); 486 | } 487 | 488 | //TODO 489 | /** 490 | * Precaches an arbitrary translation file to use it the same way if it were intrinsic translation phrases. 491 | * 492 | * @param file Translation file 493 | * @param cache Memory location where to store the cache (Database or RAM) 494 | * 495 | * @return True if the file is precached, false if the file doesn't found. 496 | * 497 | * @noerror 498 | 499 | public bool PrecacheTranslationFile(char[] file, LC_CACHE cache = LC_CACHE_RAM) 500 | { 501 | 502 | return true; 503 | } 504 | */ 505 | 506 | /** 507 | * Adds a new #phrase and translation. 508 | * 509 | * @param phrase A single #phrase. 510 | * @param translation Translation of phrase. 511 | * @param client Optional client index which language is used in provided translation (default: LANG_SERVER). 512 | * @param lang_name Optional full language name of provided translation (default: empty). 513 | * @param lang_code Optional alphabetic language code of provided translation (default: empty). 514 | * @param bOverwrite Optional, specify if you want to overwrite phrase that is already exists (default: yes). 515 | * @param bAsync Optional, spefify if you want this operation to be asynchronous (default: no). 516 | * 517 | * @note This #phrase will not survive the server reboot if installation mode == LC_INSTALL_MODE_FULLCACHE. 518 | * @error If #phrase length is <= 1. 519 | */ 520 | public void PhraseAdd(char[] phrase, char[] translation, int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "", 521 | bool bOverwrite = true, bool bAsync = false) 522 | { 523 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 524 | Loc_AddPhrase(lang_num, phrase[1], translation, bOverwrite, bAsync); 525 | } 526 | 527 | /** 528 | * Adds a new #phrase and translation to a temporarily cache L2 (StringMap). 529 | * 530 | * @param phrase A single #phrase. 531 | * @param translation Translation of phrase. 532 | * @param client Optional client index which language is used in provided translation (default: LANG_SERVER). 533 | * @param lang_name Optional full language name of provided translation (default: empty). 534 | * @param lang_code Optional alphabetic language code of provided translation (default: empty). 535 | * @param bOverwrite Optional, specify if you want to overwrite phrase that is already exists (default: yes). 536 | * 537 | * @note This #phrase will not survive the server reboot. 538 | * @error If #phrase length is <= 1. 539 | */ 540 | public void PhraseAddTemp(char[] phrase, char[] translation, int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "", 541 | bool bOverwrite = true) 542 | { 543 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 544 | g_hMapLcPhrase[lang_num].SetString(phrase, translation, bOverwrite); 545 | } 546 | 547 | /** 548 | * Removes a #phrase and all its translations. 549 | * 550 | * @param phrase A single #phrase. 551 | * 552 | * @note This will not survive the server reboot if installation mode == LC_INSTALL_MODE_FULLCACHE. 553 | * For other modes, if you removes a pre-built (resource) phrase, it can only be restored on resource update, 554 | * or via Localizer.Uninstall() method with a full re-installation. 555 | * @error If #phrase length is <= 1. 556 | */ 557 | public void PhraseRemove(char[] phrase) 558 | { 559 | SQL_Loc_RemovePhrase(phrase[1]); 560 | for( int i = 0; i < GetLanguageCount(); i++ ) 561 | { 562 | if( g_hMapLcPhrase[i] ) 563 | { 564 | g_hMapLcPhrase[i].Remove(phrase[1]); 565 | } 566 | } 567 | } 568 | 569 | /** 570 | * Generates SM compatible translation file from all L2 (StringMap) pre-cached phrases. 571 | * Note: for convenience, translation files are also mirrored to folder: "translations/localizer/" 572 | * 573 | * @return True if at least default server language file is successfully generated, false otherwise. 574 | * 575 | * @note To dump all resource phrases, you have to initialize Localizer with installation mode == LC_INSTALL_MODE_FULLCACHE. 576 | * File is stored in location: ./translations/localizer.phrases.txt 577 | * You can use it to observe and study desired phrases for further using via default installation mode. 578 | * Or you can load this file with Localizer.LoadTranslations() method and use translations via %T %t specifiers. 579 | */ 580 | public bool DumpAll() 581 | { 582 | bool result; 583 | int count; 584 | g_hLcProf.Start(); 585 | 586 | result = Loc_Dump(LANG_SERVER, count); 587 | 588 | for( int i = 0; i < GetLanguageCount(); i++ ) 589 | { 590 | if( i != LANG_SERVER ) 591 | { 592 | Loc_Dump(i, count); 593 | } 594 | } 595 | g_hLcProf.Stop(); 596 | #if defined LC_PROFILER 597 | PrintToServer(">> Profiler report for: %s (%i phrases): %.2f sec.", "Dumping" , count, g_hLcProf.Time); 598 | #endif 599 | return result; 600 | } 601 | 602 | /** 603 | * Loads previously dumped localizer.phrases.txt file via SM LoadTranslations() method. 604 | * 605 | * @return True if translations are loaded, false if translation file doesn't found. 606 | * 607 | * @note You can use translations via %T %t specifiers in any SM Print* and similar methods passing "#phrase" as an argument. 608 | */ 609 | public bool LoadTranslations() 610 | { 611 | return Loc_LoadTranslations(); 612 | } 613 | 614 | /** 615 | * Uninstall Localizer from the server completely. 616 | * 617 | * @note Database table, translation files and decoded resource files ('utf8' dir) will be removed. 618 | * You should call Localizer.Close() method manually to free the remaining resources. 619 | */ 620 | public void Uninstall() 621 | { 622 | if( g_hCvarLcState && g_hCvarLcState.IntValue == view_as(LC_OP_STATE_WAIT) ) 623 | { 624 | return; 625 | } 626 | Loc_DeleteDirectory(LC_RESOURCE_DECODED_DIR); 627 | SQL_Loc_RemoveTable(); 628 | Loc_RemoveTranslationFiles(); 629 | g_iLcInstallMode = LC_INSTALL_MODE_NONE; 630 | } 631 | 632 | /** 633 | * Frees resources allocated by Localizer instance. 634 | */ 635 | public void Close() 636 | { 637 | for( int i = 0; i < sizeof(g_hMapLcPhrase); i++ ) { 638 | if( g_hMapLcPhrase[i] != null) 639 | delete g_hMapLcPhrase[i]; 640 | } 641 | delete g_hMapLcStamp; 642 | delete g_hMapLcEnglishFile; 643 | delete g_hLcStackEncoded; 644 | delete g_hLcStackCache; 645 | delete g_hTimerLcState; 646 | delete g_hLcProf; 647 | g_fwdLcOnPhrasesProcessingCompleted.RemoveAllFunctions(GetMyHandle()); 648 | //delete g_fwdLcOnPhrasesProcessingCompleted; // disabled: potential crash, if executed in mid-call 649 | delete g_rLcCaptures; 650 | //delete g_hLcDB; // disabled: for safe, because database can still execute threaded operations 651 | g_iLcInstallMode = LC_INSTALL_MODE_NONE; 652 | } 653 | } 654 | 655 | /** 656 | * In-line formats a string according to the SourceMod format rules (see documentation). 657 | * 658 | * @param client Client index. 659 | * @param format Formatting rules (allows to accept #phrase). 660 | * @param ... Variable number of format parameters. 661 | * 662 | * @return Char array with resulting string. 663 | * 664 | * @note If a phrase doesn't found, original #phrase is stay untouched. 665 | * @error Invalid client index, or client not in game. 666 | */ 667 | stock char[] Loc_Translate(int client, const char[] format, any ...) // weird, can't declare it within methodmap due to array-based return type 668 | { 669 | char translation[LC_MAX_TRANSLATION_LENGTH]; 670 | SetGlobalTransTarget(client); 671 | VFormat(translation, sizeof(translation), format, 3); 672 | Loc_ReplacePhrases(translation, sizeof(translation), client == 0 ? g_iLcServerLanguage : GetClientLanguage(client)); 673 | return translation; 674 | } 675 | 676 | /** 677 | * In-line translates a single #phrase according to client's or spicified language. 678 | * 679 | * @param phrase A single #phrase to be translated. 680 | * @param client Optional client index which language to translate to (default: LANG_SERVER). 681 | * @param lang_name Optional full language name to translate to (default: empty). 682 | * @param lang_code Optional alphabetic language code to translate to (default: empty). 683 | * 684 | * @return Char array with a translation. 685 | * 686 | * @note If phrase doesn't found, the empty string is returned. 687 | * @error If #phrase length is <= 1. 688 | */ 689 | stock char[] Loc_TranslateToLang(char[] phrase, int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "" ) 690 | { 691 | char translation[LC_MAX_TRANSLATION_LENGTH]; 692 | int lang_num = Loc_GetLanguageNum(client, lang_name, lang_code); 693 | Loc_GetPhrase(lang_num, phrase[1], translation, sizeof(translation), false, false); 694 | return translation; 695 | } 696 | 697 | // #endregion Stocks } 698 | 699 | /* ============================================================================== 700 | Private functions 701 | ================================================================================*/ 702 | 703 | //{ #region Initialize 704 | void Loc_Init(LC_INSTALL_MODE install_mode) 705 | { 706 | /* 707 | // TODO 708 | 709 | g_hLcGlobalProf = new Profiler(); 710 | #if defined LC_PROFILER 711 | if( !(install_mode & LC_INSTALL_MODE_CUSTOM) ) 712 | { 713 | g_hLcGlobalProf.Start(); 714 | } 715 | #endif 716 | */ 717 | 718 | #if defined LC_PROFILER 719 | g_bLcProfiler = true; 720 | #endif 721 | 722 | g_iLcInstallMode = install_mode; 723 | g_iLcEngine = GetEngineVersion(); 724 | g_hCvarLcState = FindConVar(LC_CONVAR_NAME); // inter-plugin sync. mechanism 725 | Loc_RegisterCommands(); 726 | 727 | // TODO: remove it 728 | if( g_hCvarLcState ) 729 | { 730 | g_hCvarLcState.SetInt(1); 731 | } 732 | 733 | if( !(install_mode & LC_INSTALL_MODE_CUSTOM) ) 734 | { 735 | Loc_InitObjects(); 736 | if( g_hCvarLcState && g_hCvarLcState.IntValue == view_as(LC_OP_STATE_WAIT) ) 737 | { 738 | if( g_bLcProfiler ) 739 | PrintToServer("[Localizer] [h:%i] Pausing execution", GetMyHandle()); 740 | 741 | g_hCvarLcState.AddChangeHook(Loc_StateChanged); 742 | Loc_SetStateWatchDog(); 743 | } 744 | else { 745 | if( !g_hCvarLcState ) 746 | { 747 | char value[4]; 748 | IntToString(view_as(LC_OP_STATE_WAIT), value, sizeof(value)); 749 | g_hCvarLcState = CreateConVar(LC_CONVAR_NAME, value, "Signal for other plugins to continue initialization", FCVAR_SPONLY | FCVAR_DONTRECORD ); 750 | } 751 | Loc_StartProcessing(); 752 | } 753 | } 754 | } 755 | 756 | bool Loc_InitObjects() 757 | { 758 | int count = GetLanguageCount(); 759 | for( int i = 0; i < count; i++ ) { 760 | delete g_hMapLcPhrase[i]; 761 | g_hMapLcPhrase[i] = new StringMap(); 762 | } 763 | delete g_hMapLcStamp; 764 | delete g_hLcStackEncoded; 765 | delete g_hLcStackCache; 766 | g_hMapLcStamp = new StringMap(); 767 | g_hMapLcEnglishFile = new StringMap(); 768 | g_hLcStackEncoded = new ArrayStack(ByteCountToCells(LC_MAX_LANG_FILE_LENGTH)); 769 | g_hLcStackCache = new ArrayStack(ByteCountToCells(LC_MAX_LANG_FILE_LENGTH)); 770 | if( !g_hLcProf ) 771 | { 772 | g_hLcProf = new Profiler(); 773 | } 774 | if( !g_fwdLcOnPhrasesProcessingCompleted ) 775 | { 776 | g_fwdLcOnPhrasesProcessingCompleted = new PrivateForward(ET_Ignore); 777 | } 778 | //example: "#hulkzombie.start-ledge\\climb", but not greedy for "#hulkzombie.start-ledge\\climb." 779 | // 780 | if( !g_rLcCaptures ) 781 | { 782 | g_rLcCaptures = new Regex("#\\w+([\\.\\-\\\\]\\w+)*"); 783 | } 784 | g_iLcServerLanguage = GetServerLanguage(); 785 | return true; 786 | } 787 | 788 | void Loc_RegisterCommands() 789 | { 790 | static bool IsListen; 791 | if( !IsListen ) 792 | { 793 | if( CommandExists("sm_localizer_list") ) 794 | { 795 | AddCommandListener(CmdListener_Loc_List, "sm_localizer_list"); 796 | } 797 | else { 798 | RegAdminCmd("sm_localizer_list", Cmd_Loc_List, ADMFLAG_ROOT, "Lists plugin names that using Localizer API, show API version and installation mode"); 799 | } 800 | IsListen = true; 801 | } 802 | } 803 | 804 | public Action CmdListener_Loc_List(int client, const char[] command, int argc) 805 | { 806 | Loc_ShowConsumer(client); 807 | return Plugin_Continue; 808 | } 809 | 810 | public Action Cmd_Loc_List(int client, int argc) 811 | { 812 | Loc_ShowConsumer(client); 813 | return Plugin_Handled; 814 | } 815 | 816 | void Loc_ShowConsumer(int client) 817 | { 818 | char name[64]; 819 | GetPluginFilename(INVALID_HANDLE, name, sizeof(name)); 820 | ReplyToCommand(client, "%s | Install mode: %s | API Version: %s", name, Loc_InstallMode_ToString(), LOCALIZER_VERSION); 821 | } 822 | 823 | public void Loc_StateChanged(ConVar convar, const char[] oldValue, const char[] newValue) 824 | { 825 | if( view_as(convar.IntValue) == LC_OP_STATE_SIGNAL ) 826 | { 827 | if( g_bLcProfiler ) 828 | PrintToServer("[Localizer] [h:%i] Received signal => continue execution", GetMyHandle()); 829 | 830 | delete g_hTimerLcState; 831 | g_hCvarLcState.RemoveChangeHook(Loc_StateChanged); 832 | convar.SetInt(view_as(LC_OP_STATE_WAIT)); // pause the next plugin in chain 833 | Loc_StartProcessing(); 834 | } 835 | else { 836 | if( g_bLcProfiler ) 837 | PrintToServer("[Localizer] [h:%i] Restarting Dog", GetMyHandle()); 838 | 839 | Loc_SetStateWatchDog(); 840 | } 841 | } 842 | 843 | void Loc_SetStateWatchDog() 844 | { 845 | delete g_hTimerLcState; 846 | g_hTimerLcState = CreateTimer(LC_MAX_SIGNAL_WAIT_TIME, Timer_Loc_SignalWatchDog); 847 | } 848 | 849 | public Action Timer_Loc_SignalWatchDog(Handle timer) 850 | { 851 | PrintToServer("[Localizer] [h:%i] WatchDog raised !!!", GetMyHandle()); 852 | g_hTimerLcState = null; 853 | g_hCvarLcState.SetInt(view_as(LC_OP_STATE_SIGNAL)); 854 | return Plugin_Continue; 855 | } 856 | 857 | void Loc_StartProcessing() 858 | { 859 | CreateDirectory(LC_RESOURCE_DECODED_DIR, 0o755); 860 | 861 | if( !Loc_ReadIndexFile()) 862 | { 863 | g_bLcProfiler = true; 864 | PrintToChatAll(">>> [Localizer] Beginning transformation of language files..."); 865 | PrintToChatAll(">>> [Localizer] It may freeze the server. Don't panic!"); 866 | PrintToChatAll(">>> [Localizer] This is one-time procedure."); 867 | PrintToServer(">>> [Localizer] Beginning transformation of language files..."); 868 | PrintToServer(">>> [Localizer] It may freeze the server. Don't panic!"); 869 | PrintToServer(">>> [Localizer] This is one-time procedure."); 870 | } 871 | 872 | switch( g_iLcInstallMode ) 873 | { 874 | case LC_INSTALL_MODE_DATABASE: 875 | { 876 | SQL_Loc_DB_Connect(); 877 | } 878 | case LC_INSTALL_MODE_FULLCACHE: 879 | { 880 | Loc_GetPhraseFiles(""); 881 | } 882 | case LC_INSTALL_MODE_TRANSLATIONFILE: 883 | { 884 | Loc_LoadTranslations(); 885 | Loc_GetPhraseFiles(""); 886 | } 887 | } 888 | } 889 | // #endregion Initialize } 890 | 891 | //{ #region Forwards 892 | /* ============================================================================== 893 | Forwards 894 | ================================================================================*/ 895 | 896 | void OnPhrasesProcessingCompleted_CallDelayed() // wait for DB to finish last threaded operation 897 | { 898 | CreateTimer(LC_THREAD_WAIT_TIME, Timer_Loc_OnPhrasesProcessingCompleted); 899 | } 900 | 901 | public Action Timer_Loc_OnPhrasesProcessingCompleted(Handle timer) 902 | { 903 | Forward_OnPhrasesProcessingCompleted(); 904 | return Plugin_Continue; 905 | } 906 | 907 | void Forward_OnPhrasesProcessingCompleted() 908 | { 909 | if( g_bLcIndexChanged ) 910 | { 911 | Loc_WriteIndexFile(); 912 | } 913 | if( g_iLcInstallMode == LC_INSTALL_MODE_DATABASE ) 914 | { 915 | SQL_Loc_SetIndex("", 0, .bFinishPending = true); 916 | } 917 | else if( g_iLcInstallMode == LC_INSTALL_MODE_TRANSLATIONFILE ) 918 | { 919 | // TODO 920 | } 921 | 922 | g_hCvarLcState.SetInt(view_as(LC_OP_STATE_SIGNAL)); 923 | 924 | g_bLcReady = true; 925 | 926 | if( g_bLcProfiler ) 927 | { 928 | if( g_iLcInstallMode == LC_INSTALL_MODE_DATABASE ) 929 | { 930 | g_hLcProf.Stop(); 931 | PrintToServer(">>> [Localizer] Profiler report for: %s: %.2f sec.", "Pending database" , g_hLcProf.Time); 932 | } 933 | PrintToServer(">>> [Localizer] Phrases processing is completed."); 934 | } 935 | 936 | RequestFrame(Forward_OnPhrasesProcessingCompleted_Frame); 937 | } 938 | 939 | void Forward_OnPhrasesProcessingCompleted_Frame() 940 | { 941 | Action result; 942 | Call_StartForward(g_fwdLcOnPhrasesProcessingCompleted); 943 | Call_Finish(result); 944 | 945 | /* // no sense, since we can't call it by name anyway 946 | if( GetFunctionByName(null, "OnPhrasesProcessingCompleted") != INVALID_FUNCTION ) 947 | { 948 | Call_StartFunction(null, OnPhrasesProcessingCompleted); 949 | Call_Finish(result); 950 | } 951 | */ 952 | } 953 | 954 | // #endregion Forward } 955 | 956 | //{ #region Parser 957 | /* ============================================================================== 958 | Parser 959 | ================================================================================*/ 960 | 961 | bool Loc_ReadIndexFile() 962 | { 963 | g_hMapLcStamp.Clear(); 964 | 965 | int p, iStamp; 966 | char str[LC_MAX_TRANSLATION_LENGTH]; 967 | File hr = OpenFile(LC_RESOURCE_INDEX_FILE, "rt"); 968 | if( hr ) 969 | { 970 | while( !hr.EndOfFile() && hr.ReadLine(str, sizeof(str)) ) 971 | { 972 | if( -1 != (p = FindCharInString(str, '|')) ) 973 | { 974 | str[p] = 0; 975 | iStamp = StringToInt(str[p+1]); 976 | g_hMapLcStamp.SetValue(str[0], iStamp, true); 977 | } 978 | } 979 | hr.Close(); 980 | return true; 981 | } 982 | return false; 983 | } 984 | 985 | void Loc_WriteIndexFile() 986 | { 987 | char name[128], str[128]; 988 | int iStamp; 989 | File hFile = OpenFile(LC_RESOURCE_INDEX_FILE, "wt"); 990 | if( hFile ) { 991 | StringMapSnapshot hSnap = g_hMapLcStamp.Snapshot(); 992 | if( hSnap ) 993 | { 994 | for( int i = 0; i < hSnap.Length; i++ ) 995 | { 996 | hSnap.GetKey(i, name, sizeof(name)); 997 | g_hMapLcStamp.GetValue(name, iStamp); 998 | FormatEx(str, sizeof(str), "%s|%i", name, iStamp); 999 | hFile.WriteLine(str); 1000 | } 1001 | delete hSnap; 1002 | } 1003 | hFile.Close(); 1004 | } 1005 | } 1006 | 1007 | void Loc_GetPhraseFiles(char[] search) 1008 | { 1009 | DirectoryListing hDir; 1010 | char sFile[LC_MAX_LANG_FILE_LENGTH], prefix[32], guess[LC_MAX_LANG_FILE_LENGTH]; 1011 | int iCount, iLen, n, iLenSearch = strlen(search); 1012 | FileType fileType; 1013 | StringMap hUniqPrefix = new StringMap(); 1014 | 1015 | hDir = OpenDirectory(LC_RESOURCE_ENCODED_DIR, true); 1016 | if( hDir ) 1017 | { 1018 | while( hDir.GetNext(sFile, sizeof(sFile), fileType) ) 1019 | { 1020 | if( fileType == FileType_File ) 1021 | { 1022 | if( strncmp(sFile, search, iLenSearch, false) == 0 ) 1023 | { 1024 | iLen = strlen(sFile); 1025 | if( iLen > 4 ) 1026 | { 1027 | if( strcmp(sFile[iLen-4], ".txt", false) == 0 ) 1028 | { 1029 | n = FindCharInString(sFile, '_'); 1030 | 1031 | if( n != -1 ) // some text files aren't #prases 1032 | { 1033 | if( strcmp(sFile[n+1], "latam.txt") != 0 && 1034 | strcmp(sFile[n+1], "pirate.txt") != 0 ) 1035 | { 1036 | strcopy(prefix, n+1, sFile); 1037 | hUniqPrefix.SetString(prefix, sFile, false); 1038 | 1039 | g_hLcStackEncoded.PushString(sFile); 1040 | ++ iCount; 1041 | } 1042 | } 1043 | } 1044 | } 1045 | } 1046 | } 1047 | } 1048 | delete hDir; 1049 | } 1050 | 1051 | // Retrieving files having no _english.txt companion 1052 | // Seen in CS:GO 1053 | // 1054 | StringMapSnapshot hSnap = hUniqPrefix.Snapshot(); 1055 | if( hSnap ) 1056 | { 1057 | for( int i = 0; i < hSnap.Length; i++ ) 1058 | { 1059 | hSnap.GetKey(i, prefix, sizeof(prefix)); 1060 | FormatEx(guess, sizeof(guess), "%s/%s_english.txt", LC_RESOURCE_ENCODED_DIR, prefix); 1061 | if( !FileExists(guess, true) ) // phrases set has no english para? 1062 | { 1063 | // push the first file of the set to a separate stack to process [english] fallback phrases. 1064 | hUniqPrefix.GetString(prefix, sFile, sizeof(sFile)); 1065 | g_hMapLcEnglishFile.SetValue(sFile, 0); 1066 | } 1067 | } 1068 | delete hSnap; 1069 | } 1070 | delete hUniqPrefix; 1071 | 1072 | if( !g_bLcDecodeInProgress ) 1073 | { 1074 | if( g_bLcProfiler ) 1075 | { 1076 | PrintToChatAll(">>> [Localizer] Start processing of %i files...", iCount); 1077 | PrintToServer(">>> [Localizer] Start processing of %i files...", iCount); 1078 | PrintToServer(">>> [Localizer] Installation mode: %s", Loc_InstallMode_ToString()); 1079 | } 1080 | g_iTotalFilesToProcess = iCount; 1081 | Loc_StackProcessingDecode(LC_OP_FLAG_RESET | LC_OP_FLAG_DELAY); 1082 | } 1083 | } 1084 | 1085 | public Action Timer_Loc_StackProcessingDecode(Handle timer, LC_OP_FLAG flags) 1086 | { 1087 | Loc_StackProcessingDecode(flags); 1088 | return Plugin_Continue; 1089 | } 1090 | 1091 | void Loc_StackProcessingDecode(LC_OP_FLAG flags) 1092 | { 1093 | g_bLcReady = false; 1094 | 1095 | float time; 1096 | bool bUpdated; 1097 | int iFileSize; 1098 | static float total_time, max_time; 1099 | static int iCount, total_file_size, max_size; 1100 | static char sFile[LC_MAX_LANG_FILE_LENGTH]; 1101 | 1102 | g_bLcDecodeInProgress = true; 1103 | 1104 | if( flags & LC_OP_FLAG_RESET ) { 1105 | total_time = 0.0; 1106 | max_time = 0.0; 1107 | iCount = 0; 1108 | total_file_size = 0; 1109 | max_size = 0; 1110 | } 1111 | if( flags & LC_OP_FLAG_DELAY ) { 1112 | CreateTimer(LC_THREAD_WAIT_TIME, Timer_Loc_StackProcessingDecode, LC_OP_FLAG_NONE); 1113 | return; 1114 | } 1115 | 1116 | while( time < LC_THREAD_EXECUTION_TIME ) 1117 | { 1118 | if( g_hLcStackEncoded.Empty ) 1119 | { 1120 | if( g_bLcProfiler ) 1121 | Loc_PrintProfiler(iCount, total_file_size, max_size, total_time, max_time, LC_OP_TYPE_DECODE); 1122 | 1123 | Loc_StackProcessingCache(LC_OP_FLAG_RESET | LC_OP_FLAG_DELAY); 1124 | return; 1125 | } 1126 | g_hLcStackEncoded.PopString(sFile, sizeof(sFile)); 1127 | 1128 | g_hLcProf.Start(); 1129 | bUpdated = Loc_DecodeFile(sFile, iFileSize); 1130 | g_hLcStackCache.PushString(sFile); 1131 | 1132 | g_hLcProf.Stop(); 1133 | time += g_hLcProf.Time; 1134 | 1135 | if( max_time < g_hLcProf.Time ) max_time = g_hLcProf.Time; 1136 | if( bUpdated ) { 1137 | total_file_size += iFileSize; 1138 | if( max_size < iFileSize ) max_size = iFileSize; 1139 | total_time += g_hLcProf.Time; 1140 | ++ iCount; 1141 | PrintToChatAll(">>> [Localizer] Decoded file [%i/%i]: %s", iCount, g_iTotalFilesToProcess, sFile); 1142 | PrintToServer(">>> [Localizer] Decoded file [%i/%i]: %s", iCount, g_iTotalFilesToProcess, sFile); 1143 | } 1144 | } 1145 | CreateTimer(LC_THREAD_WAIT_TIME, Timer_Loc_StackProcessingDecode, LC_OP_FLAG_NONE); 1146 | } 1147 | 1148 | public Action Timer_Loc_StackProcessingCache(Handle timer, LC_OP_FLAG flags) 1149 | { 1150 | Loc_StackProcessingCache(flags); 1151 | return Plugin_Continue; 1152 | } 1153 | 1154 | void Loc_StackProcessingCache(LC_OP_FLAG flags) 1155 | { 1156 | float time; 1157 | bool bUpdated; 1158 | int iFileSize; 1159 | static float total_time, max_time; 1160 | static int iCount, total_file_size, max_size; 1161 | static char sFile[LC_MAX_LANG_FILE_LENGTH]; 1162 | 1163 | if( flags & LC_OP_FLAG_RESET ) { 1164 | total_time = 0.0; 1165 | max_time = 0.0; 1166 | total_file_size = 0; 1167 | max_size = 0; 1168 | iCount = 0; 1169 | } 1170 | if( flags & LC_OP_FLAG_DELAY ) { 1171 | CreateTimer(LC_THREAD_WAIT_TIME, Timer_Loc_StackProcessingCache, LC_OP_FLAG_NONE); 1172 | return; 1173 | } 1174 | 1175 | while( time < LC_THREAD_EXECUTION_TIME ) 1176 | { 1177 | if( g_hLcStackCache.Empty ) 1178 | { 1179 | g_bLcDecodeInProgress = false; 1180 | 1181 | if( g_bLcProfiler ) 1182 | Loc_PrintProfiler(iCount, total_file_size, max_size, total_time, max_time, LC_OP_TYPE_CACHE ); 1183 | 1184 | if( !g_hLcStackEncoded.Empty ) 1185 | { 1186 | Loc_StackProcessingDecode(LC_OP_FLAG_DELAY); 1187 | } 1188 | else { 1189 | if( g_iLcInstallMode & LC_INSTALL_MODE_DATABASE ) 1190 | { 1191 | if( g_bLcProfiler ) { 1192 | g_hLcProf.Start(); 1193 | } 1194 | if( !SQL_Loc_InsertBatch(0, "", "", .bFinishPending = true) ) 1195 | { 1196 | OnPhrasesProcessingCompleted_CallDelayed(); 1197 | } 1198 | } 1199 | else if( g_iLcInstallMode & LC_INSTALL_MODE_FULLCACHE ) 1200 | { 1201 | Forward_OnPhrasesProcessingCompleted(); 1202 | } 1203 | else if( g_iLcInstallMode & LC_INSTALL_MODE_TRANSLATIONFILE ) 1204 | { 1205 | Loc_LoadTranslations(); 1206 | Forward_OnPhrasesProcessingCompleted(); 1207 | } 1208 | } 1209 | return; 1210 | } 1211 | g_hLcStackCache.PopString(sFile, sizeof(sFile)); 1212 | g_hLcProf.Start(); 1213 | bUpdated = Loc_CacheFile(sFile, iFileSize); 1214 | g_hLcProf.Stop(); 1215 | 1216 | time += g_hLcProf.Time; 1217 | 1218 | if( max_time < g_hLcProf.Time ) max_time = g_hLcProf.Time; 1219 | if( bUpdated ) 1220 | { 1221 | total_time += g_hLcProf.Time; 1222 | total_file_size += iFileSize; 1223 | if( max_size < iFileSize ) max_size = iFileSize; 1224 | ++ iCount; 1225 | } 1226 | } 1227 | CreateTimer(LC_THREAD_WAIT_TIME, Timer_Loc_StackProcessingCache, LC_OP_FLAG_NONE); 1228 | } 1229 | 1230 | void Loc_PrintProfiler(int iCount, int total_file_size, int max_size, float time, float max_time, LC_OP_TYPE op_type) 1231 | { 1232 | float size_max_mb = max_size / 1024.0 / 1024.0; 1233 | float size_mb = total_file_size / 1024.0 / 1024.0; 1234 | float speed = size_mb / time; 1235 | PrintToServer(">>> [Localizer] Profiler report for: %s (%i items, %.1f MB), max file: %.2f MB", op_type == LC_OP_TYPE_DECODE ? "Decode" : "Cache", iCount, size_mb, size_max_mb); 1236 | PrintToServer("avg speed: %.2f MB/sec.", speed); 1237 | PrintToServer("max time per file: %.2f sec.", max_time); 1238 | PrintToServer("total time elapsed: %.2f sec.", time); 1239 | if( iCount > 0 ) { 1240 | if( size_max_mb + 0.2*size_max_mb > speed ) { // assuming, 1 sec. is the maximum time allowed (-20% reserved) 1241 | PrintToServer("Performance - problem !!! (%.2f MB +20%% > %.2f MB/sec)", size_max_mb, speed); 1242 | } 1243 | else { 1244 | PrintToServer("Performance - OK."); 1245 | } 1246 | } 1247 | } 1248 | 1249 | bool Loc_CacheFile(char[] sFile, int &iFileSize) 1250 | { 1251 | bool shouldParse; 1252 | int iOldSize, value; 1253 | char sourceFile[LC_MAX_PATH_LANG_DECODED]; 1254 | bool use_fallback = g_hMapLcEnglishFile.GetValue(sFile, value); 1255 | FormatEx(sourceFile, sizeof(sourceFile), "%s/%s", LC_RESOURCE_DECODED_DIR, sFile); 1256 | iFileSize = FileSize(sourceFile); 1257 | 1258 | switch( g_iLcInstallMode ) 1259 | { 1260 | case LC_INSTALL_MODE_DATABASE: 1261 | { 1262 | iOldSize = SQL_Loc_GetIndex(sFile); 1263 | if( iOldSize != iFileSize ) 1264 | { 1265 | shouldParse = true; 1266 | } 1267 | } 1268 | case LC_INSTALL_MODE_FULLCACHE: 1269 | { 1270 | shouldParse = true; 1271 | } 1272 | case LC_INSTALL_MODE_TRANSLATIONFILE: 1273 | { 1274 | // TODO 1275 | } 1276 | } 1277 | 1278 | if( shouldParse ) 1279 | { 1280 | Loc_ParseFile(sourceFile, use_fallback); 1281 | 1282 | if( g_iLcInstallMode == LC_INSTALL_MODE_DATABASE ) 1283 | { 1284 | SQL_Loc_SetIndex(sFile, iFileSize); 1285 | } 1286 | else if( g_iLcInstallMode == LC_INSTALL_MODE_TRANSLATIONFILE ) 1287 | { 1288 | // TODO 1289 | } 1290 | return true; 1291 | } 1292 | return false; 1293 | } 1294 | 1295 | void Loc_ParseFile(char[] sFile, bool use_fallback) 1296 | { 1297 | int pa /*absolute*/, pr /*relative*/, pc /*current*/, ps = -1 /*start*/, tok /*token number*/, qts /*quote started?*/; 1298 | int p[2] = {-1, ...} /*result tok #0,1*/, p_len[2] /*length of found*/; 1299 | int iLangIndex = -1; 1300 | char str[LC_MAX_TRANSLATION_LENGTH]; 1301 | File hr = OpenFile(sFile, "rt"); 1302 | if( hr ) 1303 | { 1304 | while( !hr.EndOfFile() && hr.ReadLine(str, sizeof(str)) ) 1305 | { 1306 | pa = 0; ps = -1; p[0] = -1; p[1] = -1; qts = 0; tok = 0; 1307 | 1308 | while( -1 != (pr = FindCharInString(str[pa], '"')) ) 1309 | { 1310 | pc = pa+pr; 1311 | if( ps == -1 ) ps = pc + 1; 1312 | if( qts ) { 1313 | if( str[pc-1] != '\\' ) { // skip screened quote 1314 | str[pc] = '\0'; 1315 | p[tok] = ps; 1316 | p_len[tok] = pc - ps; 1317 | if( tok == 1 ) break; 1318 | qts = 0; 1319 | ps = -1; 1320 | ++ tok; 1321 | } 1322 | } 1323 | else { 1324 | qts = 1; 1325 | } 1326 | pa += pr + 1; 1327 | } 1328 | // TODO: Add multiline translations support 1329 | if( p[0] != -1 && p[1] != -1 && str[p[0]] != 0 && str[p[1]] != 0 && !( p_len[1] == 1 && str[p[1]] == ' ') ) 1330 | { 1331 | if( Loc_OnProcessKeyValue(str[p[0]], str[p[1]], iLangIndex, use_fallback) == Plugin_Stop ) 1332 | { 1333 | PrintToServer("[Localizer] [WARN] File %s is skipped. Reason: language name is not recognized.", sFile); 1334 | break; 1335 | } 1336 | } 1337 | } 1338 | delete hr; 1339 | } 1340 | } 1341 | 1342 | Action Loc_OnProcessKeyValue(char[] key, char[] value, int &lang_num, bool use_fallback) 1343 | { 1344 | int indent; 1345 | bool isFallback; 1346 | 1347 | if( key[0] == '[' ) 1348 | { 1349 | if( use_fallback ) // for CSGO: "[english]phrase" "translation" 1350 | { 1351 | indent = 9; 1352 | isFallback = true; 1353 | } 1354 | else { 1355 | return Plugin_Continue; 1356 | } 1357 | } 1358 | 1359 | if( lang_num == -1 ) 1360 | { 1361 | if( strcmp(key, LC_LANGUAGE_KEY) == 0 ) 1362 | { 1363 | lang_num = GetLanguageByName(value); 1364 | if( lang_num == -1 ) 1365 | return Plugin_Stop; 1366 | } 1367 | } 1368 | else { 1369 | if( key[indent+0] == '#' ) 1370 | { 1371 | Loc_AddPhrase(isFallback ? LANG_SERVER : lang_num, key[indent+1], value, false, true, true); 1372 | } 1373 | else { 1374 | Loc_AddPhrase(isFallback ? LANG_SERVER : lang_num, key[indent+0], value, false, true, true); 1375 | } 1376 | } 1377 | return Plugin_Continue; 1378 | } 1379 | 1380 | void Loc_AddPhrase(int lang_num, char[] phrase, char[] translation, bool bOverwrite = false, bool bAsync = true, bool batch = false) 1381 | { 1382 | if( g_iLcInstallMode & LC_INSTALL_MODE_DATABASE ) 1383 | { 1384 | if( batch ) // walkaround for IDatabase handles leak #1505 1385 | { 1386 | SQL_Loc_InsertBatch(lang_num, phrase, translation, bOverwrite); 1387 | } 1388 | else { 1389 | SQL_Loc_Insert(lang_num, phrase, translation, bOverwrite, bAsync); 1390 | } 1391 | } 1392 | else if( g_iLcInstallMode & LC_INSTALL_MODE_FULLCACHE ) 1393 | { 1394 | g_hMapLcPhrase[lang_num].SetString(phrase, translation, bOverwrite); 1395 | } 1396 | else if( g_iLcInstallMode & LC_INSTALL_MODE_TRANSLATIONFILE ) 1397 | { 1398 | // TODO 1399 | } 1400 | } 1401 | 1402 | bool Loc_DecodeFile(char[] sFile, int &iFileSize) 1403 | { 1404 | char targetFile[LC_MAX_PATH_LANG_DECODED]; 1405 | char sourceFile[LC_MAX_PATH_LANG_ENCODED]; 1406 | FormatEx(sourceFile, sizeof(sourceFile), "%s/%s", LC_RESOURCE_ENCODED_DIR, sFile); 1407 | FormatEx(targetFile, sizeof(targetFile), "%s/%s", LC_RESOURCE_DECODED_DIR, sFile); 1408 | int iOldSize; 1409 | iFileSize = FileSize(sourceFile, true); 1410 | g_hMapLcStamp.GetValue(sFile, iOldSize); // Valve update happened ? 1411 | 1412 | if( iFileSize != iOldSize || !FileExists(targetFile) ) 1413 | { 1414 | Loc_ConvertFile_UTF16LE_UTF8(sourceFile, targetFile); 1415 | g_hMapLcStamp.SetValue(sFile, iFileSize, true); 1416 | g_bLcIndexChanged = true; 1417 | return true; 1418 | } 1419 | iFileSize = 0; 1420 | return false; 1421 | } 1422 | 1423 | bool Loc_ConvertFile_UTF16LE_UTF8(char[] sourceFile, char[] targetFile) 1424 | { 1425 | //LogError("Decode: %s", sourceFile); 1426 | 1427 | bool use_valve_fs = !(g_iLcEngine == Engine_CSGO); // walkaround CSGO engine (SM?) ERROR_HANDLE_EOF bug #1567 1428 | 1429 | File hr = OpenFile(sourceFile, "rb", use_valve_fs); 1430 | 1431 | if( !hr && g_iLcEngine == Engine_CSGO ) 1432 | { 1433 | // FIXIT: remove this hardcoded string walkaround as soon as ERROR_HANDLE_EOF bug will be fixed 1434 | int n = FindCharInString(sourceFile, '/', true); 1435 | if( n != -1 ) 1436 | { 1437 | Format(sourceFile, LC_MAX_PATH_LANG_ENCODED, "%s/%s", LC_RESOURCE_ENCODED_PL_DIR, sourceFile[n+1]); 1438 | hr = OpenFile(sourceFile, "rb", false); 1439 | } 1440 | } 1441 | if( hr ) 1442 | { 1443 | // TODO: Add profiler control 1444 | 1445 | File hw = OpenFile(targetFile, "wb", false); 1446 | if( hw ) 1447 | { 1448 | // Note: it seems various buffer sizes doesn't affect performance too much 1449 | // 1450 | int bytesRead, bytesWrite, buff[512/* MUST MOD 4*/], out[sizeof(buff)*3/* MUST MULTIPLY 3*/]; 1451 | 1452 | while( !hr.EndOfFile() ) 1453 | { 1454 | bytesRead = hr.Read(buff, sizeof(buff), 2); 1455 | Loc_WideCharToMultiByte(buff, bytesRead, out, bytesWrite); 1456 | hw.Write(out, bytesWrite, 1); 1457 | } 1458 | delete hw; 1459 | } 1460 | delete hr; 1461 | } 1462 | return true; 1463 | } 1464 | 1465 | // Note: Little Endian only. 1466 | // 1467 | void Loc_WideCharToMultiByte(const int[] input, int maxlen, int[] output, int &bytesWrite) 1468 | { 1469 | static int i, n, high_surrogate; 1470 | n = 0; 1471 | high_surrogate = 0; 1472 | for( i = 0; i < maxlen; i++ ) { 1473 | if( high_surrogate ) // for characters in range 0x10000 <= X <= 0x10FFFF 1474 | { 1475 | int data; 1476 | data = ((high_surrogate - 0xD800) << 10) + (input[i]/*Low surrogate*/ - 0xDC00) + 0x10000; 1477 | output[n++] = ((data >> 18) & 0x07) | 0xF0; 1478 | output[n++] = ((data >> 12) & 0x3F) | 0x80; 1479 | output[n++] = ((data >> 6) & 0x3F) | 0x80; 1480 | output[n++] = (data & 0x3F) | 0x80; 1481 | high_surrogate = 0; 1482 | } 1483 | else if( input[i] < 0x80 ) { 1484 | output[n++] = input[i]; 1485 | } 1486 | else if( input[i] < 0x800 ) { 1487 | output[n++] = ((input[i] >> 6) & 0x1F) | 0xC0; 1488 | output[n++] = (input[i] & 0x3F) | 0x80; 1489 | } else if( input[i] <= 0xFFFF ) { 1490 | if( 0xD800 <= input[i] <= 0xDFFF ) { 1491 | high_surrogate = input[i]; 1492 | continue; 1493 | } 1494 | output[n++] = ((input[i] >> 12) & 0x0F) | 0xE0; 1495 | output[n++] = ((input[i] >> 6) & 0x3F) | 0x80; 1496 | output[n++] = (input[i] & 0x3F) | 0x80; 1497 | } 1498 | } 1499 | bytesWrite = n; 1500 | } 1501 | // #endregion Parser } 1502 | 1503 | //{ #region Helpers 1504 | /* ============================================================================== 1505 | Helpers 1506 | ================================================================================*/ 1507 | 1508 | void Loc_DeleteDirectory(char[] sDir) 1509 | { 1510 | DirectoryListing hDir; 1511 | char sFile[PLATFORM_MAX_PATH]; 1512 | FileType fileType; 1513 | 1514 | hDir = OpenDirectory(sDir, false); 1515 | if( hDir ) 1516 | { 1517 | while( hDir.GetNext(sFile, sizeof(sFile), fileType) ) 1518 | { 1519 | if( fileType == FileType_File ) 1520 | { 1521 | Format(sFile, sizeof(sFile), "%s/%s", sDir, sFile); 1522 | DeleteFile(sFile, false); 1523 | } 1524 | } 1525 | delete hDir; 1526 | } 1527 | RemoveDir(sDir); 1528 | } 1529 | 1530 | bool Loc_CopyFile(char[] SourceFile, char[] TargetFile) 1531 | { 1532 | bool result = false; 1533 | Handle hr = OpenFile(SourceFile, "rb", false); 1534 | if( hr ) 1535 | { 1536 | Handle hw = OpenFile(TargetFile, "wb", false); 1537 | if( hw ) 1538 | { 1539 | int bytesRead, buff[64]; 1540 | 1541 | while( !IsEndOfFile(hr) ) 1542 | { 1543 | bytesRead = ReadFile(hr, buff, sizeof(buff), 1); 1544 | WriteFile(hw, buff, bytesRead, 1); 1545 | } 1546 | delete hw; 1547 | result = true; 1548 | } 1549 | delete hr; 1550 | } 1551 | return result; 1552 | } 1553 | 1554 | bool Loc_Dump(int lang_num, int &count = 0) 1555 | { 1556 | char translation[LC_MAX_TRANSLATION_LENGTH]; 1557 | char phrase[LC_MAX_PHRASE_LENGTH]; 1558 | char code[8], name[1]; 1559 | 1560 | GetLanguageInfo(lang_num, code, sizeof(code), name, sizeof(name)); 1561 | 1562 | StringMapSnapshot hSnap = g_hMapLcPhrase[lang_num].Snapshot(); 1563 | 1564 | char dumpPath[PLATFORM_MAX_PATH]; 1565 | BuildPath(Path_SM, dumpPath, PLATFORM_MAX_PATH, "translations/localizer"); 1566 | if( !DirExists(dumpPath) ) 1567 | { 1568 | CreateDirectory(dumpPath, 0o755); 1569 | } 1570 | if( !StrEqual(code, "en") ) 1571 | { 1572 | Format(dumpPath, PLATFORM_MAX_PATH, "%s/%s", dumpPath, code); 1573 | 1574 | if( !DirExists(dumpPath) ) 1575 | { 1576 | CreateDirectory(dumpPath, 0o755); 1577 | } 1578 | } 1579 | 1580 | char dstFile[PLATFORM_MAX_PATH]; 1581 | char dstDumpFile[PLATFORM_MAX_PATH]; 1582 | BuildPath(Path_SM, dstFile, PLATFORM_MAX_PATH, "translations/%s/%s", StrEqual(code, "en") ? "" : code, LC_TRANSLATION_FILE); 1583 | BuildPath(Path_SM, dstDumpFile, PLATFORM_MAX_PATH, "translations/localizer/%s/%s", StrEqual(code, "en") ? "" : code, LC_TRANSLATION_FILE); 1584 | 1585 | File hFile = OpenFile(dstFile, "wt"); 1586 | if( !hFile ) { 1587 | LogError("Cannot open file for writing: %s", dstFile); 1588 | return false; 1589 | } 1590 | 1591 | hFile.WriteLine("\"Phrases\"\n{"); 1592 | 1593 | char buffer[LC_MAX_PHRASE_LENGTH+24]; 1594 | 1595 | for( int i = 0; i < hSnap.Length; i++ ) 1596 | { 1597 | hSnap.GetKey(i, phrase, sizeof(phrase)); 1598 | if( phrase[0] ) 1599 | { 1600 | g_hMapLcPhrase[lang_num].GetString(phrase, translation, sizeof(translation)); 1601 | 1602 | // WriteLine's own format buffer cannot hold more than ~2048 bytes, so we're forced to use WriteString() here instead 1603 | FormatEx(buffer, sizeof(buffer), "\t\"#%s\"\n\t{\n\t\t\"%s\"\t\"", phrase, code); 1604 | hFile.WriteString(buffer, false); 1605 | hFile.WriteString(translation, false); 1606 | hFile.WriteString("\"\n\t}\n", false); 1607 | ++ count; 1608 | } 1609 | } 1610 | hFile.WriteLine("}"); 1611 | delete hFile; 1612 | delete hSnap; 1613 | 1614 | Loc_CopyFile(dstFile, dstDumpFile); 1615 | return true; 1616 | } 1617 | 1618 | int Loc_GetLanguageNum(int client = LANG_SERVER, char[] lang_name = "", char[] lang_code = "") 1619 | { 1620 | int lang_num; 1621 | if( lang_name[0] ) 1622 | { 1623 | lang_num = GetLanguageByName(lang_name); 1624 | } 1625 | else if( lang_code[0] ) 1626 | { 1627 | lang_num = GetLanguageByCode(lang_code); 1628 | } 1629 | else { 1630 | lang_num = client == 0 ? GetServerLanguage() : GetClientLanguage(client); 1631 | } 1632 | return lang_num; 1633 | } 1634 | 1635 | void Loc_ReplacePhrases(char[] str, int len, int lang_num) 1636 | { 1637 | if( lang_num == -1 ) 1638 | return; 1639 | 1640 | static char phrase[LC_MAX_PHRASE_LENGTH]; 1641 | static char translation[LC_MAX_TRANSLATION_LENGTH]; 1642 | 1643 | int matches = g_rLcCaptures.MatchAll(str); 1644 | if( matches ) 1645 | { 1646 | for( int i = matches - 1; i >= 0; i-- ) 1647 | { 1648 | g_rLcCaptures.GetSubString(0, phrase, sizeof(phrase), i); 1649 | 1650 | if( Loc_GetPhrase(lang_num, phrase[1], translation, sizeof(translation), true) ) 1651 | { 1652 | ReplaceStringEx(str, len, phrase, translation); 1653 | } 1654 | } 1655 | } 1656 | } 1657 | 1658 | stock bool Loc_GetPhrase(int lang_num, char[] phrase, char[] translation, int maxlength, bool default_on_fail = false, bool use_cache = true) 1659 | { 1660 | static char tr[LC_MAX_TRANSLATION_LENGTH]; 1661 | static char tr_def[LC_MAX_TRANSLATION_LENGTH]; 1662 | 1663 | bool useL2 = use_cache || ( g_iLcInstallMode & LC_INSTALL_MODE_FULLCACHE ); 1664 | 1665 | if( useL2 && g_hMapLcPhrase[lang_num].GetString(phrase, translation, maxlength) ) 1666 | { 1667 | return true; 1668 | } 1669 | else { 1670 | if( g_iLcInstallMode & LC_INSTALL_MODE_DATABASE ) 1671 | { 1672 | if( SQL_Loc_Query(lang_num, phrase, translation, maxlength) ) 1673 | { 1674 | if( use_cache ) 1675 | { 1676 | g_hMapLcPhrase[lang_num].SetString(phrase, translation); 1677 | 1678 | if( lang_num == LANG_SERVER ) 1679 | { 1680 | strcopy(tr_def, LC_MAX_TRANSLATION_LENGTH, translation); 1681 | } 1682 | else { 1683 | Loc_GetPhrase(LANG_SERVER, phrase, tr_def, LC_MAX_TRANSLATION_LENGTH); 1684 | } 1685 | 1686 | for( int i = 0; i < GetLanguageCount(); i++ ) // pre-cache all languages of this phrase in advance 1687 | { 1688 | if( i != lang_num ) 1689 | { 1690 | if( SQL_Loc_Query(i, phrase, tr, sizeof(tr)) ) 1691 | { 1692 | g_hMapLcPhrase[i].SetString(phrase, tr); 1693 | } 1694 | else { 1695 | g_hMapLcPhrase[i].SetString(phrase, tr_def); 1696 | } 1697 | } 1698 | } 1699 | } 1700 | return true; 1701 | } 1702 | else { 1703 | if( default_on_fail ) 1704 | { 1705 | return Loc_GetPhrase(LANG_SERVER, phrase, translation, maxlength); 1706 | } 1707 | } 1708 | } 1709 | else if( g_iLcInstallMode & LC_INSTALL_MODE_FULLCACHE ) 1710 | { 1711 | if( default_on_fail ) 1712 | { 1713 | if( lang_num != LANG_SERVER ) 1714 | { 1715 | if( g_hMapLcPhrase[LANG_SERVER].GetString(phrase, translation, maxlength) ) 1716 | { 1717 | return true; 1718 | } 1719 | } 1720 | } 1721 | } 1722 | } 1723 | translation[0] = 0; 1724 | return false; 1725 | } 1726 | 1727 | bool Loc_LoadTranslations() 1728 | { 1729 | char dstFile[PLATFORM_MAX_PATH]; 1730 | BuildPath(Path_SM, dstFile, PLATFORM_MAX_PATH, "translations/%s", LC_TRANSLATION_FILE); 1731 | 1732 | if( FileExists(dstFile) ) 1733 | { 1734 | g_hLcProf.Start(); 1735 | LoadTranslations(LC_TRANSLATION_FILE); 1736 | InsertServerCommand("sm_reload_translations"); 1737 | ServerExecute(); 1738 | g_hLcProf.Stop(); 1739 | #if defined LC_PROFILER 1740 | PrintToServer(">> Profiler report for: %s: %.2f sec.", "LoadTranslations", g_hLcProf.Time); 1741 | #endif 1742 | return true; 1743 | } 1744 | LogError("Attempt to load translation file without dumping first: %s", dstFile); 1745 | return false; 1746 | } 1747 | 1748 | void Loc_RemoveTranslationFiles() 1749 | { 1750 | char code[8], name[1], dstFile[PLATFORM_MAX_PATH]; 1751 | 1752 | for( int i = 0; i < GetLanguageCount(); i++ ) 1753 | { 1754 | GetLanguageInfo(i, code, sizeof(code), name, sizeof(name)); 1755 | BuildPath(Path_SM, dstFile, PLATFORM_MAX_PATH, "translations/%s/%s", StrEqual(code, "en") ? "" : code, LC_TRANSLATION_FILE); 1756 | DeleteFile(dstFile); 1757 | } 1758 | } 1759 | 1760 | char[] Loc_InstallMode_ToString() 1761 | { 1762 | char str[32]; 1763 | str = 1764 | g_iLcInstallMode == LC_INSTALL_MODE_DATABASE ? "DATABASE" : 1765 | g_iLcInstallMode == LC_INSTALL_MODE_FULLCACHE ? "FULLCACHE" : 1766 | g_iLcInstallMode == LC_INSTALL_MODE_TRANSLATIONFILE ? "TRANSLATIONFILE" : 1767 | g_iLcInstallMode == LC_INSTALL_MODE_CUSTOM ? "SKIP" : 1768 | "Unknown"; 1769 | return str; 1770 | } 1771 | // #endregion Helpers } 1772 | 1773 | //{ #region Database 1774 | /* ============================================================================== 1775 | Database 1776 | ================================================================================*/ 1777 | 1778 | void SQL_Loc_DB_Connect(bool uninstall = false) 1779 | { 1780 | char error[256]; 1781 | if( !g_hLcDB ) 1782 | { 1783 | #if LC_USE_SQLITE 1784 | g_hLcDB = SQLite_UseDatabase("sourcemod-local", error, sizeof(error)); 1785 | #else 1786 | g_hLcDB = SQL_DefConnect(error, sizeof(error), true); 1787 | #endif 1788 | SQL_Loc_OnDBConnect(g_hLcDB, error, uninstall); 1789 | } 1790 | else { 1791 | SQL_Loc_OnDBConnect(g_hLcDB, "", uninstall); 1792 | } 1793 | g_iLcQueueTx = 0; 1794 | } 1795 | 1796 | public void SQL_Loc_OnDBConnect(Database db, const char[] error, any data) 1797 | { 1798 | if( db == null || error[0] ) 1799 | { 1800 | SetFailState("OnDBConnect: %s", error); 1801 | return; 1802 | } 1803 | if( data ) 1804 | { 1805 | SQL_Loc_RemoveTable(); 1806 | } 1807 | else { 1808 | Loc_CreateTables(); 1809 | } 1810 | } 1811 | 1812 | void Loc_CreateTables() 1813 | { 1814 | char query[640]; 1815 | FormatEx(query, sizeof(query), "CREATE TABLE IF NOT EXISTS `" ... LC_DATABASE_TABLE ... "` (\ 1816 | `lang` INTEGER UNSIGNED NOT NULL default 0, \ 1817 | `phrase` VARCHAR(%i) NOT NULL, \ 1818 | `translation` VARCHAR(%i) NOT NULL, \ 1819 | PRIMARY KEY (lang, phrase) );", LC_MAX_PHRASE_LENGTH, LC_MAX_TRANSLATION_LENGTH); 1820 | 1821 | g_hLcDB.Query(SQL_Loc_Callback_TableCreate, query); 1822 | } 1823 | 1824 | public void SQL_Loc_Callback_TableCreate(Database db, DBResultSet hQuery, const char[] error, any data) 1825 | { 1826 | if (!db || !hQuery) { LogError(error); return; } 1827 | 1828 | g_hLcDB.SetCharset("utf8"); 1829 | 1830 | Loc_GetPhraseFiles(""); 1831 | } 1832 | 1833 | /* 1834 | Queue INSERT/REPLACE query operation to transaction until safe limit per 1 tx will be reached. 1835 | 1836 | @bOverwrite - true is REPLACE, false - INSERT 1837 | @bFinishPending - to force execution of transaction 1838 | 1839 | @return - indicates whether we can wait a callback: 1840 | true if transaction executed and callback is awaiting, 1841 | false - when query is only queued or no queue exists to finish pending operation. 1842 | */ 1843 | bool SQL_Loc_InsertBatch(int lang_num, char[] phrase, char[] translation, bool bOverwrite = false, bool bFinishPending = false) 1844 | { 1845 | const int MAX_SAFE_QUERIES_PER_TRANSACTION = 1000; /* should be < 2388 otherwise #pragma dynamic increase is required 1846 | to prevent "Not enough space on the heap" error. 1847 | Split is also required to prevent IQuery handles leak (overflow) */ 1848 | 1849 | static Transaction tx; 1850 | static int queries; 1851 | 1852 | if( bFinishPending ) 1853 | { 1854 | if( queries ) 1855 | { 1856 | ++ g_iLcQueueTx; 1857 | g_hLcDB.Execute(tx, SQL_Loc_Tx_Success, SQL_Loc_Tx_Failure); 1858 | queries = 0; 1859 | return true; 1860 | } 1861 | return false; 1862 | } 1863 | if( !queries ) 1864 | { 1865 | tx = new Transaction(); 1866 | } 1867 | 1868 | static char query[2*(LC_MAX_PHRASE_LENGTH+LC_MAX_TRANSLATION_LENGTH+100)+1]; 1869 | 1870 | if( bOverwrite ) 1871 | { 1872 | g_hLcDB.Format(query, sizeof(query), "REPLACE INTO `" ... LC_DATABASE_TABLE ... "` (lang, phrase, translation) \ 1873 | VALUES (%i, '%s', '%s');", lang_num, phrase, translation); 1874 | } 1875 | else { 1876 | g_hLcDB.Format(query, sizeof(query), "INSERT INTO `" ... LC_DATABASE_TABLE ... "` (lang, phrase, translation) \ 1877 | VALUES (%i, '%s', '%s') ON CONFLICT DO NOTHING;", lang_num, phrase, translation); 1878 | } 1879 | 1880 | tx.AddQuery(query); 1881 | ++ queries; 1882 | 1883 | if( queries >= MAX_SAFE_QUERIES_PER_TRANSACTION ) 1884 | { 1885 | ++ g_iLcQueueTx; 1886 | g_hLcDB.Execute(tx, SQL_Loc_Tx_Success, SQL_Loc_Tx_Failure); 1887 | queries = 0; 1888 | return true; 1889 | } 1890 | return false; 1891 | } 1892 | 1893 | void SQL_Loc_Insert(int lang_num, char[] phrase, char[] translation, bool bOverwrite = false, bool bAsync = true) 1894 | { 1895 | static char query[2*(LC_MAX_PHRASE_LENGTH+LC_MAX_TRANSLATION_LENGTH+100)+1]; 1896 | 1897 | if( bOverwrite ) 1898 | { 1899 | g_hLcDB.Format(query, sizeof(query), "REPLACE INTO `" ... LC_DATABASE_TABLE ... "` (lang, phrase, translation) \ 1900 | VALUES (%i, '%s', '%s');", lang_num, phrase, translation); 1901 | } 1902 | else { 1903 | g_hLcDB.Format(query, sizeof(query), "INSERT INTO `" ... LC_DATABASE_TABLE ... "` (lang, phrase, translation) \ 1904 | VALUES (%i, '%s', '%s') ON CONFLICT DO NOTHING;", lang_num, phrase, translation); 1905 | } 1906 | 1907 | if( bAsync ) 1908 | { 1909 | g_hLcDB.Query(SQL_Loc_Callback, query); 1910 | } 1911 | else { 1912 | SQL_LockDatabase(g_hLcDB); 1913 | DBResultSet hQuery = SQL_Query(g_hLcDB, query); 1914 | SQL_UnlockDatabase(g_hLcDB); 1915 | delete hQuery; 1916 | } 1917 | } 1918 | 1919 | void SQL_Loc_RemovePhrase(char[] phrase) 1920 | { 1921 | char query[2*(LC_MAX_PHRASE_LENGTH+50)+1]; 1922 | g_hLcDB.Format(query, sizeof(query), "DELETE FROM `" ... LC_DATABASE_TABLE ... "` WHERE UPPER(phrase) = UPPER('%s');", phrase); 1923 | g_hLcDB.Query(SQL_Loc_Callback, query); 1924 | } 1925 | 1926 | void SQL_Loc_RemoveTable() 1927 | { 1928 | if( g_hLcDB ) 1929 | { 1930 | char query[100]; 1931 | FormatEx(query, sizeof(query), "DROP TABLE IF EXISTS `" ... LC_DATABASE_TABLE ... "`;"); 1932 | g_hLcDB.Query(SQL_Loc_Callback, query); // DBPrio_High is interfere here for some reason 1933 | } 1934 | else { 1935 | SQL_Loc_DB_Connect(true); 1936 | } 1937 | } 1938 | 1939 | void SQL_Loc_Callback(Database db, DBResultSet hQuery, const char[] error, any data) 1940 | { 1941 | if (!db || !hQuery) { LogError(error); return; } 1942 | } 1943 | 1944 | public bool SQL_Loc_Query(int lang_num, char[] phrase, char[] translation, int maxlength) 1945 | { 1946 | static DBStatement hUserStmt; 1947 | if( hUserStmt == null ) 1948 | { 1949 | char error[255]; 1950 | hUserStmt = SQL_PrepareQuery(g_hLcDB, "SELECT translation FROM `" ... LC_DATABASE_TABLE ... "` \ 1951 | WHERE lang = ? AND UPPER(phrase) = UPPER(?);", error, sizeof(error)); 1952 | } 1953 | 1954 | SQL_BindParamInt(hUserStmt, 0, lang_num, true); 1955 | SQL_BindParamString(hUserStmt, 1, phrase, false); 1956 | 1957 | SQL_LockDatabase(g_hLcDB); 1958 | SQL_Execute(hUserStmt); 1959 | SQL_UnlockDatabase(g_hLcDB); 1960 | 1961 | if( SQL_FetchRow(hUserStmt) ) 1962 | { 1963 | SQL_FetchString(hUserStmt, 0, translation, maxlength); 1964 | return true; 1965 | } 1966 | 1967 | /* // alternate 1968 | static char query[1024]; 1969 | Format(query, sizeof(query), 1970 | "SELECT translation FROM `" ... LC_DATABASE_TABLE ... "` WHERE lang = %i AND phrase = '%s';", lang_num, phrase); 1971 | 1972 | SQL_LockDatabase(g_hLcDB); 1973 | DBResultSet hQuery = SQL_Query(g_hLcDB, query); 1974 | SQL_UnlockDatabase(g_hLcDB); 1975 | 1976 | if( hQuery ) 1977 | { 1978 | if( SQL_FetchRow(hQuery) ) 1979 | { 1980 | SQL_FetchString(hQuery, 0, translation, maxlength); 1981 | return true; 1982 | } 1983 | } 1984 | */ 1985 | 1986 | translation[0] = 0; 1987 | return false; 1988 | } 1989 | 1990 | public void SQL_Loc_Tx_Success(Database db, any data, int numQueries, DBResultSet[] results, any[] queryData) 1991 | { 1992 | -- g_iLcQueueTx; 1993 | 1994 | if( !g_bLcDecodeInProgress && g_iLcQueueTx == 0 ) 1995 | { 1996 | OnPhrasesProcessingCompleted_CallDelayed(); 1997 | } 1998 | } 1999 | 2000 | public void SQL_Loc_Tx_Failure(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) 2001 | { 2002 | -- g_iLcQueueTx; 2003 | LogError(error); 2004 | } 2005 | 2006 | int SQL_Loc_GetIndex(char[] name) 2007 | { 2008 | char str[16]; 2009 | if( SQL_Loc_Query(0, name, str, sizeof(str)) ) 2010 | { 2011 | return StringToInt(str); 2012 | } 2013 | return 0; 2014 | } 2015 | 2016 | void SQL_Loc_SetIndex(char[] name, int iStamp, bool bFinishPending = false) 2017 | { 2018 | static StringMap hMap; 2019 | if( bFinishPending ) 2020 | { 2021 | if( !hMap ) 2022 | { 2023 | return; 2024 | } 2025 | char str[16], _name[128]; 2026 | StringMapSnapshot hSnap = hMap.Snapshot(); 2027 | if( hSnap ) 2028 | { 2029 | for( int i = 0; i < hSnap.Length; i++ ) 2030 | { 2031 | hSnap.GetKey(i, _name, sizeof(_name)); 2032 | hMap.GetValue(_name, iStamp); 2033 | IntToString(iStamp, str, sizeof(str)); 2034 | SQL_Loc_Insert(0, _name, str, true, true); 2035 | } 2036 | delete hSnap; 2037 | } 2038 | delete hMap; 2039 | } 2040 | else { 2041 | if( !hMap ) 2042 | { 2043 | hMap = new StringMap(); 2044 | } 2045 | hMap.SetValue(name, iStamp, true); // we must wait until transactions are really finished before populating the index 2046 | } 2047 | } 2048 | // #endregion Database } 2049 | --------------------------------------------------------------------------------