516 lines
15 KiB
C++
516 lines
15 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "glk/adrift/scare.h"
|
|
#include "glk/adrift/scprotos.h"
|
|
|
|
namespace Glk {
|
|
namespace Adrift {
|
|
|
|
/*
|
|
* Module notes:
|
|
*
|
|
* o Standard libc ctype.h functions vary their results according to the
|
|
* currently set locale. We want consistent Windows codepage 1252 or
|
|
* codepage 1251 (WinLatin1 or WinCyrillic) results. To get this, then,
|
|
* we have to define the needed functions internally to SCARE.
|
|
*/
|
|
|
|
/*
|
|
* All the ctype boolean and character tables contain 256 elements, one for
|
|
* each possible sc_char value. This is used to size arrays and to verify
|
|
* that range setting functions do not overrun array boundaries.
|
|
*/
|
|
enum { TABLE_SIZE = 256 };
|
|
|
|
|
|
/*
|
|
* loc_setrange_bool()
|
|
* loc_setranges_bool()
|
|
*
|
|
* Helpers for building ctype tables. Sets all elements from start to end
|
|
* inclusive to TRUE, and iterate this on a ranges array.
|
|
*/
|
|
static void loc_setrange_bool(sc_int start, sc_int end, sc_bool table[]) {
|
|
sc_int index_;
|
|
|
|
for (index_ = start; index_ <= end; index_++) {
|
|
assert(index_ > -1 && index_ < TABLE_SIZE);
|
|
table[index_] = TRUE;
|
|
}
|
|
}
|
|
|
|
static void loc_setranges_bool(const sc_int ranges[], sc_bool table[]) {
|
|
sc_int index_;
|
|
|
|
for (index_ = 0; ranges[index_] > -1; index_ += 2) {
|
|
assert(ranges[index_] <= ranges[index_ + 1]);
|
|
loc_setrange_bool(ranges[index_], ranges[index_ + 1], table);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_setrange_char()
|
|
* loc_setranges_char()
|
|
*
|
|
* Helpers for building ctype conversion tables. Sets all elements from start
|
|
* to end inclusive to their index value plus the given offset, and iterate
|
|
* this on a ranges array.
|
|
*/
|
|
static void loc_setrange_char(sc_int start, sc_int end, sc_int offset, sc_char table[]) {
|
|
sc_int index_;
|
|
|
|
for (index_ = start; index_ <= end; index_++) {
|
|
assert(index_ > -1 && index_ < TABLE_SIZE);
|
|
assert(index_ + offset > -1 && index_ + offset < TABLE_SIZE);
|
|
table[index_] = index_ + offset;
|
|
}
|
|
}
|
|
|
|
static void loc_setranges_char(const sc_int ranges[], sc_char table[]) {
|
|
sc_int index_;
|
|
|
|
for (index_ = 0; ranges[index_] > -1; index_ += 3) {
|
|
assert(ranges[index_] <= ranges[index_ + 1]);
|
|
loc_setrange_char(ranges[index_],
|
|
ranges[index_ + 1], ranges[index_ + 2], table);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* A locale consists of a name, ranges for each table, and signatures for
|
|
* autodetection based on the game's compilation date. This is the static
|
|
* data portion of a locale.
|
|
*/
|
|
enum { RANGES_LENGTH = 32 };
|
|
enum { SIGNATURE_COUNT = 24, SIGNATURE_LENGTH = 3 };
|
|
struct sc_locale_t {
|
|
const sc_char *const name;
|
|
const sc_int isspace_ranges[RANGES_LENGTH];
|
|
const sc_int isdigit_ranges[RANGES_LENGTH];
|
|
const sc_int isalpha_ranges[RANGES_LENGTH];
|
|
const sc_int toupper_ranges[RANGES_LENGTH];
|
|
const sc_int tolower_ranges[RANGES_LENGTH];
|
|
const sc_byte signature[SIGNATURE_COUNT][SIGNATURE_LENGTH];
|
|
};
|
|
|
|
|
|
/*
|
|
* The locale table set is built from a locale using its ranges. There is one
|
|
* table for each function, and a pointer to the locale used to construct the
|
|
* table, for synchronization with changed locales. This is the dynamic data
|
|
* portion of a locale.
|
|
*/
|
|
struct sc_locale_table_t {
|
|
const sc_locale_t *locale;
|
|
sc_bool isspace[TABLE_SIZE];
|
|
sc_bool isdigit[TABLE_SIZE];
|
|
sc_bool isalpha[TABLE_SIZE];
|
|
sc_char toupper[TABLE_SIZE];
|
|
sc_char tolower[TABLE_SIZE];
|
|
};
|
|
|
|
/*
|
|
* Define a single static locale table set. This set re-initializes if it
|
|
* detects a locale change.
|
|
*/
|
|
static sc_locale_table_t loc_locale_tables = {nullptr, {0}, {0}, {0}, {0}, {0}};
|
|
|
|
|
|
/*
|
|
* loc_synchronize_tables()
|
|
* loc_check_tables_synchronized()
|
|
*
|
|
* Initialize tables for a locale. And compare the locale tables to a locale
|
|
* and if not for the same locale, (re-)initialize.
|
|
*/
|
|
static void loc_synchronize_tables(const sc_locale_t *locale) {
|
|
/* Clear all tables and the locale pointer. */
|
|
memset(&loc_locale_tables, 0, sizeof(loc_locale_tables));
|
|
|
|
/* Set ranges and attach the new locale. */
|
|
loc_setranges_bool(locale->isspace_ranges, loc_locale_tables.isspace);
|
|
loc_setranges_bool(locale->isdigit_ranges, loc_locale_tables.isdigit);
|
|
loc_setranges_bool(locale->isalpha_ranges, loc_locale_tables.isalpha);
|
|
loc_setranges_char(locale->toupper_ranges, loc_locale_tables.toupper);
|
|
loc_setranges_char(locale->tolower_ranges, loc_locale_tables.tolower);
|
|
|
|
loc_locale_tables.locale = locale;
|
|
}
|
|
|
|
static void loc_check_tables_synchronized(const sc_locale_t *locale) {
|
|
if (locale != loc_locale_tables.locale)
|
|
loc_synchronize_tables(locale);
|
|
}
|
|
|
|
|
|
/*
|
|
* Locale for Latin1. The signatures in this locale are null since it is the
|
|
* default locale; no matching required. Also, none may be practical, as this
|
|
* locale works for a large number of Western European languages (though in
|
|
* practice, it seems that only English and French Adrift Latin1 games exist).
|
|
*/
|
|
static const sc_locale_t LATIN1_LOCALE = {
|
|
"Latin1",
|
|
{9, 13, 32, 32, 160, 160, -1},
|
|
{48, 57, -1},
|
|
{
|
|
65, 90, 97, 122, 192, 214, 216, 246, 248, 255, 138, 138, 140, 140,
|
|
142, 142, 154, 154, 156, 156, 158, 158, 159, 159, -1
|
|
},
|
|
{
|
|
0, TABLE_SIZE - 1, 0, 97, 122, -32, 224, 246, -32, 248, 254, -32, 154, 154, -16,
|
|
156, 156, -16, 158, 158, -16, 255, 255, -96, -1
|
|
},
|
|
{
|
|
0, TABLE_SIZE - 1, 0, 65, 90, 32, 192, 214, 32, 216, 222, 32, 138, 138, 16,
|
|
140, 140, 16, 142, 142, 16, 159, 159, 96, -1
|
|
},
|
|
{{0}}
|
|
};
|
|
|
|
|
|
/*
|
|
* Locale for Cyrillic. The signatures in this locale are month names in
|
|
* both mixed case and lowercase Russian Cyrillic.
|
|
*/
|
|
static const sc_locale_t CYRILLIC_LOCALE = {
|
|
"Cyrillic",
|
|
{9, 13, 32, 32, 160, 160, -1},
|
|
{48, 57, -1},
|
|
{
|
|
65, 90, 97, 122, 168, 168, 184, 184, 175, 175, 191, 191, 178, 179,
|
|
192, 255, -1
|
|
},
|
|
{
|
|
0, TABLE_SIZE - 1, 0, 97, 122, -32, 184, 184, -16, 191, 191, -16, 179, 179, -1,
|
|
224, 255, -32, -1
|
|
},
|
|
{
|
|
0, TABLE_SIZE - 1, 0, 65, 90, 32, 168, 168, 16, 175, 175, 16, 178, 178, 1,
|
|
192, 223, 32, -1
|
|
},
|
|
{ {223, 237, 226}, {212, 229, 226}, {204, 224, 240}, {192, 239, 240},
|
|
{204, 224, 233}, {200, 254, 237}, {200, 254, 235}, {192, 226, 227},
|
|
{209, 229, 237}, {206, 234, 242}, {205, 238, 255}, {196, 229, 234},
|
|
{255, 237, 226}, {244, 229, 226}, {236, 224, 240}, {224, 239, 240},
|
|
{236, 224, 233}, {232, 254, 237}, {232, 254, 235}, {224, 226, 227},
|
|
{241, 229, 237}, {238, 234, 242}, {237, 238, 255}, {228, 229, 234}
|
|
}
|
|
};
|
|
|
|
|
|
/* List of pointers to supported and available locales, NULL terminated. */
|
|
static const sc_locale_t *const AVAILABLE_LOCALES[] = {
|
|
&LATIN1_LOCALE,
|
|
&CYRILLIC_LOCALE,
|
|
nullptr
|
|
};
|
|
|
|
/*
|
|
* The locale for the game, set below explicitly or on game startup, and
|
|
* a flag to note if it's been set explicitly, to prevent autodetection from
|
|
* overwriting a manual setting.
|
|
*/
|
|
static const sc_locale_t *loc_locale = &LATIN1_LOCALE;
|
|
static sc_bool loc_is_autodetect_enabled = TRUE;
|
|
|
|
|
|
/*
|
|
* loc_locate_signature_in_date()
|
|
*
|
|
* Checks the format of the input date to ensure it matches the format
|
|
* "dd [Mm]mm yyyy". Returns the address of the month part of the string, or
|
|
* NULL if it doesn't match the expected format.
|
|
*/
|
|
static const sc_char *loc_locate_signature_in_date(const sc_char *date) {
|
|
sc_int day, year, converted;
|
|
sc_char signature[SIGNATURE_LENGTH + 1];
|
|
|
|
/* Clear signature, and convert using a scanf format. */
|
|
memset(signature, 0, sizeof(signature));
|
|
converted = sscanf(date, "%2ld %3[^ 0-9] %4ld", &day, signature, &year);
|
|
|
|
/* Valid if we converted three values, and month has three characters. */
|
|
if (converted == 3 && strlen(signature) == SIGNATURE_LENGTH)
|
|
return strstr(date, signature);
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_compare_locale_signatures()
|
|
*
|
|
* Search a locale's signatures for a match with the signature passed in.
|
|
* Returns TRUE if a match found, FALSE otherwise. Uses memcmp rather than
|
|
* any strcasecmp() variant because the signatures are in the locale's
|
|
* codepage, but the locale is not yet (by definition) set.
|
|
*/
|
|
static sc_bool loc_compare_locale_signatures(const char *signature, const sc_locale_t *locale) {
|
|
sc_int index_;
|
|
sc_bool is_matched;
|
|
|
|
/* Compare signatures, stopping on the first match found. */
|
|
is_matched = FALSE;
|
|
for (index_ = 0; index_ < SIGNATURE_COUNT; index_++) {
|
|
if (memcmp(locale->signature[index_],
|
|
signature, sizeof(locale->signature[0])) == 0) {
|
|
is_matched = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return is_matched;
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_find_matching_locale()
|
|
*
|
|
* Look at the incoming date, expected to be in the format "dd [Mm]mm yyyy",
|
|
* where "[Mm]mm" is a standard month abbreviation of the locale in which the
|
|
* Generator was run. Match this with locale signatures, and return the
|
|
* first locale that matches, or NULL if none match.
|
|
*/
|
|
static const sc_locale_t *loc_find_matching_locale(const sc_char *date,
|
|
const sc_locale_t *const *locales) {
|
|
const sc_char *signature;
|
|
const sc_locale_t *matched = nullptr;
|
|
|
|
/* Get the month part of date, and if valid, search locale signatures. */
|
|
signature = loc_locate_signature_in_date(date);
|
|
if (signature) {
|
|
const sc_locale_t *const *iterator;
|
|
|
|
/* Search for this signature in the locale's signatures. */
|
|
for (iterator = locales; *iterator; iterator++) {
|
|
if (loc_compare_locale_signatures(signature, *iterator)) {
|
|
matched = *iterator;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Return the matching locale, NULL if none matched. */
|
|
return matched;
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_detect_game_locale()
|
|
*
|
|
* Set an autodetected value for the locale based on looking at a game's
|
|
* compilation date.
|
|
*/
|
|
void loc_detect_game_locale(sc_prop_setref_t bundle) {
|
|
assert(bundle);
|
|
|
|
/* If an explicit locale has already been set, ignore the call. */
|
|
if (loc_is_autodetect_enabled) {
|
|
sc_vartype_t vt_key[1];
|
|
const sc_char *compile_date;
|
|
const sc_locale_t *matched;
|
|
|
|
/* Read the game's compilation date from the properties. */
|
|
vt_key[0].string = "CompileDate";
|
|
compile_date = prop_get_string(bundle, "S<-s", vt_key);
|
|
|
|
/* Search for a matching locale based on the game compilation date. */
|
|
matched = loc_find_matching_locale(compile_date, AVAILABLE_LOCALES);
|
|
|
|
/* If a locale matched, set the global locale to it. */
|
|
if (matched)
|
|
loc_locale = matched;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_ascii_tolower()
|
|
* loc_ascii_strncasecmp()
|
|
*
|
|
* The standard sc_strncasecmp() calls sc_tolower(), which is locale specific.
|
|
* This isn't a particular problem because it will set the Latin1 locale
|
|
* automatically before continuing. However, since locale names should always
|
|
* be in ascii anyway, it's slightly safer to just use an ascii-only version
|
|
* of this function.
|
|
*/
|
|
static sc_char loc_ascii_tolower(sc_char ch) {
|
|
return (ch >= 'A' && ch <= 'Z') ? ch - 'A' + 'a' : ch;
|
|
}
|
|
|
|
static sc_int loc_ascii_strncasecmp(const sc_char *s1, const sc_char *s2, sc_int n) {
|
|
sc_int index_;
|
|
|
|
for (index_ = 0; index_ < n; index_++) {
|
|
sc_int diff;
|
|
|
|
diff = loc_ascii_tolower(s1[index_]) - loc_ascii_tolower(s2[index_]);
|
|
if (diff < 0 || diff > 0)
|
|
return diff < 0 ? -1 : 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_set_locale()
|
|
* loc_get_locale()
|
|
*
|
|
* Set a locale explicitly from the name passed in, returning TRUE if a locale
|
|
* matched the name. Get the current locale, which may be the default locale
|
|
* if none yet set.
|
|
*/
|
|
sc_bool loc_set_locale(const sc_char *name) {
|
|
const sc_locale_t *matched = nullptr;
|
|
const sc_locale_t *const *iterator;
|
|
assert(name);
|
|
|
|
/*
|
|
* Search locales for a matching name, abbreviated if necessary. Stop on
|
|
* the first match found.
|
|
*/
|
|
for (iterator = AVAILABLE_LOCALES; *iterator; iterator++) {
|
|
const sc_locale_t *const locale = *iterator;
|
|
|
|
if (loc_ascii_strncasecmp(name, locale->name, strlen(name)) == 0) {
|
|
matched = locale;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* If matched, set the global locale, and lock out future autodetection. */
|
|
if (matched) {
|
|
loc_locale = matched;
|
|
loc_is_autodetect_enabled = FALSE;
|
|
}
|
|
|
|
return matched ? TRUE : FALSE;
|
|
}
|
|
|
|
const sc_char *loc_get_locale(void) {
|
|
return loc_locale->name;
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_debug_dump_new_line()
|
|
* loc_debug_dump_bool_table()
|
|
* loc_debug_dump_char_table()
|
|
* loc_debug_dump()
|
|
*
|
|
* Print out locale tables.
|
|
*/
|
|
static int loc_debug_dump_new_line(sc_int index_, sc_int count) {
|
|
return index_ < TABLE_SIZE - 1 && index_ % count == count - 1;
|
|
}
|
|
|
|
static void loc_debug_dump_bool_table(const sc_char *label, sc_int count, const sc_bool table[]) {
|
|
sc_int index_;
|
|
|
|
sc_trace("loc_locale_tables.%s = {\n ", label);
|
|
for (index_ = 0; index_ < TABLE_SIZE; index_++) {
|
|
sc_trace("%s%s", table[index_] ? "T" : "F",
|
|
loc_debug_dump_new_line(index_, count) ? "\n " : "");
|
|
}
|
|
sc_trace("\n}\n");
|
|
}
|
|
|
|
static void loc_debug_dump_char_table(const sc_char *label, sc_int count, const sc_char table[]) {
|
|
sc_int index_;
|
|
|
|
sc_trace("loc_locale_tables.%s = {\n ", label);
|
|
for (index_ = 0; index_ < TABLE_SIZE; index_++) {
|
|
sc_trace("%02lx%s", (sc_int)(sc_byte) table[index_],
|
|
loc_debug_dump_new_line(index_, count) ? "\n " : " ");
|
|
}
|
|
sc_trace("\n}\n");
|
|
}
|
|
|
|
void loc_debug_dump(void) {
|
|
sc_trace("Locale: debug dump follows...\n");
|
|
|
|
loc_check_tables_synchronized(loc_locale);
|
|
sc_trace("loc_locale_tables"
|
|
".locale->name = %s\n", loc_locale_tables.locale->name);
|
|
|
|
loc_debug_dump_bool_table("isspace", 64, loc_locale_tables.isspace);
|
|
loc_debug_dump_bool_table("isdigit", 64, loc_locale_tables.isdigit);
|
|
loc_debug_dump_bool_table("isalpha", 64, loc_locale_tables.isalpha);
|
|
loc_debug_dump_char_table("toupper", 16, loc_locale_tables.toupper);
|
|
loc_debug_dump_char_table("tolower", 16, loc_locale_tables.tolower);
|
|
}
|
|
|
|
|
|
/*
|
|
* loc_bool_template()
|
|
* loc_char_template()
|
|
*
|
|
* "Template" functions for locale variant ctype functions. Synchronize
|
|
* tables to the currently set locale, and return the value from the table.
|
|
*/
|
|
static sc_bool loc_bool_template(sc_char character, const sc_bool table[]) {
|
|
loc_check_tables_synchronized(loc_locale);
|
|
return table[(sc_byte) character];
|
|
}
|
|
|
|
static sc_char loc_char_template(sc_char character, const sc_char table[]) {
|
|
loc_check_tables_synchronized(loc_locale);
|
|
return table[(sc_byte) character];
|
|
}
|
|
|
|
|
|
/*
|
|
* sc_isspace()
|
|
* sc_isalpha()
|
|
* sc_isdigit()
|
|
* sc_tolower()
|
|
* sc_toupper()
|
|
*
|
|
* Public entry points into locale variant ctype functions.
|
|
*/
|
|
sc_bool sc_isspace(sc_char character) {
|
|
return loc_bool_template(character, loc_locale_tables.isspace);
|
|
}
|
|
|
|
sc_bool sc_isalpha(sc_char character) {
|
|
return loc_bool_template(character, loc_locale_tables.isalpha);
|
|
}
|
|
|
|
sc_bool sc_isdigit(sc_char character) {
|
|
return loc_bool_template(character, loc_locale_tables.isdigit);
|
|
}
|
|
|
|
sc_char sc_toupper(sc_char character) {
|
|
return loc_char_template(character, loc_locale_tables.toupper);
|
|
}
|
|
|
|
sc_char sc_tolower(sc_char character) {
|
|
return loc_char_template(character, loc_locale_tables.tolower);
|
|
}
|
|
|
|
} // End of namespace Adrift
|
|
} // End of namespace Glk
|